0Pricing
Node.js Backend Development Bootcamp · 강의

상관관계 ID를 활용한 구조화된 로깅

기계가 분석할 수 있는 JSON 로그를 출력하고 비동기 컨텍스트를 통해 상관관계 ID를 전달해 요청을 추적합니다.

상관관계 ID를 활용한 구조화된 로깅은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Structured Logging

In production, logs are read by machines before humans. A line like console.log('User ' + id + ' failed login') is a free-form string: to find it later you must write brittle regex and you cannot aggregate or filter reliably.

Structured logging emits each log as a JSON object with consistent fields. Log aggregators (Loki, Elasticsearch, Datadog, CloudWatch) index those fields so you can query level="error" AND userId=42 instantly.

  • Free-form: easy to write, painful to query.
  • Structured: one JSON object per line (NDJSON), trivially parseable.

A Minimal JSON Logger

The core idea is small: build a plain object, stamp it with a level and timestamp, and write one JSON string per line to stdout. The runtime or container collects stdout and ships it to your aggregator.

Each log is a single line of valid JSON. This format is called NDJSON (newline-delimited JSON) and every modern log backend understands it.

function log(level, message, fields = {}) {
  const entry = {
    level,
    time: new Date().toISOString(),
    message,
    ...fields,
  };
  process.stdout.write(JSON.stringify(entry) + '\n');
}

log('info', 'server started', { port: 3000 });
log('error', 'login failed', { userId: 42, reason: 'bad_password' });

Use a Real Logger: pino

Hand-rolling works for learning, but production apps use a battle-tested logger. pino is the de-facto choice for Node.js: it is extremely fast because it serializes JSON in a worker-friendly way and writes asynchronously.

  • logger.info(obj, msg) — the first arg is the object of fields, the second is the message string.
  • Levels: trace, debug, info, warn, error, fatal.
  • In production you pipe stdout to pino-pretty only in dev; raw JSON goes to prod.
const pino = require('pino');
const logger = pino({ level: 'info' });

logger.info({ port: 3000 }, 'server started');
logger.error({ userId: 42, reason: 'bad_password' }, 'login failed');

// Child loggers bind fields onto every subsequent log:
const reqLog = logger.child({ requestId: 'abc-123' });
reqLog.info('handling request');

The Problem: Tracing One Request

Under load, hundreds of requests interleave their logs. When user 42 reports an error, you need to see every log line that belongs to their request — across middleware, services, and database calls.

The solution is a correlation ID (also called request ID or trace ID): a unique identifier generated once per request and attached to every log emitted while handling that request.

  • Query correlationId="7f3a..." and the aggregator reconstructs the whole request timeline.
  • If the ID arrives in an incoming header, you can correlate logs across services.

Generating and Accepting the ID

At the edge of your system, read an incoming correlation header if a caller (gateway, upstream service) already set one; otherwise generate a fresh UUID. Always echo it back in the response so clients and proxies can record it too.

Standard header names are x-request-id or x-correlation-id. Reusing an inbound ID is what makes tracing work across service boundaries.

const { randomUUID } = require('crypto');

function correlationMiddleware(req, res, next) {
  const incoming = req.headers['x-correlation-id'];
  const correlationId = incoming || randomUUID();
  req.correlationId = correlationId;
  res.setHeader('x-correlation-id', correlationId);
  next();
}

module.exports = { correlationMiddleware };

The Naive Approach and Its Pain

The obvious move is to pass req.correlationId as an argument into every function and every log call. This works but does not scale: a deep call stack (controller → service → repository → helper) forces you to thread the ID through functions that otherwise have no reason to know about it.

This is prop drilling for the backend. You want the ID available implicitly to any code running during the request — without changing every function signature.

AsyncLocalStorage to the Rescue

Node.js ships AsyncLocalStorage (in the async_hooks module). It creates a store that stays bound to the current asynchronous execution context — surviving across await, callbacks, timers, and promises — without being shared between concurrent requests.

Think of it as request-scoped thread-local storage. You call als.run(store, callback) once per request; anywhere inside that callback (no matter how deep or how many awaits later) als.getStore() returns the same store.

const { AsyncLocalStorage } = require('async_hooks');
const als = new AsyncLocalStorage();

async function deep() {
  await new Promise((r) => setTimeout(r, 10));
  // Same store, even after awaits and timers:
  return als.getStore().correlationId;
}

async function main() {
  await als.run({ correlationId: 'req-1' }, async () => {
    console.log('inside:', await deep());
  });
  console.log('outside:', als.getStore());
}

