Journalisation structurée côté serveur et côté Edge
Émettez des journaux structurés corrélés qui résistent aux démarrages à froid sans serveur et aux contraintes d’Edge.
Journalisation structurée côté serveur et côté Edge est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
Why Structured Logging Matters in Next.js 15
Traditional console.log outputs unstructured strings. In production Next.js 15 apps running across serverless functions and Edge runtimes, these strings are nearly useless: no correlation between requests, no machine-parseable fields, and cold starts discard buffered output before it flushes.
Structured logging solves this by emitting JSON objects with consistent fields on every log line:
- requestId — ties every log to one HTTP request
- timestamp — ISO-8601, always UTC
- level —
info/warn/error - service — which route or function emitted the log
- message — human-readable summary
- context — arbitrary structured payload
Observability platforms (Datadog, Axiom, Grafana Loki) ingest these fields automatically, letting you filter and correlate across thousands of concurrent requests in seconds.
The Core Logger Interface
Before integrating with any runtime, define a minimal, portable logger interface. This keeps your business logic decoupled from the concrete logging implementation and makes testing trivial.
Create lib/logger/types.ts with the shape every logger must satisfy:
// lib/logger/types.ts
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface LogContext {
requestId?: string;
userId?: string;
route?: string;
durationMs?: number;
[key: string]: unknown;
}
export interface Logger {
debug(message: string, context?: LogContext): void;
info(message: string, context?: LogContext): void;
warn(message: string, context?: LogContext): void;
error(message: string, context?: LogContext & { err?: unknown }): void;
}
export interface LogEntry {
timestamp: string;
level: LogLevel;
service: string;
message: string;
context: LogContext;
}Building a JSON Logger for the Node.js Runtime
Next.js 15 Server Components, Route Handlers, and Server Actions run in the Node.js runtime by default. Here you have access to process.stdout and can safely emit multi-line JSON.
The key discipline: write one JSON object per line (NDJSON / JSON Lines format). Log aggregators split on newlines, so multi-line output corrupts ingestion.
Notice how err is serialised manually — JSON.stringify silently drops Error properties like stack and message.
// lib/logger/node-logger.ts
import type { Logger, LogContext, LogEntry, LogLevel } from './types';
function serializeError(err: unknown): Record<string, unknown> {
if (err instanceof Error) {
return { name: err.name, message: err.message, stack: err.stack };
}
return { raw: String(err) };
}
function createEntry(
level: LogLevel,
service: string,
message: string,
context: LogContext = {}
): LogEntry {
return {
timestamp: new Date().toISOString(),
level,
service,
message,
context,
};
}
export function createNodeLogger(service: string): Logger {
const write = (entry: LogEntry) =>
process.stdout.write(JSON.stringify(entry) + '\n');
return {
debug: (msg, ctx) => write(createEntry('debug', service, msg, ctx)),
info: (msg, ctx) => write(createEntry('info', service, msg, ctx)),
warn: (msg, ctx) => write(createEntry('warn', service, msg, ctx)),
error: (msg, ctx) => {
const { err, ...rest } = ctx ?? {};
write(createEntry('error', service, msg, {
...rest,
...(err !== undefined ? { error: serializeError(err) } : {}),
}));
},
};
}Edge-Compatible Logger: Surviving the Constraint
The Edge Runtime (used by Middleware and Route Handlers with export const runtime = 'edge') strips out most Node.js APIs. You cannot use process.stdout.write or fs.
What you can use:
console.log/console.error— always available- The
Fetch API— to forward logs to an external endpoint crypto.randomUUID()— for request IDs
The trick: still emit JSON strings via console.log. Vercel Edge logs are line-buffered and forward to your log drain as plain text, so one JSON per line is safe.
// lib/logger/edge-logger.ts
import type { Logger, LogContext, LogEntry, LogLevel } from './types';
function createEntry(
level: LogLevel,
service: string,
message: string,
context: LogContext = {}
): LogEntry {
return {
timestamp: new Date().toISOString(),
level,
service,
message,
context,
};
}
export function createEdgeLogger(service: string): Logger {
const emit = (entry: LogEntry) => {
// console.log is the only safe output channel in Edge Runtime
const line = JSON.stringify(entry);
if (entry.level === 'error') {
console.error(line);
} else {
console.log(line);
}
};
return {
debug: (msg, ctx) => emit(createEntry('debug', service, msg, ctx)),
info: (msg, ctx) => emit(createEntry('info', service, msg, ctx)),
warn: (msg, ctx) => emit(createEntry('warn', service, msg, ctx)),
error: (msg, ctx) => emit(createEntry('error', service, msg, ctx)),
};
}Request Correlation with AsyncLocalStorage
The hardest problem in serverless logging is correlation: attaching the same requestId to every log emitted during one request, even deep inside utility functions, without passing the ID as a parameter everywhere.
Node.js AsyncLocalStorage solves this. It creates a per-request context store that propagates automatically through async chains — await, Promise.then, timers — without any explicit threading.
Next.js 15 supports this natively in the Node.js runtime. Create a store module:
// lib/logger/request-store.ts
import { AsyncLocalStorage } from 'async_hooks';
export interface RequestStore {
requestId: string;
userId?: string;
startTime: number;
}
// One singleton for the process lifetime
export const requestStore = new AsyncLocalStorage<RequestStore>();
export function getRequestContext(): Partial<RequestStore> {
return requestStore.getStore() ?? {};
}
// Wrap any async work in this to bind a store
export function runWithRequestContext<T>(
store: RequestStore,
fn: () => Promise<T>
): Promise<T> {
return requestStore.run(store, fn);
}Context-Aware Logger Using the Store
Now wire the AsyncLocalStorage store into the logger. Every log call automatically reads requestId and userId from the running context — no prop-drilling needed.
Update lib/logger/node-logger.ts to merge the ambient store before writing:
// lib/logger/context-logger.ts
import { createNodeLogger } from './node-logger';
import { getRequestContext } from './request-store';
import type { Logger, LogContext } from './types';
export function createContextLogger(service: string): Logger {
const base = createNodeLogger(service);
function enrich(ctx: LogContext = {}): LogContext {
const { requestId, userId, startTime } = getRequestContext();
return {
...(requestId ? { requestId } : {}),
...(userId ? { userId } : {}),
...(startTime
? { elapsedMs: Date.now() - startTime }
: {}),
...ctx, // caller can override ambient values if needed
};
}
return {
debug: (msg, ctx) => base.debug(msg, enrich(ctx)),
info: (msg, ctx) => base.info(msg, enrich(ctx)),
warn: (msg, ctx) => base.warn(msg, enrich(ctx)),
error: (msg, ctx) => base.error(msg, enrich(ctx)),
};
}
// Singleton used everywhere in Node.js routes
export const logger = createContextLogger('nextjs-app');Injecting Context in Middleware
The ideal place to assign a requestId is Next.js Middleware — it intercepts every request before routing. You can generate an ID, attach it to a request header (x-request-id), and pass it downstream to both Server Components and API routes.
Note: Middleware runs in the Edge Runtime. Use crypto.randomUUID() (not uuid package) and createEdgeLogger.
// middleware.ts (project root)
import { NextRequest, NextResponse } from 'next/server';
import { createEdgeLogger } from '@/lib/logger/edge-logger';
const log = createEdgeLogger('middleware');
export function middleware(req: NextRequest) {
// Honour an upstream gateway's ID if present
const requestId =
req.headers.get('x-request-id') ?? crypto.randomUUID();
const start = Date.now();
log.info('request started', {
requestId,
method: req.method,
path: req.nextUrl.pathname,
});
const response = NextResponse.next();
// Forward the ID so Route Handlers and Server Actions can read it
response.headers.set('x-request-id', requestId);
log.info('request forwarded', {
requestId,
durationMs: Date.now() - start,
});
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Seeding AsyncLocalStorage in a Route Handler
In Node.js Route Handlers, read the x-request-id header (set by Middleware) and seed the AsyncLocalStorage store. Everything called within runWithRequestContext — including nested awaits and utility functions — inherits this context automatically.
// app/api/orders/route.ts
import { type NextRequest, NextResponse } from 'next/server';
import { runWithRequestContext } from '@/lib/logger/request-store';
import { logger } from '@/lib/logger/context-logger';
import { fetchOrders } from '@/lib/orders';
export async function GET(req: NextRequest) {
const requestId =
req.headers.get('x-request-id') ?? crypto.randomUUID();
return runWithRequestContext(
{ requestId, startTime: Date.now() },
async () => {
logger.info('fetching orders');
// up arrow automatically includes requestId + elapsedMs
try {
const orders = await fetchOrders();
logger.info('orders fetched', { count: orders.length });
return NextResponse.json(orders);
} catch (err) {
logger.error('failed to fetch orders', { err });
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
);
}Logging Inside Server Actions
Server Actions in Next.js 15 run in the Node.js runtime on the server, so AsyncLocalStorage works here too. However, there is a subtle constraint: the action is invoked by the React renderer, not a raw HTTP handler, so you must seed the store yourself at the top of the action.
A common pattern is a wrapper utility that seeds the store and provides the logger, keeping each action's body clean:
// lib/actions/with-logging.ts
import { headers } from 'next/headers';
import { runWithRequestContext } from '@/lib/logger/request-store';
import { logger } from '@/lib/logger/context-logger';
type ActionFn<TArgs extends unknown[], TResult> =
(...args: TArgs) => Promise<TResult>;
export function withLogging<TArgs extends unknown[], TResult>(
name: string,
fn: ActionFn<TArgs, TResult>
): ActionFn<TArgs, TResult> {
return async (...args) => {
const headersList = await headers();
const requestId =
headersList.get('x-request-id') ?? crypto.randomUUID();
return runWithRequestContext(
{ requestId, startTime: Date.now() },
async () => {
logger.info('action started: ' + name, { action: name });
try {
const result = await fn(...args);
logger.info('action completed: ' + name);
return result;
} catch (err) {
logger.error('action failed: ' + name, { err });
throw err;
}
}
);
};
}
// Usage in a Server Action:
// export const submitOrder = withLogging('submitOrder', async (data) => { ... });Sampling and Log Levels in Production
In a high-traffic Next.js app, logging every debug statement is expensive — both in compute time and in ingestion costs. Use two techniques together:
- Level gating: check
process.env.LOG_LEVELand skip levels below the threshold. Production typically runs atinfo; debug is enabled only during incidents. - Sampling: for noisy
infopaths (e.g., health checks), log only a percentage of requests to reduce volume without losing signal entirely.
A standalone demonstration of the core sampling + level-gating logic:
// Standalone demo — runs without any framework
const LEVELS = ['debug', 'info', 'warn', 'error'];
const MIN_LEVEL = process.env['LOG_LEVEL'] || 'info';
function shouldLog(level) {
return LEVELS.indexOf(level) >= LEVELS.indexOf(MIN_LEVEL);
}
function sample(rate) {
// rate = 0.1 means log 10% of the time
return Math.random() < rate;
}
function log(level, message, sampleRate = 1) {
if (!shouldLog(level)) return;
if (!sample(sampleRate)) return;
console.log(JSON.stringify({ level, message, ts: new Date().toISOString() }));
}
// Simulated high-frequency health-check path — logs only ~10%
for (let i = 0; i < 20; i++) {
log('info', '/api/health called', 0.1);
}
// Errors always log regardless of sample rate
log('error', 'Database connection failed');Forwarding Logs to an External Sink
Serverless functions are ephemeral — stdout is only reliable if your platform captures it (Vercel does; bare AWS Lambda does not by default). A log drain / remote sink (Axiom, Better Stack, Datadog) guarantees durability.
From the Edge Runtime you can forward logs using fetch with waitUntil so the HTTP response returns immediately while the log POST happens in the background. From Node.js Route Handlers, use after() (Next.js 15) for the same non-blocking guarantee.
// lib/logger/axiom-drain.ts
// Edge-compatible log drain using fetch
const AXIOM_DATASET = process.env['AXIOM_DATASET'] ?? '';
const AXIOM_TOKEN = process.env['AXIOM_API_TOKEN'] ?? '';
export async function sendToAxiom(
entries: object[]
): Promise<void> {
if (!AXIOM_DATASET || !AXIOM_TOKEN) return; // skip in dev
await fetch(
'https://api.axiom.co/v1/datasets/' + AXIOM_DATASET + '/ingest',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-ndjson',
Authorization: 'Bearer ' + AXIOM_TOKEN,
},
// NDJSON: one JSON object per line
body: entries.map((e) => JSON.stringify(e)).join('\n'),
}
);
}
// In a Middleware or Edge Route Handler:
// context.waitUntil(sendToAxiom([entry]));
//
// In a Node.js Route Handler (Next.js 15):
// import { after } from 'next/server';
// after(() => sendToAxiom([entry]));Quick Check: Correlation Across Runtimes
You need every log line from a single user request — spanning Middleware (Edge), a Route Handler (Node.js), and a nested Server Action — to share the same requestId. Which approach is correct?
Lesson Recap: Structured Logging Across Server and Edge
In this lesson you built a complete, production-grade structured logging pipeline for Next.js 15:
- Portable interface (
Logger,LogContext) keeps business logic decoupled from the logging backend. - Runtime-specific implementations:
createNodeLoggerwrites NDJSON toprocess.stdout;createEdgeLoggerusesconsole.log— the only safe output channel in the Edge Runtime. - Correlation via
AsyncLocalStorage: seed a store once (in a Route Handler or Server Action wrapper) and every nestedawaitinheritsrequestId,userId, and elapsed time automatically. - Middleware as the request-ID origin: generate or honour an upstream
x-request-idheader in Middleware, forward it downstream, and seed the Node.js store on arrival. - Production discipline: gate log levels with
LOG_LEVELenv var, sample noisy paths, serialiseErrorobjects explicitly, and forward logs to a durable external sink withwaitUntilorafter()to avoid blocking responses.
These patterns together give you correlated, machine-parseable observability that survives cold starts, concurrent requests, and the constraints of both the Node.js and Edge runtimes.
Apprends TypeScript avec un tuteur IA — gratuit
Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.
- Cours
- 22
- Leçons
- 88
Questions Fréquemment Posées
La leçon « Journalisation structurée côté serveur et côté Edge » est-elle gratuite ?
Oui — le texte complet de « Journalisation structurée côté serveur et côté Edge » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Journalisation structurée côté serveur et côté Edge » ?
Émettez des journaux structurés corrélés qui résistent aux démarrages à froid sans serveur et aux contraintes d’Edge. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?
Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.
Combien de temps prend la leçon « Journalisation structurée côté serveur et côté Edge » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?
Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Traçage OpenTelemetry avec instrumentation.ts
- Limites d’erreur granulaires avec error.tsx et global-error
- Journalisation structurée côté serveur et côté Edge
- Capturer les échecs des actions serveur et la télémétrie