Structured Logging: Warum printf-Debugging ned skaliert
console.log('user logged in', userId) funktioniert wunderbar, bis du zehn Server, tausend Logzeilen pro Sekunde und an Vorfall host, wo du jeden Request von oam Kunden üba drei Services in de letzten zwanzig Minuten finden muasst. Do hören Klartext-Logs auf, a Debugging-Werkzeug zum sei, und werdn zum Heuhaufn. Structured Logging is die Lösung, und es kost fost nix, es einzuführn.
Des Problem mit String-Logs
A klassische Logzeile is a menschenlesbarer Satz:
2026-03-17T09:14:02Z User 4821 logged in from 203.0.113.4 after 2 failed attempts
Isoliert leicht zum lesn. Im großen Stil fost unmöglich abzufrogn. Um jeden Login von ana bestimmten IP über a Woch Logs zu finden, schreibst a Regex gegn Freitext, hoffst, dass niemand jemois den Wortlaut von dera Logzeile ändert (des bricht jede gspeicherte Abfrage), und hoffst, dass de Felder, de du brauchst - User-ID, IP, Anzoi Versuche - überhaupt an parsebarer Stelle drin warn.
Structured Logging: Daten loggn, ned Sätze
A strukturierte Logzeile is a maschinenlesbarer Datensatz, fost imma JSON, wo jedes Stückl Kontext a eigenes Feld is:
logger.info('user_login', {
userId: 4821,
ip: '203.0.113.4',
failedAttempts: 2,
});
{"level":"info","event":"user_login","userId":4821,"ip":"203.0.113.4","failedAttempts":2,"timestamp":"2026-03-17T09:14:02.118Z"}
Jetzt is "jeder Login von dera IP" a Abfrage aufs ip-Feld, ned a Regex gegn Prosa. "Olle Events für User 4821 in da letztn Stund" is a Filter, ka grep. Da Event-Nama (user_login) bleibt stabil, a wenn du später beschließt, umzuformulieren, wos a menschenlesbare Version sogn würd - weil's beim Schreibn ka menschenlesbare Version gibt; Formatierung is a Anzeigeproblem, des später von dem erledigt wird, wos de Logs liest.
A minimaler strukturierter Logger
Du brauchst ka schwergewichtiges Framework für den Hauptnutzen. Do is a Logger, der konsistente Basisfelder anhängt und imma JSON ausgibt:
function createLogger(baseFields = {}) {
const emit = (level, event, fields = {}) => {
const record = {
level,
event,
timestamp: new Date().toISOString(),
...baseFields,
...fields,
};
process.stdout.write(JSON.stringify(record) + '\n');
};
return {
info: (event, fields) => emit('info', event, fields),
warn: (event, fields) => emit('warn', event, fields),
error: (event, fields) => emit('error', event, fields),
child: (extraFields) => createLogger({ ...baseFields, ...extraFields }),
};
}
const logger = createLogger({ service: 'auth-api' });
logger.info('user_login', { userId: 4821, ip: '203.0.113.4' });
child() is wichtiger, ois es ausschaut: Damit hängst Kontext einmal an und er fließt durch jeden folgenden Aufruf, statt dass du Felder überoi wiederholst.
Request-bezogener Kontext: Da echte Lohn
Da wirkliche Gewinn zeigt si, wenn jede Logzeile für oan einzigen Request automatisch a gemeinsame Request-ID trägt, ohne dass jede Aufrufstelle sie explizit weitergebn muass.
import { AsyncLocalStorage } from 'node:async_hooks';
const requestContext = new AsyncLocalStorage();
function withRequestContext(requestId, fn) {
return requestContext.run({ requestId }, fn);
}
function requestLogger(event, fields = {}) {
const context = requestContext.getStore() ?? {};
logger.info(event, { ...context, ...fields });
}
// In middleware, per incoming request:
app.use((req, res, next) => {
withRequestContext(crypto.randomUUID(), next);
});
// Anywhere deeper in the call stack, no requestId parameter needed:
requestLogger('db_query_start', { table: 'orders' });
requestLogger('db_query_end', { table: 'orders', durationMs: 12 });
Jede Logzeile, de irgendwo während da Lebenszeit von dem Request ausgebn wird - drei Services tief, in am Callback, in am Error-Handler - trägt dieselbe requestId. Genau des macht aus "find olles, wos während dem oan fehlgschlogenen Request passiert is" von am Archäologie-Projekt an einzigen Filter.
Wos wirklich in a Logfeld gehört
- IDs, ned Beschreibungen.
userId: 4821, ned"for user number 4821". Lass den Leser gegn deine User-Tabelle joinen, wenn er den Nama braucht. - Dauern ois Zohln, in oaner einheitlichen Einheit.
durationMs: 340, imma Millisekunden, nie"340ms"und2.1(Sekunden) an verschiedenen Aufrufstellen mischn. - Stabile Event-Namen.
user_login, ned a Satz, dessen Wortlaut zwischn Commits wechselt. Behandl den Event-Nama wia a API - Umbenennen bricht jedes Dashboard und jeden Alert, der drauf aufbaut. - Fehler ois strukturierte Objekte, ned nur ihr
.message:
logger.error('payment_failed', {
orderId: order.id,
error: {
message: err.message,
name: err.name,
stack: err.stack,
},
});
Wos des ned ersetzt
Structured Logging is ka Ersatz für Metriken oda Traces - es is die dritte Säule von Observability, und die am besten geeignete für "wos genau is passiert, der Reihe noch, bei dem oan Request". Metriken beantworten billig "wie vül" über die Zeit; Traces beantworten "wo is de Zeit hingangen" über Services hinweg. Logs beantworten "wöche Werte woarn tatsächlich im Spiel". Nimm das, wos de Frage beantwortet, die du wirklich stellst - bei am konkreten fehlgschlogenen Request von an bestimmten Kunden sand des fost imma Logs.
Fazit
Da Umstieg von String-Logs auf strukturierte Logs is beim Schreibn klan - a Objekt übagebn statt an Satz zammzubaun - und beim Lesn enorm, weil jedes Feld abfragbar wird, statt in Prosa vagrabn zum sei. Setz Request-Kontext obendrauf und du kriagst, wos printf-Debugging nie kunnt hätt: olle Zeilen zu oam Request, aus am Wasserfall von allen ondern, in oaner Abfrage rauszuziehn.