Advanced (업데이트: 2026. 6. 2.)

Claude Code로 로그와 모니터링 설계하기: 운영 관측성 가이드

구조화 로그, OpenTelemetry, 알림, 헬스체크를 Claude Code로 안전하게 구현하는 방법.

Claude Code로 로그와 모니터링 설계하기: 운영 관측성 가이드

먼저 운영 계약을 정한다

Claude Code에 “로그를 추가해 줘”라고만 말하면 보통 console.log가 늘어납니다. 로컬 디버깅에는 도움이 되지만, 운영 장애에서는 부족합니다. 제대로 된 로그와 모니터링은 어떤 사용자 동작이 어떤 requestId로 흘렀는지, 어느 서비스나 의존성이 느려졌는지, 다음 담당자에게 어떤 정보를 안전하게 넘길 수 있는지 답해야 합니다.

OpenTelemetry Observability primer는 관측성을 시스템 내부를 미리 몰라도 외부 신호로 문제를 이해하는 능력으로 설명합니다. What is OpenTelemetry?도 OpenTelemetry가 백엔드 제품이 아니라 벤더 중립 계측 레이어라고 말합니다. 이 글은 그 원칙을 Claude Code 프롬프트, 실행 가능한 코드, 알림 규칙, 대시보드 리뷰, 인시던트 인계 형식으로 바꿉니다.

Masa가 작은 결제 API에서 처음 시도했을 때 프롬프트는 “로그를 자세히”였습니다. Claude Code는 결제 실패 원인을 남기려고 request body 전체를 기록하는 코드를 제안했습니다. 카드 번호는 없었지만 이메일, 쿠폰, 주소, 고객 메모가 섞일 위험이 있었습니다. 이후 금지 필드를 먼저 쓰고, requestIdtraceparent만 상관 키로 쓰며, redaction 테스트를 요구하자 리뷰가 훨씬 명확해졌습니다.

신호답하는 질문Claude Code 제약
로그무엇이 일어났나JSON, 고정 필드, PII 마스킹
메트릭얼마나 심각한가rate, p95, 오류율
트레이스어디서 느려졌나traceparent 전파, span 이름
헬스체크의존성이 살아 있나의존성별 상태와 latency

안전한 프롬프트와 CLAUDE.md

Claude Code의 memory 문서는 CLAUDE.md가 프로젝트, 사용자, 조직 지침을 세션 시작 때 제공한다고 설명합니다. 관측성 작업에서는 로그 레벨, 필드 이름, 금지 필드, 테스트 명령, 대시보드 이름, 인시던트 인계 템플릿을 넣어 두는 것이 좋습니다. 내부 글 CLAUDE.md best practices와 공식 Claude Code permissions, hooks도 같이 보십시오.

Claude Code task:
- Add observability to the checkout API only.
- Keep all changes inside src/checkout and tests/checkout.
- Use structured JSON logs with requestId and traceparent.
- Never log passwords, tokens, cookies, email, phone, address,
  raw prompt text, or full request/response bodies.
- Add tests proving redaction and requestId propagation.
- Add a /healthz report with database and cache latency.
- Add alert rules for 5xx rate, p95 latency, and redaction failure.
- Show a diff summary and remaining manual checks at the end.

이 프롬프트는 일부러 좁습니다. Claude Code가 모니터링 제품을 새로 고르거나 무관한 라우트를 고치지 않게 하고, 운영 diff를 리뷰할 수 있게 만듭니다.

구조화 로그와 요청 ID

OWASP Logging Cheat Sheet는 로그를 보안 기능처럼 다루라고 합니다. 로그 주입, 접근 제어, 디스크 고갈, 로그 시스템 실패도 테스트 대상입니다. 따라서 Claude Code에는 “문자열을 더 찍어라”가 아니라 “redaction 테스트와 실패 모드를 추가하라”고 요청해야 합니다.

다음은 의존성 없는 JSON logger입니다. structured-logger.mjs로 저장하고 Node.js 18 이상에서 실행할 수 있습니다.

import { randomUUID } from "node:crypto";

const rank = { debug: 10, info: 20, warn: 30, error: 40 };
const current = process.env.LOG_LEVEL || "info";
const threshold = rank[current] ?? rank.info;

const secretKeys = [
  "password",
  "token",
  "authorization",
  "cookie",
  "set-cookie",
  "apikey",
];

function cleanText(value) {
  return String(value).replace(/[\r\n\t]/g, " ").slice(0, 500);
}

function redact(value) {
  if (Array.isArray(value)) return value.map(redact);
  if (!value || typeof value !== "object") return value;

  return Object.fromEntries(
    Object.entries(value).map(([key, item]) => {
      if (secretKeys.includes(key.toLowerCase())) {
        return [key, "[REDACTED]"];
      }
      return [key, redact(item)];
    }),
  );
}

