Gestructureerde logging voor Server en Edge
Produceer gecorreleerde, gestructureerde logs die serverless cold starts en Edge-beperkingen doorstaan.
Gestructureerde logging voor Server en Edge is een gratis Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)-les op CoddyKit. Dit is les 3 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Fullstackontwikkeling met Next.js 15 (App Router + Server Actions). Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.
Waarom gestructureerd loggen belangrijk is in Next.js 15
Traditionele uitvoer van console.log bestaat uit ongestructureerde tekenreeksen. In productie-apps met Next.js 15 die draaien op serverloze functies en Edge-runtimes, zijn deze tekenreeksen vrijwel nutteloos: er is geen verband tussen aanvragen, geen velden die machines kunnen parseren en bij koude starts gaat gebufferde uitvoer verloren voordat die wordt weggeschreven.
Gestructureerd loggen lost dit op door bij elke logregel JSON-objecten met consistente velden uit te schrijven:
- requestId — koppelt elke log aan één HTTP-aanvraag
- timestamp — ISO-8601, altijd UTC
- level —
info/warn/error - service — welke route of functie de log heeft geproduceerd
- message — een voor mensen leesbare samenvatting
- context — willekeurige gestructureerde gegevens
Platforms voor observeerbaarheid (Datadog, Axiom, Grafana Loki) nemen deze velden automatisch op, zodat je binnen enkele seconden kunt filteren en verbanden kunt leggen tussen duizenden gelijktijdige aanvragen.
De basisinterface voor loggers
Voordat je een logger met een runtime integreert, definieer je een minimale, draagbare loggerinterface. Hierdoor blijft je bedrijfslogica losgekoppeld van de concrete implementatie voor loggen en wordt testen eenvoudig.
Maak lib/logger/types.ts met de vorm waaraan elke logger moet voldoen:
// 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;
}Een JSON-logger bouwen voor de Node.js-runtime
Next.js 15 Server Components, Route Handlers en Server Actions draaien standaard in de Node.js-runtime. Hier heb je toegang tot process.stdout en kun je veilig JSON over meerdere regels uitschrijven.
De belangrijkste regel: schrijf één JSON-object per regel (NDJSON / JSON Lines-indeling). Logverzamelaars splitsen op nieuwe regels, waardoor uitvoer over meerdere regels de opname beschadigt.
Let erop hoe err handmatig wordt geserialiseerd — JSON.stringify laat eigenschappen van Error zoals stack en message stilletjes weg.
// 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) } : {}),
}));
},
};
}Een logger die met Edge werkt: omgaan met de beperking
De Edge-runtime (gebruikt door Middleware en Route Handlers met export const runtime = 'edge') verwijdert de meeste Node.js-API's. Je kunt process.stdout.write of fs niet gebruiken.
Wat je wel kunt gebruiken:
console.log/console.error— altijd beschikbaar- De
Fetch API— om logs door te sturen naar een extern eindpunt crypto.randomUUID()— voor aanvraag-ID's
De truc: schrijf nog steeds JSON-tekenreeksen uit via console.log. Edge-logs van Vercel worden per regel gebufferd en als platte tekst naar je loguitvoer gestuurd, dus één JSON-object per regel is veilig.
// 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)),
};
}Aanvragen aan elkaar koppelen met AsyncLocalStorage
Het lastigste probleem bij loggen in serverloze omgevingen is correlatie: dezelfde requestId koppelen aan elke log die tijdens één aanvraag wordt geproduceerd, zelfs diep binnen hulpfuncties, zonder de ID overal als parameter door te geven.
Node.js AsyncLocalStorage lost dit op. Het maakt een contextopslag per aanvraag die automatisch wordt doorgegeven via asynchrone ketens — await, Promise.then en timers — zonder dat je dit expliciet hoeft door te geven.
Next.js 15 ondersteunt dit standaard in de Node.js-runtime. Maak een opslagmodule:
// 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);
}Een contextbewuste logger met de opslag
Koppel nu de opslag van AsyncLocalStorage aan de logger. Elke logaanroep leest automatisch requestId en userId uit de actieve context — je hoeft niets via opeenvolgende props door te geven.
Werk lib/logger/node-logger.ts bij zodat de aanwezige opslag vóór het schrijven wordt samengevoegd:
// 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');Context injecteren in Middleware
De ideale plek om een requestId toe te wijzen is Next.js Middleware — dit onderschept elke aanvraag vóór de routering. Je kunt een ID genereren, die aan een aanvraagheader (x-request-id) koppelen en vervolgens doorgeven aan zowel Server Components als API-routes.
Let op: Middleware draait in de Edge-runtime. Gebruik crypto.randomUUID() (niet het pakket uuid) en 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).*)'],
};AsyncLocalStorage vullen in een Route Handler
Lees in Node.js Route Handlers de header x-request-id (ingesteld door Middleware) en vul de opslag van AsyncLocalStorage. Alles wat binnen runWithRequestContext wordt aangeroepen — waaronder geneste awaits en hulpfuncties — erft deze context automatisch.
// 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 }
);
}
}
);
}Loggen binnen Server Actions
Server Actions in Next.js 15 draaien op de server in de Node.js-runtime, dus ook hier werkt AsyncLocalStorage. Er is echter een subtiele beperking: de actie wordt aangeroepen door de React-renderer en niet door een gewone HTTP-handler. Daarom moet je de opslag zelf bovenaan de actie vullen.
Een veelgebruikt patroon is een wrapperhulpprogramma dat de opslag vult en de logger beschikbaar maakt, zodat de body van elke actie overzichtelijk blijft:
// 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 en logniveaus in productie
In een Next.js-app met veel verkeer is elke debug-instructie loggen duur — zowel qua rekentijd als qua kosten voor opname. Gebruik twee technieken samen:
- Niveaufiltering: controleer
process.env.LOG_LEVELen sla niveaus onder de drempel over. In productie wordt doorgaansinfogebruikt; debug wordt alleen tijdens incidenten ingeschakeld. - Sampling: log bij luidruchtige
info-paden (bijvoorbeeld gezondheidscontroles) slechts een percentage van de aanvragen om het volume te beperken zonder het signaal volledig te verliezen.
Een zelfstandige demonstratie van de basislogica voor sampling en niveaufiltering:
// 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');Logs doorsturen naar een externe bestemming
Serverloze functies zijn tijdelijk — stdout is alleen betrouwbaar als je platform het vastlegt (Vercel doet dit; een gewone AWS Lambda standaard niet). Een loguitvoer / externe bestemming (Axiom, Better Stack, Datadog) garandeert duurzame opslag.
Vanuit de Edge-runtime kun je logs doorsturen met fetch en waitUntil, zodat de HTTP-respons meteen wordt teruggestuurd terwijl de POST van de log op de achtergrond plaatsvindt. Gebruik vanuit Node.js Route Handlers after() (Next.js 15) voor dezelfde garantie zonder blokkering.
// 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]));Korte controle: correlatie tussen runtimes
Je hebt nodig dat elke logregel van één gebruikersaanvraag — via Middleware (Edge), een Route Handler (Node.js) en een geneste Server Action — dezelfde requestId deelt. Welke aanpak is correct?
Samenvatting van de les: gestructureerd loggen op server en Edge
In deze les heb je een complete, productiewaardige pipeline voor gestructureerd loggen in Next.js 15 gebouwd:
- Draagbare interface (
Logger,LogContext) houdt de bedrijfslogica losgekoppeld van de logbackend. - Runtime-specifieke implementaties:
createNodeLoggerschrijft NDJSON naarprocess.stdout;createEdgeLoggergebruiktconsole.log— het enige veilige uitvoerkanaal in de Edge-runtime. - Correlatie via
AsyncLocalStorage: vul een opslag één keer (in een Route Handler of wrapper voor een Server Action) en elke genesteawaiterft automatischrequestId,userIden de verstreken tijd. - Middleware als oorsprong van de aanvraag-ID: genereer in Middleware een upstream-header
x-request-idof neem die over, stuur hem downstream door en vul bij aankomst de Node.js-opslag. - Discipline voor productie: filter logniveaus met de omgevingsvariabele
LOG_LEVEL, gebruik sampling voor luidruchtige paden, serialiseerError-objecten expliciet en stuur logs door naar een duurzame externe bestemming metwaitUntilofafter()om te voorkomen dat reacties worden geblokkeerd.
Deze patronen geven je samen observeerbaarheid met correlatie en machinale verwerkbaarheid, die koude starts, gelijktijdige aanvragen en de beperkingen van zowel de Node.js- als de Edge-runtime doorstaat.
Leer TypeScript met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 22
- Lessen
- 88
Veelgestelde vragen
Is de les “Gestructureerde logging voor Server en Edge” gratis?
Ja — je kunt hier op het web alle 3 lessen van het leerpad Fullstackontwikkeling met Next.js 15 (App Router + Server Actions), waaronder “Gestructureerde logging voor Server en Edge”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.
Wat leer ik in “Gestructureerde logging voor Server en Edge”?
Produceer gecorreleerde, gestructureerde logs die serverless cold starts en Edge-beperkingen doorstaan. Je oefent met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) te beginnen?
Ervaring vooraf is niet nodig. Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “Gestructureerde logging voor Server en Edge”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)?
Ja. Elke les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- OpenTelemetry-tracing met instrumentation.ts
- Fijnmazige error.tsx- en global-error-boundaries
- Gestructureerde logging voor Server en Edge
- Fouten en telemetrie van Server Actions vastleggen