main();

Wiring AsyncLocalStorage into the Request

Replace the naive middleware: instead of attaching the ID to req, run the rest of the request inside als.run() with the ID in the store. Now every line of code executed for this request — controllers, services, DB callbacks — can reach the ID via getStore().

Crucially, you must call next() inside the run callback so the downstream handlers inherit the context.

const { AsyncLocalStorage } = require('async_hooks');
const { randomUUID } = require('crypto');

const als = new AsyncLocalStorage();

function context() {
  return als.getStore() || {};
}

function correlationMiddleware(req, res, next) {
  const correlationId = req.headers['x-correlation-id'] || randomUUID();
  res.setHeader('x-correlation-id', correlationId);
  als.run({ correlationId }, () => next());
}

module.exports = { als, context, correlationMiddleware };

Auto-Injecting the ID into Every Log

The payoff: wrap your logger so it reads the correlation ID from AsyncLocalStorage automatically. Application code calls log.info('saved order') with no ID argument, yet every emitted line carries the right correlationId.

With pino you express this declaratively using the mixin option, which merges extra fields into every log record at write time.

const pino = require('pino');
const { als } = require('./context');

const logger = pino({
  level: 'info',
  mixin() {
    const store = als.getStore();
    return store ? { correlationId: store.correlationId } : {};
  },
});

// Anywhere deep in the request, no ID passed explicitly:
function saveOrder(order) {
  logger.info({ orderId: order.id }, 'order saved');
}

module.exports = { logger, saveOrder };

Propagating the ID to Downstream Calls

Correlation only spans services if you forward the ID on outbound requests. When your service calls another HTTP API, read the ID from context and set it as a header. The downstream service's middleware will reuse it, so both services' logs share one ID.

The same principle applies to message queues (put the ID in message metadata) and background jobs (store it on the job payload).

const { context } = require('./context');

async function callInventoryService(sku) {
  const { correlationId } = context();
  const res = await fetch('https://inventory.internal/check', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-correlation-id': correlationId,
    },
    body: JSON.stringify({ sku }),
  });
  return res.json();
}

module.exports = { callInventoryService };

Production Hygiene

A few rules keep your structured logs clean and safe:

  • Never log secrets or PII — passwords, tokens, full card numbers. Configure pino redact paths (e.g. ['req.headers.authorization', '*.password']).
  • Log at the right level — info for business events, warn for recoverable issues, error with the serialized error object for failures.
  • Keep field names stable — correlationId always means the same thing; dashboards and alerts depend on it.
  • One JSON object per line — do not pretty-print in production; it breaks NDJSON parsing.
const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['req.headers.authorization', 'password', '*.password', 'creditCard'],
});

logger.info({ user: { id: 7, password: 'hunter2' } }, 'login');
// -> password is replaced with [Redacted] in the output

Quick Check

You need the correlation ID available to every function in a deep call stack during a request, without passing it as an argument and without leaking it between concurrent requests. What is the correct mechanism in Node.js?

Recap

You learned how to make logs both machine-parseable and traceable:

  • Structured logging emits one JSON object per line (NDJSON) so aggregators can index and query fields.
  • pino is the standard fast Node.js logger; the field object comes first, the message second.
  • A correlation ID is generated or reused from an inbound header once per request and echoed in the response.
  • AsyncLocalStorage threads that ID through the whole async call stack without changing function signatures, and isolates concurrent requests.
  • Pino's mixin auto-injects the ID into every log; forwarding it as an outbound header extends tracing across services.
  • Practice good hygiene: redact secrets, use correct levels, keep field names stable, never pretty-print in production.

자주 묻는 질문

“상관관계 ID를 활용한 구조화된 로깅” 강의는 무료인가요?

네 — “상관관계 ID를 활용한 구조화된 로깅” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“상관관계 ID를 활용한 구조화된 로깅”에서 뭘 배우나요?

기계가 분석할 수 있는 JSON 로그를 출력하고 비동기 컨텍스트를 통해 상관관계 ID를 전달해 요청을 추적합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“상관관계 ID를 활용한 구조화된 로깅” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 상관관계 ID를 활용한 구조화된 로깅
  2. OpenTelemetry 스팬을 활용한 분산 추적
  3. 애플리케이션 메트릭 노출 및 RED 방법
  4. AsyncLocalStorage를 활용한 컨텍스트 전파
← Node.js Backend Development Bootcamp(으)로 돌아가기