From 930931483860b62051ecf6144ae132221d6d0c1b Mon Sep 17 00:00:00 2001 From: ematipico Date: Wed, 8 Apr 2026 14:44:20 +0100 Subject: [PATCH 1/8] feat: astro logger --- proposals/0059-custom-logger.md | 324 ++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 proposals/0059-custom-logger.md diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md new file mode 100644 index 00000000..4e592954 --- /dev/null +++ b/proposals/0059-custom-logger.md @@ -0,0 +1,324 @@ + + +**If you have feedback and the feature is released as experimental, please leave it on the Stage 3 PR. Otherwise, +comment on the Stage 2 issue (links below).** + +- Start Date: 2026-04-07 +- Reference Issues: +- Implementation PR: +- Stage 2 Issue: https://github.com/withastro/roadmap/issues/1335 +- Stage 3 PR: + +# Summary + +Add support for configurable log handlers in Astro, allowing users to replace the default console output with custom +logging implementations (e.g. structured JSON). + +# Example + +Using a built-in handler to get JSON logs: + +```js +// astro.config.mjs +import { defineConfig, logHandlers } from "astro/config"; + +export default defineConfig({ + experimental: { + logger: logHandlers.json({ pretty: false }), + }, +}); +``` + +Using a third-party handler: + +```js +// astro.config.mjs +import { defineConfig } from "astro/config"; +import { pino } from "astro-pino-logger"; + +export default defineConfig({ + experimental: { + logger: pino({ destination: "/var/log/app.log" }), + }, +}); +``` + +# Background & Motivation + +Users deploying Astro SSR to production need structured logging for integration with log aggregation services like +Kibana, Logstash, CloudWatch, and Grafana/Loki. Agents running builds and dev locally work better with JSON. Today, +Astro's logger is hardcoded – `BaseApp` uses `consoleLogDestination` which cannot be replaced, even by adapter authors. +Error logs in rendering are split across multiple lines and formatted for human reading, making them unparseable by log +aggregation tools. + +This is a common requirement for any production SSR deployment and has been widely requested for a long time (this is +currently the most upvoted feature request). + +# Goals + +- Allow users to configure a custom log handler that receives all Astro log output +- Support structured logging (JSON) for production deployments out of the box +- Provide a `--json` CLI flag for zero-config JSON output (e.g. `astro build --json`, `astro dev --json`, `astro preview --json`) +- Follow the established `{ entrypoint, config }` factory pattern used by session drivers, font providers etc +- Work across all scopes: build, dev, and SSR runtime +- Ensure Vite logs flow through the custom handler (they already route through Astro's logger) + +# Non-Goals + +- Request-scoped child loggers with correlation IDs +- Replacing the `debug` package integration – separate concern +- Separate handlers per scope (build vs server) – single handler with platform detection if needed +- Redaction capabilities, library authors are responsible for that + +# Detailed Design + +The proposal is split into three main sub-features, somewhat interconnected internally, but different from the user's point +of view. + +- Emit Astro logs using the JSON format. +- Provide users control over the destination of where logs are written, and how. +- A runtime API for custom logs. + +The objective is to provide users with powers over the destination of the log, and structured logs. Users will receive +the log message, and they decide where the log is written. + +This is also true for Astro's built-in logs. + +## JSON logger + +Users will be able to change the look and feel of the logs in JSON format in two ways: + +CLI via `--json` + +```shell +astro dev --json +``` + +Configuration via the proper handler + +```js +import { logHandler } from "astro/config"; + +export default defineConfig({ + logger: logHandler.json({ pretty: true }), +}); +``` + +Eventually, a log like the following will be turned into JSON: + +```shell +# now +11:13:51 [content] Syncing content +``` + +```shell +# json +{ "time": "11:13:51", "label": "content", "message": "Syncing content" } +``` + +## Customize logger + +Users can swap Astro's default logger with their own, however they also become responsible for the printing of Astro's +logs. All of Astro's logs will be redirected +to the logger provided by the user. + +```js +import pino from "astro-pino-logger"; + +export default defineConfig({ + logger: pino({ destination: "/var/log/app.log" }), +}); +``` + +Astro will export a `defineDestination` type-safe function where users can create their custom destination: + +```ts +// astro-pino-logger/src/handler.ts +import { defineDestination } from "astro/logger"; +import type { AstroLogMessage } from "astro"; + +interface PinoConfig { + destination?: string; + level?: string; +} + +export default defineDestination((config) => { + const pino = require("pino")(config); + return { + write(event: AstroLogMessage) { + pino[event.level]({ label: event.label }, event.message); + }, + flush() { + pino.flush(); + }, + close() { + pino.flush(); + pino.destination?.end(); + }, + }; +}); +``` + +The `LoggerDestination` interface will be defined as follows: + +```ts +export interface LoggerDestination { + /** Write a log event. Called synchronously on every log call. */ + write: (chunk: T) => void; + /** Flush buffered writes without releasing resources. Optional. */ + flush?: () => void | Promise; + /** Flush and release resources (file descriptors, connections). Optional. Implies flush. */ + close?: () => void | Promise; +} +``` + +`flush` and `close` are optional, but needed for two particular cases: + +- Allow third-party logging systems to use their `flush` and `close` APIs. +- SSR, where the file descriptors of the destinations must be closed. Particularly: + - `flush` is needed in serverless environments (Vercel, Netlify, Cloudflare, etc.) so that logs are written to the + destination at the end of a rendering request, without closing the file descriptor. + - `close` is needed in server environments (Node.js, Bun, Deno, etc.) so that logs are written to the destination + when a `SIGTERM`/`SIGINT` signal is sent by the server, and the file descriptor can be closed safely. + +## Runtime + +The `Astro` global and `APIContext` type will expose a new `log` field that can be used as follows: + +```ts +// endpoint.ts +export const GET = (context: APIContext) => { + context.log.info("Hello!"); + return Response.json({ greeting: "Hello world" }); +}; +``` + +Methods exposed: + +- `Astro.log.info` +- `Astro.log.warn` +- `Astro.log.error` + +## Configuration + +The configuration will accept a `logger` object: + +```ts +export default defineConfig({ + logger: { + entrypoint: "astro/logger/default", + config: {}, + }, +}); +``` + +Where the configuration has the following shape: + +```ts +export interface LogHandlerConfig { + entrypoint: string | URL; + config?: Record; +} +``` + +Internally, Astro will use Vite to resolve the entrypoint. We will probably use the same instance of the configuration ( +or a different one), because, internally, the logger is instantiated before the +dev server starts. + +The type `config` is optional, and when provided, is passed as parameter to the function exported by the entrypoint. + +## Built-in handlers + +Astro will expose some default handlers, as well as some utilities. All exported from the `astro/config` entrypoint. + +### `logHandlers.json(config?)` + +A logger that writes to `stdout` and `stderr`, but the information is printed in JSON format. When `pretty` is provided, +fields are broken down on multiple lines. + +```ts +import { defineConfig, logHandlers } from "astro/config"; + +export default defineConfig({ + logger: logHandlers.json({ pretty: true }), +}); +``` + +### `logHandlers.compose(...LogHandlerConfig[])` + +With this API, users will be able to configure multiple logger destinations. Under the hood, `compose` will create a +destination where `write`, `flush` and `close` are called for all handlers. + +```ts +import { defineConfig, logHandlers } from "astro/config"; +import pino from "astro-pino-logger"; + +export default defineConfig({ + logger: logHandlers.compose( + logHandlers.json(), + pino({ destination: "/var/log/app.log" }), + ), +}); +``` + +## Additional APIs + +We will provide some utilities so that users can create better logging. As designed, if users create their own logger, Astro's logs will flow into their destination. + +### `isAstroMessage` + +```ts +import { isAstroMessage } from "astro/logger"; + +const dest = { + write(chunk) { + if (!isAstroMessage(chunk)) { + // write something in the custom destination + } + }, +}; +``` + +## Internals + +Internally, Astro already has a logger, however types and implementation might not fit the new APIs. We will expose the internals to users, which means options and such will need to be documented. + +Internal refactors are expected (renaming, change of APIs, etc.). + +# Testing Strategy + +- The objective is to unit test the majority of the use cases, so the architecture of the feature must account for that. +- Few integration tests to verify `write`, `flush` and `close` are called in the correct focal points. + +# Drawbacks + +- This design also redirects our internal logs to the library authors, so users will need to filter them if they don't want them. +- The initialization of the logger is now asynchronous due to `entrypoint` +- The logger configuration must be serializable, no runtime functions. +- `flush` and `close` are `async` and could cause a failure point in the runtime if these functions fail, and the more handlers there are, the more awaited functions we have. + +# Alternatives + +- I did consider `astro:logger`, but internally our logger is initialized before the dev server, plus the logger is mostly runtime, so preferred avoiding Vite as much as possible. +- I considered an API where custom loggers will receive only user-defined logs, however this would have made the internal implementation more complex, probably caused mismatches of what's printed (look and feel). + +# Adoption strategy + +- The feature ships behind `experimental.logger`. +- Alternatively, users can use JSON logging via `--experimental-json`. +- After some testing time, the experimental flag will be removed, and the new feature will be shipped in a minor + +# Unresolved Questions + +- `flush` and `close` are asynchronous functions, and in the presence of multiple handlers we might need to await them. Should we call them without await? I wouldn't want to delay rendering. How do we catch errors if they fail? +- `debug` info level has been kept out of the picture because we use [`obug`](https://npmx.dev/package/obug) for it, which is noisy and completely different from the rest of the logs. Should we keep it out? Docs should address the differences from normal logging. From d4297d67091d0856007f891bb78f91402898de6c Mon Sep 17 00:00:00 2001 From: ematipico Date: Mon, 20 Apr 2026 14:56:23 +0100 Subject: [PATCH 2/8] update logger APIs --- proposals/0059-custom-logger.md | 49 +++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index 4e592954..b7a99264 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -34,7 +34,10 @@ import { defineConfig, logHandlers } from "astro/config"; export default defineConfig({ experimental: { - logger: logHandlers.json({ pretty: false }), + logger: logHandlers.json({ + pretty: false, + level: "warn" + }), }, }); ``` @@ -110,7 +113,10 @@ Configuration via the proper handler import { logHandler } from "astro/config"; export default defineConfig({ - logger: logHandler.json({ pretty: true }), + logger: logHandler.json({ + pretty: true, + level: "warn" + }), }); ``` @@ -250,7 +256,44 @@ fields are broken down on multiple lines. import { defineConfig, logHandlers } from "astro/config"; export default defineConfig({ - logger: logHandlers.json({ pretty: true }), + logger: logHandlers.json({ + pretty: true, + level: "warn" + }), +}); +``` + + +### `logHandlers.console(config?)` + +A logger that writes to `console`. Each level is mapped to the respective console binding: +- "error" -> `console.error` +- "warn" -> `console.warn` +- "info" -> `console.info` + +```ts +import { defineConfig, logHandlers } from "astro/config"; + +export default defineConfig({ + logger: logHandlers.console({ + pretty: true, + level: "warn" + }), +}); +``` + +### `logHandlers.node(config?)` + +A logger that writes to `stdout` and `stderr`. + +```ts +import { defineConfig, logHandlers } from "astro/config"; + +export default defineConfig({ + logger: logHandlers.node({ + pretty: true, + level: "warn" + }), }); ``` From 4a44db05112114b2e414e98a6e254669a5c5eb9e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 22 Apr 2026 14:14:31 +0100 Subject: [PATCH 3/8] Update proposals/0059-custom-logger.md Co-authored-by: Florian Lefebvre --- proposals/0059-custom-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index b7a99264..9fca8708 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -17,7 +17,7 @@ comment on the Stage 2 issue (links below).** - Reference Issues: - Implementation PR: - Stage 2 Issue: https://github.com/withastro/roadmap/issues/1335 -- Stage 3 PR: +- Stage 3 PR: https://github.com/withastro/roadmap/pull/1339 # Summary From 8acc87debcf16ff5b487f67c50828a2712cfdc79 Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 28 Apr 2026 15:22:07 +0100 Subject: [PATCH 4/8] Use correct astro logger destination --- proposals/0059-custom-logger.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index 9fca8708..c0eb3e86 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -175,12 +175,12 @@ export default defineDestination((config) => { }); ``` -The `LoggerDestination` interface will be defined as follows: +The `AstroLoggerDestination` interface will be defined as follows: ```ts -export interface LoggerDestination { +export interface AstroLoggerDestination { /** Write a log event. Called synchronously on every log call. */ - write: (chunk: T) => void; + write: (chunk: AstroLoggerMessage) => void; /** Flush buffered writes without releasing resources. Optional. */ flush?: () => void | Promise; /** Flush and release resources (file descriptors, connections). Optional. Implies flush. */ From 4851769c252f7604a4af2610806410be2784bfbe Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 28 Apr 2026 15:24:59 +0100 Subject: [PATCH 5/8] remove defineDestination --- proposals/0059-custom-logger.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index c0eb3e86..e61aa666 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -146,22 +146,20 @@ export default defineConfig({ }); ``` -Astro will export a `defineDestination` type-safe function where users can create their custom destination: +Library authors can use the `AstroLoggerDestination` interface if they need to create custom loggers: ```ts // astro-pino-logger/src/handler.ts -import { defineDestination } from "astro/logger"; -import type { AstroLogMessage } from "astro"; +import type { AstroLoggerMessage, AstroLoggerDestination } from "astro"; interface PinoConfig { destination?: string; level?: string; } -export default defineDestination((config) => { - const pino = require("pino")(config); +const pinoLogger = (config: PinoConfig = {}): AstroLoggerDestination => { return { - write(event: AstroLogMessage) { + write(event: AstroLoggerMessage) { pino[event.level]({ label: event.label }, event.message); }, flush() { @@ -172,7 +170,9 @@ export default defineDestination((config) => { pino.destination?.end(); }, }; -}); +} + +export default pinoLogger; ``` The `AstroLoggerDestination` interface will be defined as follows: From d834e686db18e49a83e750ba489132358997a639 Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 28 Apr 2026 15:26:36 +0100 Subject: [PATCH 6/8] Document `matchesLevel` --- proposals/0059-custom-logger.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index e61aa666..c8a7265d 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -318,14 +318,14 @@ export default defineConfig({ We will provide some utilities so that users can create better logging. As designed, if users create their own logger, Astro's logs will flow into their destination. -### `isAstroMessage` +### `matchesLevel` ```ts -import { isAstroMessage } from "astro/logger"; +import { matchesLevel } from "astro/logger"; const dest = { - write(chunk) { - if (!isAstroMessage(chunk)) { + write(message) { + if (matchesLevel(message.level, 'info')) { // write something in the custom destination } }, From 9273d645ad82ea18a99c3430aa213cfa7d95341d Mon Sep 17 00:00:00 2001 From: ematipico Date: Tue, 28 Apr 2026 15:28:11 +0100 Subject: [PATCH 7/8] Don't use vite for loading the logger --- proposals/0059-custom-logger.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index c8a7265d..1e7b7d9d 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -237,9 +237,7 @@ export interface LogHandlerConfig { } ``` -Internally, Astro will use Vite to resolve the entrypoint. We will probably use the same instance of the configuration ( -or a different one), because, internally, the logger is instantiated before the -dev server starts. +Internally, Astro will load the specifier using a simple dynamic import. The type `config` is optional, and when provided, is passed as parameter to the function exported by the entrypoint. From fdd9e13623a8198a3211500f90e33ce305841e52 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 18 May 2026 13:28:50 +0100 Subject: [PATCH 8/8] Update proposals/0059-custom-logger.md Co-authored-by: Florian Lefebvre --- proposals/0059-custom-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0059-custom-logger.md b/proposals/0059-custom-logger.md index 1e7b7d9d..5ecd172e 100644 --- a/proposals/0059-custom-logger.md +++ b/proposals/0059-custom-logger.md @@ -15,7 +15,7 @@ comment on the Stage 2 issue (links below).** - Start Date: 2026-04-07 - Reference Issues: -- Implementation PR: +- Implementation PR: https://github.com/withastro/astro/pull/16477 - Stage 2 Issue: https://github.com/withastro/roadmap/issues/1335 - Stage 3 PR: https://github.com/withastro/roadmap/pull/1339