Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/silver-berries-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
'astro': minor
---

Adds experimental support for configurable log handlers.

This experimental feature provides better control over Astro's logging infrastructure by allowing users to replace the default console output with custom logging implementations (e.g., structured JSON). This is particularly useful for users using on-demand rendering and wishing to connect their log aggregation services such as Kibana, Logstash, CloudWatch, Grafana, or Loki.

By default, Astro provides three built-in log handlers (`json`, `node` and `console`), but you can also create your own.
#### JSON logging

JSON logging can be enabled via the CLI for the `build`, `dev`, and `sync` commands using the `experimentalJson` flag:


```js
// astro.config.mjs
import { defineConfig, logHandlers } from "astro/config";

export default defineConfig({
experimental: {
logger: logHandlers.json({
pretty: true,
level: 'warn'
})
}
})
```

#### Custom logger

You can also create your own custom logger by implementing the correct interface:

```js
// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
experimental: {
logger: {
entrypoint: "@org/custom-logger"
}
}
})
```

```ts
// @org/custom-logger.js
import type { AstroLoggerDestination, AstroLoggerMessage } from "astro";
import { matchesLevel } from "astor/logger";

function customLogger(level = 'info'): AstroLoggerDestination {
return {
write(message: AstroLoggerMessage) {
if (matchesLevel(message.level, level)) {
// write message somewhere
}
}
}
}
export default customLogger
```

For more information on enabling and using this feature in your project, see the [Experimental Logger docs](https://docs.astro.build/en/reference/experimental-flags/logger/).

For a complete overview, and to give feedback on this experimental API, see the [Custom logger RFC](https://github.com/withastro/roadmap/blob/logger/proposals/0059-custom-logger.md).
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/
/triage/
/.compiler/
dist/
dist-**/
temp/
*.tsbuildinfo
.DS_Store
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@
"./zod": "./dist/zod.js",
"./errors": "./dist/core/errors/userError.js",
"./middleware": "./dist/core/middleware/index.js",
"./virtual-modules/*": "./dist/virtual-modules/*"
"./virtual-modules/*": "./dist/virtual-modules/*",
"./logger": "./dist/core/logger/public.js",
"./logger/json": "./dist/core/logger/impls/json.js",
"./logger/node": "./dist/core/logger/impls/node.js",
"./logger/compose": "./dist/core/logger/impls/compose.js",
"./logger/console": "./dist/core/logger/impls/console.js"
},
"bin": {
"astro": "./bin/astro.mjs"
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/actions/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export type ActionAPIContext = Pick<
| 'session'
| 'cache'
| 'csp'
| 'logger'
>;

export type MaybePromise<T> = T | Promise<T>;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { getConfiguredImageService, getImage } from './internal.js';
export { getConfiguredImageService, getImage, verifyOptions } from './internal.js';
export { baseService, isLocalService } from './services/service.js';
export { hashTransform, propsToFilename } from './utils/hash.js';
export type { LocalImageProps, RemoteImageProps } from './types.js';
33 changes: 22 additions & 11 deletions packages/astro/src/cli/flags.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { Arguments } from 'yargs-parser';
import type { AstroLogger, AstroLogOptions } from '../core/logger/core.js';
import { createNodeLogger, nodeLogDestination } from '../core/logger/node.js';
import type { AstroLogger } from '../core/logger/core.js';
import { createNodeLoggerFromFlags } from '../core/logger/impls/node.js';
import { createJsonLoggerFromFlags } from '../core/logger/impls/json.js';
import type { AstroInlineConfig } from '../types/public/config.js';

// Alias for now, but allows easier migration to node's `parseArgs` in the future.
export type Flags = Arguments;

/** @deprecated Use AstroConfigResolver instead */
export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
return {
const inlineConfig: AstroInlineConfig = {
// Inline-only configs
configFile: typeof flags.config === 'string' ? flags.config : undefined,
mode: typeof flags.mode === 'string' ? flags.mode : undefined,
Expand All @@ -34,23 +35,33 @@ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
: [],
},
};

if (flags.experimentalJson) {
inlineConfig.experimental = {
logger: {
entrypoint: 'astro/logger/json',
},
};
}

return inlineConfig;
}

/**
* The `logging` is usually created from an `AstroInlineConfig`, but some flows like `add`
* doesn't read the AstroConfig directly, so we create a `logging` object from the CLI flags instead.
*/
export function createLoggerFromFlags(flags: Flags): AstroLogger {
const logging: AstroLogOptions = {
destination: nodeLogDestination,
level: 'info',
};
let logLevel = flags.level;

if (flags.verbose) {
logging.level = 'debug';
logLevel = 'debug';
} else if (flags.silent) {
logging.level = 'silent';
logLevel = 'silent';
}
if (flags.experimentalJson) {
return createJsonLoggerFromFlags({ logLevel });
} else {
return createNodeLoggerFromFlags({ logLevel });
}

return createNodeLogger({ logLevel: logging.level });
}
1 change: 1 addition & 0 deletions packages/astro/src/cli/help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const DEFAULT_HELP_PAYLOAD: HelpPayload = {
['--silent', 'Disable all logging.'],
['--version', 'Show the version number and exit.'],
['--help', 'Show this help message.'],
['--experimental-json', 'Enables JSON logging.'],
],
},
};
5 changes: 3 additions & 2 deletions packages/astro/src/cli/preferences/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js';
import dlv from '../../preferences/dlv.js';
import { coerce, isValidKey, type PreferenceKey } from '../../preferences/index.js';
import type { AstroSettings } from '../../types/astro.js';
import { createLoggerFromFlags, type Flags, flagsToAstroInlineConfig } from '../flags.js';
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
import { loadOrCreateNodeLogger } from '../../core/logger/load.js';

