Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
55 changes: 55 additions & 0 deletions .changeset/silver-berries-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
'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"

const customLogger: AstroLoggerDestination = {
write(message: AstroLoggerMessage) {
// write message somewhere
}
}
export default customLogger
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"./errors": "./dist/core/errors/userError.js",
"./middleware": "./dist/core/middleware/index.js",
"./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",
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: 2 additions & 0 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ function createManifest(
experimentalQueuedRendering: manifest?.experimentalQueuedRendering ?? {
enabled: false,
},
experimentalLogger: manifest?.experimentalLogger ?? false,
};
}

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

type AstroContainerConstructor = {
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export type SSRManifest = {
};
internalFetchHeaders?: Record<string, string>;
logLevel: AstroLoggerLevel;
// This is temporary, and only used while the logger is experimental
experimentalLogger: boolean;
};

export type SSRActions = {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,5 +388,6 @@ async function buildManifest(
internalFetchHeaders,
logLevel: settings.logLevel,
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config.security.csp),
experimentalLogger: !!settings.config.experimental?.logger,
};
}
14 changes: 11 additions & 3 deletions packages/astro/src/core/logger/core.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import colors from 'piccolore';

/**
*
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is this empty comment expected or you forgot to put something here?

export interface AstroLoggerDestination<T = unknown> {
/**
* It receives a message and writes it into a destination
*/
write: (chunk: T) => void;
// NOTE: will document once we actually use it
/**
* It dumps logs without closing the connection to the destination.
* Method that can be used by specialized loggers.
*/
flush?: () => Promise<void> | void;
// NOTE: will document once we actually use it
/**
* It dumps logs and closes the connection to the destination.
* Method that can be used by specialized loggers.
*/
close?: () => Promise<void> | void;
}

// NOTE: this is a public type

