본문 바로가기
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
"관측 가능성은 대시보드가 많다는 뜻이 아니야. 예상하지 못한 장애가 생겼을 때, 남아 있는 신호만으로 무슨 일이 있었는지 물을 수 있다는 뜻이야."

로그, 메트릭, 트레이스가 답하는 질문

  • 로그: 특정 시점에 어떤 사건이 일어났는지 기록한다. 오류 메시지와 요청 문맥처럼 개별 사건의 세부 정보를 담기 좋다.
  • 메트릭: 시간에 따른 수치와 분포를 집계한다. 요청률, 오류율, p99 지연, 열린 연결 수처럼 서비스 상태의 변화를 알림과 대시보드에 쓰기 좋다.
  • 트레이스: 요청 하나가 여러 작업과 서비스를 지나간 경로를 span으로 잇는다. 지연이 어느 구간에서 생겼는지 찾는 데 유용하다.

세 신호는 서로 대신하지 않아. 메트릭으로 이상을 발견하고, 트레이스로 느린 구간을 좁힌 뒤, 같은 요청 ID나 trace ID가 담긴 로그에서 세부 사건을 확인하는 식으로 이어져야 해.

구조화 로그에는 문맥과 경계를 함께 담아

운영 로그는 사람이 읽는 문장만 모아 두기보다 JSON처럼 필드가 분리된 형식이 다루기 쉬워. 요청 ID, 서비스 이름, 로그 수준, 작업 이름처럼 반복해서 검색할 문맥을 일정한 키로 남겨. 메시지는 사람이 이해할 설명을 맡고, 필드는 필터와 집계를 맡는 거야.

import pino from 'pino';

const log = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  redact: ['req.headers.authorization'],
});

const requestLog = log.child({ requestId, route: '/users/:id' });
requestLog.info({ userId }, 'user loaded');

전체 요청이나 사용자 객체를 그대로 기록하면 토큰, 쿠키, 개인정보가 섞일 수 있어. 필요한 필드만 허용하고 로거의 redaction 기능을 방어선으로 더해. 로그 보존 기간과 접근 권한도 애플리케이션 보안의 일부야.

메트릭은 cardinality를 설계해야 해

Counter, gauge, histogram은 서로 다른 모양의 수치를 표현해.
  • Counter: 요청 수나 오류 수처럼 누적되는 값.
  • Gauge: 큐 길이나 열린 연결 수처럼 오르내리는 현재 값.
  • Histogram: 요청 지연이나 응답 크기의 분포.
레이블에는 HTTP 메서드와 정규화된 route처럼 값의 종류가 제한된 항목을 써. 사용자 ID, 원본 URL, 오류 메시지를 레이블로 넣으면 시계열 수가 폭발해 저장 비용과 질의 성능을 망칠 수 있어. 이런 고유 값은 로그나 트레이스 속성으로 보내는 편이 맞아.

트레이스는 요청의 경로를 잇는다

OpenTelemetry는 계측 코드와 내보내기 형식을 특정 관측 백엔드에서 분리하는 표준이야. Node SDK와 지원되는 자동 계측을 애플리케이션 코드보다 먼저 초기화하면 HTTP 클라이언트와 서버, 일부 프레임워크와 데이터베이스 라이브러리에 span을 붙일 수 있어. 자동 계측 범위는 패키지와 버전에 따라 다르므로 실제로 생성된 span을 확인해야 해.

// instrumentation.mjs
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(),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

트레이스도 무한히 모을 수는 없어. 샘플링 정책을 정하고, 쿼리 원문이나 사용자 데이터 같은 민감한 속성이 span에 들어가지 않도록 검토해. 중요한 작업이 자동 계측 범위 밖이라면 직접 span을 추가하되, 의미 있는 경계에만 두는 편이 좋아.

비동기 문맥과 상관관계

Node용 OpenTelemetry는 비동기 작업 사이에서 현재 trace 문맥을 이어 가기 위해 AsyncLocalStorage 계열의 문맥 관리 기능을 사용해. 그 덕분에 한 요청에서 생긴 하위 작업들이 같은 trace에 연결될 수 있어. 로그에도 현재 trace ID와 span ID를 넣으면 트레이스 화면에서 발견한 요청을 로그 검색으로 바로 이어 갈 수 있지.

문맥이 끊기는 사용자 정의 스케줄러나 지원되지 않는 라이브러리를 만나면 span이 서로 다른 trace로 갈라질 수 있어. 자동 계측을 설치했다는 사실보다 실제 요청 하나가 끝까지 연결되는지 검증하는 일이 중요해.

Pippa의 고백

산문 로그만 잔뜩 남기면 장애 뒤에 grep으로 단어를 추측하게 돼. 요청 ID와 작업 이름을 일정한 필드로 남기고 나면 질문이 달라져. “무슨 문자열을 검색하지?”가 아니라 “이 요청에서 어떤 단계가 실패했지?”가 되거든. 관측 신호는 미래의 조사자에게 보내는 기록이야. 그 조사자가 문맥을 다시 조립할 수 있게 남겨야 해.

Code

요청 문맥과 redaction을 갖춘 구조화 로그·javascript
// 요청 문맥을 상속하는 구조화 로거
import pino from 'pino';

const log = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  redact: [
    'req.headers.authorization',
    'req.headers.cookie',
    'password',
  ],
});

function handleRequest(req) {
  const requestLog = log.child({
    requestId: req.headers['x-request-id'],
    method: req.method,
    route: '/users/:id',
  });

  requestLog.info('request started');
  try {
    // 요청을 처리한다
    requestLog.info({ statusCode: 200 }, 'request completed');
  } catch (error) {
    requestLog.error({ err: error }, 'request failed');
    throw error;
  }
}
cardinality를 제한한 Prometheus 메트릭·javascript
// 낮은 cardinality 레이블을 쓰는 Prometheus 메트릭
import http from 'node:http';
import client from 'prom-client';

client.collectDefaultMetrics();

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

const latency = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route', 'status'],
});

http.createServer(async (req, res) => {
  if (req.url === '/metrics') {
    res.setHeader('Content-Type', client.register.contentType);
    res.end(await client.register.metrics());
    return;
  }

  const method = req.method;
  const route = '/example'; // 원본 URL 대신 정규화된 경로를 쓴다
  const end = latency.startTimer({ method, route });
  const status = '200';

  requests.inc({ method, route, status });
  end({ status });
  res.writeHead(Number(status)).end('ok');
}).listen(3000);

External links

Exercise

작은 Node HTTP 서비스에 세 신호를 연결해 봐. 요청 ID와 정규화된 route가 담긴 구조화 로그를 남기고, 요청 수와 지연 분포를 /metrics로 내보내고, OpenTelemetry로 요청 trace를 하나 생성해. 같은 요청을 세 신호에서 찾아 서로 어떻게 이어지는지 확인해.
Hint
메트릭 레이블에는 원본 URL이나 요청 ID를 넣지 말고 정규화된 route를 써. 로그에는 요청 ID를, 가능하다면 로그와 trace 양쪽에는 trace ID를 남겨. 자동 계측을 쓴다면 초기화 파일이 애플리케이션보다 먼저 로드됐는지도 확인해.

Progress

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

댓글 0

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

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