export function log(level, message, fields = {}) {
  if ((rank[level] ?? 99) < threshold) return;

  const entry = {
    ts: new Date().toISOString(),
    level,
    service: process.env.SERVICE_NAME || "checkout-api",
    env: process.env.NODE_ENV || "development",
    requestId: fields.requestId || randomUUID(),
    msg: cleanText(message),
    ...redact(fields),
  };

  process.stdout.write(`${JSON.stringify(entry)}\n`);
}

log("info", "payment accepted", {
  requestId: "req_demo_001",
  userId: "user_123",
  amount: 4980,
  token: "sk_live_should_not_leak",
});

웹 애플리케이션에서는 요청 ID를 응답 헤더로 돌려주고 async context에도 보관합니다. W3C Trace Contexttraceparent를 새로 만들거나 이어받는 처리 모델을 설명합니다. x-request-id는 애플리케이션 상관 ID, traceparent는 분산 트레이싱 전달자라고 분리해 두면 혼란이 줄어듭니다.

import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { log } from "./structured-logger";

type RequestContext = {
  requestId: string;
  traceparent?: string;
  userId?: string;
};

const storage = new AsyncLocalStorage<RequestContext>();

export function getRequestContext() {
  return storage.getStore();
}

export function requestContext(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const started = performance.now();
  const user = (req as Request & { user?: { id?: string } }).user;
  const requestId =
    req.get("x-request-id") ||
    req.get("cf-ray") ||
    randomUUID();

  const context = {
    requestId,
    traceparent: req.get("traceparent"),
    userId: user?.id,
  };

  res.setHeader("x-request-id", requestId);

  storage.run(context, () => {
    res.on("finish", () => {
      const durationMs = Math.round(performance.now() - started);
      const level = res.statusCode >= 500
        ? "error"
        : res.statusCode >= 400
          ? "warn"
          : "info";

      log(level, "http request completed", {
        requestId,
        method: req.method,
        path: req.path,
        statusCode: res.statusCode,
        durationMs,
      });
    });

    next();
  });
}

로그 레벨은 단순해야 합니다. debug는 로컬 세부 정보, info는 정상 주요 이벤트, warn은 복구 가능한 위험, error는 사람이 조사해야 하는 상태입니다.

OpenTelemetry 기본 설정

OpenTelemetry는 앱에서 나오는 신호를 표준화해 기존 백엔드로 보내기 위한 계층입니다. Node.js 설정은 공식 JavaScript Node.js guideJavaScript exporters guide를 기준으로 확인하십시오.

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto \
  @opentelemetry/sdk-metrics
const opentelemetry = require("@opentelemetry/sdk-node");
const {
  getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
  OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-proto");
const {
  OTLPMetricExporter,
} = require("@opentelemetry/exporter-metrics-otlp-proto");
const {
  PeriodicExportingMetricReader,
} = require("@opentelemetry/sdk-metrics");

process.env.OTEL_SERVICE_NAME ||= "checkout-api";

const endpoint =
  process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
  "http://localhost:4318";

const sdk = new opentelemetry.NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: `${endpoint}/v1/traces`,
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: `${endpoint}/v1/metrics`,
    }),
    exportIntervalMillis: 30000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

process.on("SIGTERM", () => {
  sdk.shutdown().finally(() => process.exit(0));
});
flowchart LR
  A["사용자 동작"] --> B["애플리케이션"]
  B --> C["구조화 로그"]
  B --> D["메트릭"]
  B --> E["Trace span"]
  C --> F["로그 저장소"]
  D --> G["알림 규칙"]
  E --> H["Trace 백엔드"]
  F --> I["인시던트 인계"]
  G --> I
  H --> I

헬스체크와 알림 규칙

좋은 헬스체크는 단순한 200 OK가 아닙니다. 데이터베이스, 캐시, 큐, 외부 API를 따로 확인하고 각 의존성의 latency를 반환해야 합니다. 연결 문자열이나 토큰은 절대 반환하지 않습니다.

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("timeout")), ms);
  });
}

export async function buildHealthReport(checks) {
  const started = Date.now();
  const results = {};

  for (const [name, check] of Object.entries(checks)) {
    const before = Date.now();
    try {
      await Promise.race([check(), timeout(800)]);
      results[name] = {
        status: "ok",
        latencyMs: Date.now() - before,
      };
    } catch (error) {
      const message =
        error instanceof Error ? error.message : String(error);
      results[name] = {
        status: "fail",
        latencyMs: Date.now() - before,
        reason: message.slice(0, 120),
      };
    }
  }

  const failed = Object.values(results)
    .filter((item) => item.status === "fail")
    .length;

  return {
    status: failed ? "degraded" : "ok",
    uptimeSec: Math.round(process.uptime()),
    totalLatencyMs: Date.now() - started,
    checks: results,
  };
}

