Skip to content
57 changes: 57 additions & 0 deletions .changeset/plenty-meals-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
'astro': minor
---

The custom logger feature introduced behind a flag in [v6.2.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#620) is no longer experimental and is available for general use.

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

Astro provides three built-in log handlers (`json`, `node`, and `console`), and you can also create your own.

#### JSON logging

```js
import { defineConfig, logHandlers } from 'astro/config';

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

#### Custom logger

```js
import { defineConfig } from 'astro/config';

export default defineConfig({
logger: {
entrypoint: '@org/custom-logger',
},
});
```

Additionally, `context.logger` is now always available in API routes and middleware, even without a custom logger configured.

If you were previously using this feature, please remove the experimental flag from your Astro config:

```diff
import { defineConfig } from 'astro/config';

export default defineConfig({
- experimental: {
- logger: {
- entrypoint: '@org/custom-logger',
- },
- },
+ logger: {
+ entrypoint: '@org/custom-logger',
+ },
});
```

If you have been waiting for stabilization before using custom loggers, you can now do so.

Please see the [Logger docs](https://docs.astro.build/en/reference/configuration-reference/#logger) for more about this feature.
4 changes: 2 additions & 2 deletions packages/astro/e2e/cloudflare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ test.describe('Cloudflare', () => {

test.beforeAll(async ({ astro, browser }) => {
const logger = createLoggerSpy({ info: infoLogs });
// @ts-expect-error `logger` is an @internal option stripped from the public type
devServer = await astro.startDevServer({ logger });
// @ts-expect-error: `_logger` is an internal API
devServer = await astro.startDevServer({ _logger: logger });
await warmupDevServer(browser, astro.resolveUrl('/'));
});

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/cli/dev/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function background({
if (flags.config) args.push('--config', String(flags.config));
if (flags.root) args.push('--root', String(flags.root));
if (flags.allowedHosts) args.push('--allowed-hosts', String(flags.allowedHosts));
if (flags.experimentalJson) args.push('--experimental-json');
if (flags.json) args.push('--json');

// Open the log file for writing, ensuring the .astro directory exists
const logFileURL = getLogFileURL(root);
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/cli/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function dev({ flags }: DevOptions) {
// When an AI coding agent is detected, enable background mode and JSON logging automatically.
const agentDetected = !process.env.ASTRO_DEV_BACKGROUND && isRunByAgent();
if (agentDetected) {
flags.experimentalJson = true;
flags.json = true;
}

const logger = createLoggerFromFlags(flags);
Expand Down
10 changes: 4 additions & 6 deletions packages/astro/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,9 @@ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
},
};

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

Expand All @@ -59,7 +57,7 @@ export function createLoggerFromFlags(flags: Flags): AstroLogger {
} else if (flags.silent) {
logLevel = 'silent';
}
if (flags.experimentalJson) {
if (flags.json) {
return createJsonLoggerFromFlags({ logLevel });
} else {
return createNodeLoggerFromFlags({ logLevel });
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/cli/help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +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.'],
['--json', 'Enables JSON logging.'],
],
},
};
4 changes: 2 additions & 2 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function createManifest(
placement: undefined,
},
logLevel: 'silent',
experimentalLogger: manifest?.experimentalLogger ?? undefined,
loggerConfig: manifest?.loggerConfig ?? undefined,
};
}

Expand Down Expand Up @@ -272,7 +272,7 @@ type AstroContainerManifest = Pick<
| 'middlewareMode'
| 'assetsDir'
| 'image'
| 'experimentalLogger'
| 'loggerConfig'
>;

type AstroContainerConstructor = {
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/app/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,8 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
for (const feature of missing) {
this.logger.warn(
'router',
`Your project uses ${feature}, but your custom src/fetch.ts does not call the ${feature}() handler. ` +
`This feature will not work unless you add it to your fetch.ts pipeline.`,
`Your project uses ${feature}, but your custom src/fetch.ts does not call the ${feature}() handler. ` +
`This feature will not work unless you add it to your fetch.ts pipeline.`,
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ export type SSRManifest = {
};
internalFetchHeaders?: Record<string, string>;
logLevel: AstroLoggerLevel;
// Configure that tells us how to load the logger
experimentalLogger: LoggerHandlerConfig | undefined;
// Configuration that tells us how to load the logger
loggerConfig: LoggerHandlerConfig | undefined;
};

export type SSRActions = {
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,16 @@ export abstract class Pipeline {

/**
* Resolves the logger destination from the manifest and updates the pipeline logger.
* If the user configured `experimental.logger`, the bundled logger factory is loaded
* If the user configured `logger`, the bundled logger factory is loaded
* and replaces the default console destination. This is lazy and only resolves once.
*/
async getLogger(): Promise<AstroLogger> {
if (this.resolvedLogger) {
return this.logger;
}
this.resolvedLogger = true;
if (this.manifest.experimentalLogger) {
this.logger = await loadLogger(this.manifest.experimentalLogger);
if (this.manifest.loggerConfig) {
this.logger = await loadLogger(this.manifest.loggerConfig);
}
return this.logger;
}
Expand Down
8 changes: 4 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ async function buildManifest(

const middlewareMode = resolveMiddlewareMode(opts.settings.adapter?.adapterFeatures);

let experimentalLogger = undefined;
if (settings.config.experimental.logger) {
experimentalLogger = settings.config.experimental.logger;
let loggerConfig = undefined;
if (settings.config.logger) {
loggerConfig = settings.config.logger;
}

return {
Expand Down Expand Up @@ -409,6 +409,6 @@ async function buildManifest(
internalFetchHeaders,
logLevel: settings.logLevel,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config.security.csp),
experimentalLogger,
loggerConfig,
};
}
3 changes: 2 additions & 1 deletion packages/astro/src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,14 @@ function splitInlineConfig(inlineConfig: AstroInlineConfig): {
inlineUserConfig: AstroUserConfig;
inlineOnlyConfig: AstroInlineOnlyConfig;
} {
const { configFile, mode, logLevel, ...inlineUserConfig } = inlineConfig;
const { configFile, mode, logLevel, _logger, ...inlineUserConfig } = inlineConfig;
return {
inlineUserConfig,
inlineOnlyConfig: {
configFile,
mode,
logLevel,
_logger,
},
};
}
Expand Down
15 changes: 6 additions & 9 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
clientPrerender: false,
contentIntellisense: false,
chromeDevtoolsWorkspace: false,
logger: {
entrypoint: 'astro/logger/node',
},
},
} satisfies AstroUserConfig & {
server: { open: boolean };
Expand Down Expand Up @@ -556,6 +553,12 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.prerenderConflictBehavior),
fetchFile: z.string().nullable().optional().default(ASTRO_CONFIG_DEFAULTS.fetchFile),
logger: z
.object({
entrypoint: z.string(),
config: z.record(z.string(), z.any()).optional(),
})
.optional(),
fonts: z.array(FontFamilySchema).optional(),
experimental: z
.strictObject({
Expand All @@ -574,12 +577,6 @@ export const AstroConfigSchema = z.object({
svgOptimizer: SvgOptimizerSchema.optional(),
cache: CacheSchema.optional(),
routeRules: RouteRulesSchema.optional(),
logger: z
.object({
entrypoint: z.string(),
config: z.record(z.string(), z.any()).optional(),
})
.optional(),
})
.prefault({}),
legacy: z
Expand Down
7 changes: 0 additions & 7 deletions packages/astro/src/core/fetch/fetch-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,13 +1057,6 @@ export class FetchState implements AstroFetchState {
return state.getCsp();
},
get logger(): APIContext['logger'] {
if (!state.pipeline.manifest.experimentalLogger) {
state.pipeline.logger.warn(
null,
'The Astro.logger is available only when experimental.logger is defined.',
);
return undefined;
}
return {
info(msg: string) {
state.pipeline.logger.info(null, msg);
Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/logger/impls/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ export default function (options?: NodeHandlerConfig): AstroLoggerDestination<As
}

export function createNodeLoggerFromFlags(inlineConfig: AstroInlineConfig): AstroLogger {
if (inlineConfig.logger) return inlineConfig.logger;

return new AstroLogger({
destination: nodeLogDestination(),
level: inlineConfig.logLevel ?? 'info',
Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/core/logger/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,13 @@ export async function loadOrCreateNodeLogger(
astroConfig: AstroConfig,
inlineAstroConfig: AstroInlineConfig,
) {
// Internal testing shortcut: if a pre-built AstroLogger instance was
// passed via the internal `_logger` property, use it directly.
if (inlineAstroConfig._logger) return inlineAstroConfig._logger;

try {
if (astroConfig.experimental.logger) {
return await loadLogger(astroConfig.experimental.logger, inlineAstroConfig.logLevel);
if (astroConfig.logger) {
return await loadLogger(astroConfig.logger, inlineAstroConfig.logLevel);
} else {
return createNodeLoggerFromFlags(inlineAstroConfig);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/core/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ function createContext({
session: undefined,
cache: new DisabledAstroCache(),
csp: undefined,
logger: undefined,
logger: {
info() {},
warn() {},
error() {},
},
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
Expand Down
8 changes: 4 additions & 4 deletions packages/astro/src/manifest/serialized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ async function createSerializedManifest(
};
}

let experimentalLogger = undefined;
if (settings.config.experimental.logger) {
experimentalLogger = settings.config.experimental.logger;
let loggerConfig = undefined;
if (settings.config.logger) {
loggerConfig = settings.config.logger;
}

return {
Expand Down Expand Up @@ -237,6 +237,6 @@ async function createSerializedManifest(
},
logLevel: settings.logLevel,
shouldInjectCspMetaTags: false,
experimentalLogger,
loggerConfig,
};
}
Loading
Loading