/**
* The level of logging. Priority is the following:
* 1. debug
Expand Down
58 changes: 56 additions & 2 deletions packages/astro/src/core/logger/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,78 @@
import type { LoggerHandlerConfig } from './config.js';
import type { JonsHandlerConfig } from './impls/json.js';
import type { NodeHandlerConfig } from './impls/node.js';
import type { ConsoleHandlerConfig } from './impls/console.js';

export const logHandlers = {
/**
* It uses the built-in Astro JSON logger.
* @example
* ```js
* export default defineConfig({
* experimental: {
* logger: logHandlers.json({ pretty: true })
* }
* })
* ```
*/
json(config?: JonsHandlerConfig): LoggerHandlerConfig {
return {
entrypoint: 'astro/logger/json',
config,
};
},
node(): LoggerHandlerConfig {
/**
* It uses the built-in Astro Node.js logger.
*
* @example
* ```js
* export default defineConfig({
* experimental: {
* logger: logHandlers.node({ pretty: true })
* }
* })
* ```
*/
node(config?: NodeHandlerConfig): LoggerHandlerConfig {
return {
entrypoint: 'astro/logger/node',
config,
};
},
console(): LoggerHandlerConfig {
/**
* It uses the built-in Astro console logger.
*
* @example
* ```js
* export default defineConfig({
* experimental: {
* logger: logHandlers.console({ pretty: true })
* }
* })
* ```
*/
console(config?: ConsoleHandlerConfig): LoggerHandlerConfig {
return {
entrypoint: 'astro/logger/console',
config,
};
},

/**
* It allows composing different loggers
*
* @example
* ```js
* export default defineConfig({
* experimental: {
* logger: logHandlers.compose([
* logHandlers.console(),
* logHandlers.json(),
* ])
* }
* })
* ```
*/
compose(...loggers: LoggerHandlerConfig[]): LoggerHandlerConfig {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe neither the documentation nor the changeset mentions this feature. Maybe for the changeset it's not important (ie. we can focus on the most important parts) but we probably want to mention it in the docs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it out in purpose here, I forgot to add to the docs, I will address this shortly

return {
entrypoint: 'astro/logger/compose',
Expand Down
18 changes: 11 additions & 7 deletions packages/astro/src/core/logger/impls/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ import {
type AstroLoggerLevel,
AstroLogger,
} from '../core.js';
import type { NodeHandlerConfig } from './node.js';
import { matchesLevel } from '../public.js';

export type ConsoleHandlerConfig = {
level?: AstroLoggerLevel;
};

function consoleLogDestination(
level: AstroLoggerLevel = 'info',
config: ConsoleHandlerConfig = {},
): AstroLoggerDestination<AstroLoggerMessage> {
const { level = 'info' } = config;
return {
write(event: AstroLoggerMessage) {
let dest = console.error;
if (levels[event.level] < levels['error']) {
dest = console.info;
}

if (levels[event.level] < levels[level]) {
if (!matchesLevel(event.level, level)) {
return;
}

Expand All @@ -37,9 +44,6 @@ export function createConsoleLogger({ level }: { level: AstroLoggerLevel }): Ast
});
}

type Options = {
level?: AstroLoggerLevel;
};
export default function (options?: Options): AstroLoggerDestination<AstroLoggerMessage> {
return consoleLogDestination(options?.level ?? 'info');
export default function (options?: NodeHandlerConfig): AstroLoggerDestination<AstroLoggerMessage> {
return consoleLogDestination(options);
}
9 changes: 8 additions & 1 deletion packages/astro/src/core/logger/impls/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import {
} from '../core.js';
import type { Writable } from 'node:stream';
import type { AstroInlineConfig } from '../../../types/public/index.js';
import { matchesLevel } from '../public.js';

export type JonsHandlerConfig = {
Comment thread
ematipico marked this conversation as resolved.
Outdated
/**
* Whether the JSON line should format on multiple lines
*/
pretty?: boolean;
/**
* The level of logs that should be printed by the logger.
*/
level?: AstroLoggerLevel;
};

Expand All @@ -30,7 +37,7 @@ export default function jsonLoggerDestination(
dest = process.stdout;
}

if (levels[event.level] < levels[level]) {
if (!matchesLevel(event.level, level)) {
return;
}

Expand Down
17 changes: 10 additions & 7 deletions packages/astro/src/core/logger/impls/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@ import {
} from '../core.js';
import type { Writable } from 'node:stream';
import type { AstroInlineConfig } from '../../../types/public/index.js';
import { matchesLevel } from '../public.js';

type ConsoleStream = Writable & {
fd: 1 | 2;
};

export type NodeHandlerConfig = {
level?: AstroLoggerLevel;
};

function nodeLogDestination(
level: AstroLoggerLevel = 'info',
config: NodeHandlerConfig = {},
): AstroLoggerDestination<AstroLoggerMessage> {
const { level = 'info' } = config;
return {
write(event: AstroLoggerMessage) {
let dest: ConsoleStream = process.stderr;
if (levels[event.level] < levels['error']) {
dest = process.stdout;
}

if (levels[event.level] < levels[level]) {
if (!matchesLevel(event.level, level)) {
return;
}

Expand All @@ -37,11 +43,8 @@ function nodeLogDestination(
};
}

type Options = {
level?: AstroLoggerLevel;
};
export default function (options?: Options): AstroLoggerDestination<AstroLoggerMessage> {
return nodeLogDestination(options?.level ?? 'info');
export default function (options?: NodeHandlerConfig): AstroLoggerDestination<AstroLoggerMessage> {
return nodeLogDestination(options);
}

export function createNodeLoggerFromFlags(inlineConfig: AstroInlineConfig): AstroLogger {
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/core/logger/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This is public module with functions exported to the user

import { type AstroLoggerLevel, levels } from './core.js';

/**
* Returns `true` if `messageLevel` has a level equals or higher than `configuredLevel`
*
* @param messageLevel The level of the incoming message
* @param configuredLevel The level the logger is configured with
*/
export function matchesLevel(
messageLevel: AstroLoggerLevel,
configuredLevel: AstroLoggerLevel,
): boolean {
return levels[messageLevel] >= levels[configuredLevel];
}
2 changes: 1 addition & 1 deletion packages/astro/src/core/logger/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const RESOLVED_LOGGER_MODULE_ID = '\0' + LOGGER_MODULE_ID;
/**
* Generates virtual module code for a logger factory given a `LoggerHandlerConfig`.
* Handles built-in loggers (node, console, json), the compose meta-logger,
* and arbitrary user-provided entrypoints.
* and arbitrary user-provided entrypoint.
*/
export function generateLoggerCode(config: LoggerHandlerConfig): string {
switch (config.entrypoint) {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function createContext({
session: undefined,
cache: new DisabledAstroCache(),
csp: undefined,
logger: undefined,
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
Expand Down
Loading
Loading