알림은 한 줄의 error 로그가 아니라 시간 창의 비율과 p95로 만듭니다.

groups:
  - name: checkout-api
    rules:
      - alert: CheckoutHigh5xxRate
        expr: |
          sum(rate(http_requests_total{
            service="checkout-api",
            status_code=~"5.."
          }[5m]))
          /
          sum(rate(http_requests_total{
            service="checkout-api"
          }[5m])) > 0.02
        for: 10m
        labels:
          severity: page
        annotations:
          summary: "Checkout 5xx rate is above 2%"

      - alert: CheckoutP95LatencyHigh
        expr: |
          histogram_quantile(
            0.95,
            sum by (le) (
              rate(http_request_duration_seconds_bucket{
                service="checkout-api"
              }[5m])
            )
          ) > 1.5
        for: 15m
        labels:
          severity: ticket
        annotations:
          summary: "Checkout p95 latency is above 1.5s"

세 가지 실제 사용 사례

첫째, 전자상거래 결제 API입니다. orderId, requestId, paymentProvider, amount는 남기되 카드 정보, 이메일, 주소, access token은 남기지 않습니다. 알림은 5xx 비율, 결제 실패율, 결제 제공자 p95 latency를 분리합니다.

둘째, SaaS 관리자 화면입니다. 로그인, 권한 변경, 멤버 초대, 플랜 변경은 감사 로그로 남겨야 합니다. 초대 메일 본문이나 개인 메모는 필요 없습니다. Claude Code에는 감사 로그와 앱 로그 분리, actor ID와 target user ID 분리, RBAC 테스트를 요청합니다.

셋째, 미디어 사이트나 블로그 CMS입니다. 게시, CTA 클릭, 리드 폼 성공, 이미지 생성 실패, 번역 누락을 추적합니다. PV만 보아서는 수익 개선이 어렵습니다. cta_clickgenerate_lead를 분리하고 analytics implementation guide와 함께 대시보드를 확인합니다.

서비스가 여러 개라면 microservices guide도 읽어야 합니다. service.name과 환경 라벨이 흔들리면 OpenTelemetry 검색이 어려워집니다.

실패 모드와 인시던트 인계

자주 생기는 실패는 명확합니다. request body 전체를 기록한다, 자유문 메시지만 남긴다, requestIdtraceId를 여러 이름으로 만든다, /healthz가 의존성을 확인하지 않는다, 알림은 있는데 담당자와 대시보드가 없다.

Claude Code에는 또 다른 위험이 있습니다. 원본 운영 로그를 그대로 붙이면 prompt 안에 개인정보나 비밀이 들어갑니다. 장애 조사를 요청할 때는 마스킹된 로그, 집계 메트릭, 짧은 trace ID 목록만 제공하십시오. 읽기 전용 조사 습관은 permissions guide와 잘 맞습니다.

{
  "incident_id": "INC-2026-06-02-001",
  "severity": "SEV2",
  "owner": "oncall-api",
  "customer_impact": "Checkout errors for some card payments",
  "first_seen": "2026-06-02T09:15:00+09:00",
  "request_ids": ["req_7f3a", "req_8b21"],
  "trace_ids": ["7bba9f33312b3dbb8b2c2c62bb7abe2d"],
  "dashboards": ["Checkout API overview"],
  "current_hypothesis": "Payment provider latency spike",
  "actions_taken": ["Disabled checkout_v2 feature flag"],
  "next_checks": ["Compare p95 latency by region"],
  "do_not_do": ["Do not paste raw customer data into prompts"]
}

대시보드 리뷰, CTA, 검증 결과

대시보드는 매주 검토합니다. 상위 5개 오류, p95 latency, 로그량 증가, 오탐 알림, redaction 실패, 미해결 인시던트를 봅니다. 한 달에 한 번은 실제 장애를 골라 새 온콜 담당자가 로그, 메트릭, trace로 더 빨리 원인에 도달할 수 있었는지 확인합니다.

개인은 free Claude Code cheatsheet로 일상 점검을 고정할 수 있습니다. 재사용 가능한 프롬프트와 설정 자료가 필요하면 products page를 보십시오. 팀에서 로그 표준, CLAUDE.md, 권한, CI, 인시던트 운영을 함께 설계하려면 Claude Code training and consultation이 현실적인 다음 단계입니다.

본문의 흐름을 실제로 따라 해 보니 가장 큰 효과는 금지 필드를 먼저 쓰는 것이었습니다. structured-logger.mjs는 token을 [REDACTED]로 바꾸고 줄바꿈을 한 줄로 정규화했습니다. 캐시 실패를 흉내 내면 health report가 degraded가 되었고, 인계 JSON에 requestIdtraceId만 넣어도 리뷰 대화가 짧아졌습니다.

#Claude Code #로그 #모니터링 #OpenTelemetry #관측성
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.