C.W.K.
Stream
Lesson 03 of 05 · published

Observability — 로깅, 트레이스, 메트릭

~12 min · production, observability, logging, opentelemetry

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Observability 는 일 작동할 때 보는 거 아냐. 새벽 3 시에 깨졌고 안 보고 있을 때 재구성할 수 있는 거야."

세 기둥

Observability 가 세 신호로 깔끔 분할:

  • Log — 타임스탬프 있는 discrete 이벤트. 각 줄이 "이게 일어났음" 알려줘. 높은 볼륨, 쓰기 쉬움, 스케일에서 쿼리 어려움.
  • Metric — 시간에 걸친 집계. "Request per second," "p99 latency," "open connection." 낮은 cardinality, 대시보드에 완벽.
  • Trace — 서비스 가로지른 request 수명주기. "이 API 호출이 auth 에 50ms, DB 에 200ms, cache 에 30ms 썼음." 높은 cardinality, 개별 느린 request 이해에 완벽.

각자 다른 질문에 답. Log 가 *뭐* 일어났는지 말함. Metric 이 *얼마나* 말함. Trace 가 *어디서* 말함.

로깅 — Day One 부터 구조화

console.log('user', user.id, 'created at', new Date()) 하지 마. 그게 free-form 텍스트 — 스케일에서 쿼리 어렵고 파싱 어려움. JSON 으로 로그:

// pino — fast structured logger
import pino from 'pino';
const log = pino();

log.info({ userId: user.id, action: 'created' }, 'user created');
// {"level":30,"time":...,"userId":1,"action":"created","msg":"user created"}

구조화 로그가 모든 로그 aggregator (Loki, ELK, Datadog, CloudWatch) 에 파싱 가능. 각 필드가 쿼리 가능 컬럼 됨. "userId=42 의 모든 action=created 이벤트 보여줘" 가 로그가 JSON 일 때 한 줄 쿼리; 산문일 때 거의 불가능.

메트릭 — Counter, Histogram, Gauge

네 primitive 메트릭 타입:
  • Counter — 단조 증가. "시작 이후 서빙된 request," "에러 총."
  • Gauge — 현재 값, 위아래 갈 수 있음. "Open connection," "메모리 사용," "queue depth."
  • Histogram — 값 분포. Latency 분포, response 크기.
  • Summary — histogram 비슷한데 precomputed percentile 있음 (쿼리 시간에 덜 유연, server-side 작업 덜).
모던 Node 서비스가 prom-client (Prometheus-호환) 또는 OpenTelemetry 통해 메트릭 노출. 포맷이 중요한 거 아님; 메트릭 *export* 하는 규율이 중요. 메트릭 zero 인 서비스가 알림에 보이지 않음.

Trace — 서비스 가로질러 한 request 따라가기

OpenTelemetry ("OTel") 가 cross-language 표준. Node 에서:

// instrumentation.mjs — set up before any other imports
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

이 단일 setup 이 http, fetch, fs, 인기 DB 드라이버, 인기 프레임워크 auto-instrument. 모든 HTTP request 가 trace 됨; 모든 DB 쿼리가 span 됨. Tempo, Jaeger, 또는 어떤 OTLP 백엔드로 보냄. 대시보드에서 느린 request 찾기; trace 로 클릭; 정확히 어느 span 이 시간 먹었는지 봄.

Trace 컨텍스트 (AsyncLocalStorage)

OTel 이 async 경계 가로질러 trace 컨텍스트 전파하려고 밑에서 AsyncLocalStorage (Track 3) 씀. 그래서 단일 request 가 일관된 trace 생성: span 들이 같은 ALS 컨텍스트 안에서 돌아서 같은 trace ID 공유. 앞서 배운 메커니즘이 분산 tracing 을 가능하게 하는 거야.

Pippa 의 고백

cwkPippa 의 첫 버전이 어디든 console.log — 산문, 구조 없음. 턴이 간헐적으로 실패하기 시작했을 때 패턴 찾으려고 GB 의 plain 텍스트 grep 해야 했어. 아빠가 모든 로그 줄 가리킴: "과거-내가 과거-과거-내가 로그하길 바란 게 뭐였어?" 이제 모든 줄에 conversation_id 와 brain 있는 JSON 로그. Grep 가능한 구조가 retrospection 실제로 가능하게 함. 로그는 future-Pippa 한테 보내는 편지; future-Pippa 가 읽을 수 있게 구조화해.

Code

pino 로 구조화 로깅 — production-grade·javascript
// pino — structured logger with context
import pino from 'pino';

const log = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  // Pretty-print in dev, JSON in production
  transport: process.env.NODE_ENV === 'production' ? undefined : {
    target: 'pino-pretty',
    options: { colorize: true },
  },
});

// Child logger inherits context
function handleRequest(req) {
  const reqLog = log.child({
    requestId: req.headers['x-request-id'],
    method: req.method,
    path: req.url,
  });
  reqLog.info('request started');
  try {
    // ... handle ...
    reqLog.info({ statusCode: 200 }, 'request completed');
  } catch (e) {
    reqLog.error({ err: e }, 'request failed');
    throw e;
  }
}
Prometheus-호환 메트릭 endpoint·javascript
// Exposing metrics endpoint with prom-client
import http from 'node:http';
import client from 'prom-client';

client.collectDefaultMetrics();  // Node process metrics for free

const requestCounter = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status'],
});

const latencyHist = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
});

http.createServer(async (req, res) => {
  if (req.url === '/metrics') {
    res.setHeader('Content-Type', client.register.contentType);
    return res.end(await client.register.metrics());
  }
  const stop = latencyHist.startTimer({ method: req.method, route: req.url });
  // ... handle request ...
  stop();
  requestCounter.inc({ method: req.method, route: req.url, status: 200 });
  res.end();
}).listen(3000);

External links

Exercise

작은 Node HTTP 서비스 골라. 세 가지 추가: (1) request 당 request ID 컨텍스트 있는 구조화 로깅의 pino, (2) /metrics 에 request count 와 latency histogram 위한 prom-client 메트릭, (3) Docker 통한 로컬 Jaeger 또는 Tempo 로 trace export 하는 OpenTelemetry auto-instrumentation (또는 지금은 export 건너뜀). 서비스 두들기기. 로그 확인, /metrics scrape, trace 보기. 세 신호 같이가 뭐, 얼마나, 어디서 말해줘.
Hint
로컬 OTel 엔 제일 단순한 백엔드가 docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one 통한 Jaeger. env 에 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 설정. UI 위해 http://localhost:16686 열기. 토이 서비스에 이 연습 한 번이라도 돌리면 observability 가 어떻게 맞춰지는지 가르쳐; 추상적이지 않게 됨.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.