const { bgGreen, black, bold, dim, yellow } = colors;

Expand Down Expand Up @@ -68,8 +69,8 @@ export async function preferences(
}

const inlineConfig = flagsToAstroInlineConfig(flags);
const logger = createLoggerFromFlags(flags);
const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev');
const logger = await loadOrCreateNodeLogger(astroConfig, inlineConfig ?? {});
const settings = await createSettings(
astroConfig,
inlineConfig.logLevel,
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/config/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { validateConfig } from '../core/config/validate.js';
export { envField } from '../env/config.js';
export { defineConfig, getViteConfig } from './index.js';
export { sessionDrivers } from '../core/session/drivers.js';
export { logHandlers } from '../core/logger/handlers.js';

/**
* Return the configuration needed to use the Sharp-based image service
Expand Down
13 changes: 8 additions & 5 deletions packages/astro/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Keep this imports free of possible runtime imports
import type { UserConfig as ViteUserConfig, UserConfigFn as ViteUserConfigFn } from 'vite';
import type { FontProvider } from '../assets/fonts/types.js';
import { createRoutesList } from '../core/routing/create-manifest.js';
import { getPrerenderDefault } from '../prerender/utils.js';
import type { SessionDriverConfig, SessionDriverName } from '../core/session/types.js';
import type { AstroInlineConfig, AstroUserConfig, Locales } from '../types/public/config.js';

Expand Down Expand Up @@ -32,19 +31,23 @@ export function getViteConfig(
// Use dynamic import to avoid pulling in deps unless used
const [
{ mergeConfig },
{ createNodeLogger },
{ loadOrCreateNodeLogger },
{ resolveConfig, createSettings },
{ createVite },
{ runHookConfigSetup, runHookConfigDone },
{ createRoutesList },
{ getPrerenderDefault },
] = await Promise.all([
import('vite'),
import('../core/logger/node.js'),
import('../core/logger/load.js'),
import('../core/config/index.js'),
import('../core/create-vite.js'),
import('../integrations/hooks.js'),
import('../core/routing/create-manifest.js'),
import('../prerender/utils.js'),
]);
const logger = createNodeLogger(inlineAstroConfig);
const { astroConfig: config } = await resolveConfig(inlineAstroConfig, cmd);
const logger = await loadOrCreateNodeLogger(config, inlineAstroConfig);
let settings = await createSettings(config, inlineAstroConfig.logLevel, userViteConfig.root);
settings = await runHookConfigSetup({ settings, command: cmd, logger });
const routesList = await createRoutesList(
Expand Down
11 changes: 4 additions & 7 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { getDefaultClientDirectives } from '../core/client-directive/index.js';
import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schemas/index.js';
import { validateConfig } from '../core/config/validate.js';
import { createKey } from '../core/encryption.js';
import { AstroLogger } from '../core/logger/core.js';
import { nodeLogDestination } from '../core/logger/node.js';
import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js';
import { removeLeadingForwardSlash } from '../core/path.js';
import { RenderContext } from '../core/render-context.js';
Expand All @@ -26,6 +24,7 @@ import type {
SSRResult,
} from '../types/public/internal.js';
import { ContainerPipeline } from './pipeline.js';
import { createConsoleLogger } from '../core/logger/impls/console.js';

/**
* Public type, used for integrations to define a renderer for the container API
Expand Down Expand Up @@ -135,7 +134,6 @@ function createManifest(
onRequest: middleware ?? NOOP_MIDDLEWARE_FN,
};
}

const root = new URL(import.meta.url);
return {
rootDir: root,
Expand Down Expand Up @@ -182,6 +180,7 @@ function createManifest(
experimentalQueuedRendering: manifest?.experimentalQueuedRendering ?? {
enabled: false,
},
experimentalLogger: manifest?.experimentalLogger ?? undefined,
};
}

Expand Down Expand Up @@ -274,6 +273,7 @@ type AstroContainerManifest = Pick<
| 'assetsDir'
| 'image'
| 'experimentalQueuedRendering'
| 'experimentalLogger'
>;

type AstroContainerConstructor = {
Expand Down Expand Up @@ -301,10 +301,7 @@ export class experimental_AstroContainer {
astroConfig,
}: AstroContainerConstructor) {
this.#pipeline = ContainerPipeline.create({
logger: new AstroLogger({
level: 'info',
destination: nodeLogDestination,
}),
logger: createConsoleLogger({ level: 'error' }),
manifest: createManifest(manifest, renderers),
streaming,
renderers: renderers ?? manifest?.renderers ?? [],
Expand Down
39 changes: 30 additions & 9 deletions packages/astro/src/core/app/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import {
} from '../cookies/index.js';
import { getCookiesFromResponse } from '../cookies/response.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { AstroIntegrationLogger, AstroLogger } from '../logger/core.js';
import { AstroIntegrationLogger, type AstroLogger } from '../logger/core.js';
import { type CreateRenderContext, RenderContext } from '../render-context.js';
import { redirectTemplate } from '../routing/3xx.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
Expand Down Expand Up @@ -129,20 +128,29 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
manifest: SSRManifest;
manifestData: RoutesList;
pipeline: P;
adapterLogger: AstroIntegrationLogger;
#adapterLogger: AstroIntegrationLogger | undefined;
baseWithoutTrailingSlash: string;
logger: AstroLogger;
#router: Router;

get logger(): AstroLogger {
return this.pipeline.logger;
}

get adapterLogger(): AstroIntegrationLogger {
if (!this.#adapterLogger) {
this.#adapterLogger = new AstroIntegrationLogger(
this.logger.options,
this.manifest.adapterName,
);
}
return this.#adapterLogger;
}

constructor(manifest: SSRManifest, streaming = true, ...args: any[]) {
this.manifest = manifest;
this.manifestData = { routes: manifest.routes.map((route) => route.routeData) };
this.baseWithoutTrailingSlash = removeTrailingForwardSlash(manifest.base);
this.pipeline = this.createPipeline(streaming, manifest, ...args);
this.logger = new AstroLogger({
destination: consoleLogDestination,
level: manifest.logLevel,
});
this.adapterLogger = new AstroIntegrationLogger(this.logger.options, manifest.adapterName);
// This is necessary to allow running middlewares for 404 in SSR. There's special handling
// to return the host 404 if the user doesn't provide a custom 404
ensure404Route(this.manifestData);
Expand All @@ -159,6 +167,14 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
return this.adapterLogger;
}

/**
* Resets the cached adapter logger so it picks up a new logger instance.
* Used by BuildApp when the logger is replaced via setOptions().
*/
protected resetAdapterLogger(): void {
this.#adapterLogger = undefined;
}

getAllowedDomains() {
return this.manifest.allowedDomains;
}
Expand Down Expand Up @@ -390,6 +406,10 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
routeData,
}: RenderOptions = {},
): Promise<Response> {
// Lazily resolve the logger destination from the manifest on the first request.
// This swaps the user-configured logger destination (if any) into the shared
// AstroLogger instance before any logging occurs.
await this.pipeline.getLogger();
const timeStart = performance.now();
const url = new URL(request.url);
const redirect = this.redirectTrailingSlash(url.pathname);
Expand Down Expand Up @@ -590,6 +610,7 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
}
}

this.logger.flush();
Reflect.set(response, responseSentSymbol, true);
}

Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/app/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ import type { RoutesList } from '../../../types/astro.js';
import { req } from '../../messages/runtime.js';

export class DevApp extends BaseApp<NonRunnablePipeline> {
logger: AstroLogger;
constructor(manifest: SSRManifest, streaming = true, logger: AstroLogger) {
super(manifest, streaming, logger);
this.logger = logger;
}

createPipeline(
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/entrypoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export {
type LogRequestPayload,
} from '../base.js';
export { fromRoutingStrategy, toRoutingStrategy } from '../common.js';
export { createConsoleLogger } from '../logging.js';
export { createConsoleLogger } from '../../logger/impls/console.js';
export {
deserializeManifest,
deserializeRouteData,
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/entrypoints/virtual/dev.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { manifest } from 'virtual:astro:manifest';
import { DevApp } from '../../dev/app.js';
import { createConsoleLogger } from '../../logging.js';
import type { CreateApp, RouteInfo } from '../../types.js';
import type { RoutesList } from '../../../../types/astro.js';
import { createConsoleLogger } from '../../../logger/impls/console.js';

let currentDevApp: DevApp | null = null;

Expand Down
Loading
Loading