structured-logging

Implement JSON-based structured logging for observability. Use when setting up logging, debugging production issues, or preparing for log aggregation (ELK, Datadog). Covers log levels, context, and best practices.

allowed_tools: Read, Glob, Grep, Edit, Write, Bash

$ Installieren

git clone https://github.com/Doyajin174/myskills /tmp/myskills && cp -r /tmp/myskills/.public/skills/structured-logging ~/.claude/skills/myskills

// tip: Run this command in your terminal to install the skill


name: structured-logging description: Implement JSON-based structured logging for observability. Use when setting up logging, debugging production issues, or preparing for log aggregation (ELK, Datadog). Covers log levels, context, and best practices. allowed-tools: Read, Glob, Grep, Edit, Write, Bash license: MIT metadata: author: antigravity-team version: "1.0"

Structured Logging

JSON 포맷의 구조화된 로깅을 구현하는 스킬입니다.

Core Principle

"print문 대신 구조화된 로그를 남겨라." "로그는 검색 가능하고, 집계 가능해야 한다."

왜 Structured Logging인가?

❌ 일반 텍스트 로그

[2024-01-15 10:30:45] ERROR User login failed for user123
[2024-01-15 10:30:46] INFO Processing request
  • 파싱 어려움
  • 필터링/검색 제한
  • 컨텍스트 손실

✅ 구조화된 로그 (JSON)

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "level": "error",
  "message": "User login failed",
  "userId": "user123",
  "errorCode": "AUTH_INVALID_PASSWORD",
  "requestId": "req-abc-123",
  "duration": 45
}
  • 쉬운 파싱/검색
  • 필드별 필터링
  • 풍부한 컨텍스트

Log Levels

Level용도예시
fatal시스템 종료 필요DB 연결 완전 실패
error에러 발생, 복구 가능API 호출 실패
warn잠재적 문제지연된 응답
info주요 이벤트사용자 로그인 성공
debug디버깅 정보함수 파라미터
trace상세 추적실행 흐름

프로덕션 로그 레벨

프로덕션: info 이상만
개발: debug 이상
디버깅 시: trace까지

필수 로그 필드

interface LogEntry {
  // 필수
  timestamp: string;    // ISO 8601
  level: string;        // error, warn, info, debug
  message: string;      // 사람이 읽을 수 있는 메시지

  // 권장
  requestId?: string;   // 요청 추적
  userId?: string;      // 사용자 식별
  service?: string;     // 서비스명
  environment?: string; // prod, staging, dev

  // 상황별
  error?: {
    name: string;
    message: string;
    stack?: string;
  };
  duration?: number;    // ms
  metadata?: Record<string, unknown>;
}

Node.js 구현

Pino (권장 - 고성능)

npm install pino pino-pretty
// lib/logger.ts
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',

  // 기본 필드
  base: {
    service: 'my-app',
    environment: process.env.NODE_ENV,
  },

  // 타임스탬프 포맷
  timestamp: pino.stdTimeFunctions.isoTime,

  // 개발 환경: pretty print
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty' }
    : undefined,
});

// 사용
logger.info({ userId: '123' }, 'User logged in');
logger.error({ error, requestId }, 'Request failed');

Winston

npm install winston
// lib/logger.ts
import winston from 'winston';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: {
    service: 'my-app',
    environment: process.env.NODE_ENV,
  },
  transports: [
    new winston.transports.Console({
      format: process.env.NODE_ENV === 'development'
        ? winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
          )
        : winston.format.json(),
    }),
  ],
});

Request Context

Request ID 전파

// middleware/requestId.ts
import { randomUUID } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const requestId = request.headers.get('x-request-id') || randomUUID();

  const response = NextResponse.next();
  response.headers.set('x-request-id', requestId);

  return response;
}

AsyncLocalStorage (권장)

// lib/context.ts
import { AsyncLocalStorage } from 'async_hooks';

interface RequestContext {
  requestId: string;
  userId?: string;
  startTime: number;
}

export const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();

// 미들웨어에서 설정
export function withContext<T>(context: RequestContext, fn: () => T): T {
  return asyncLocalStorage.run(context, fn);
}

// 로거에서 사용
export function getContext(): RequestContext | undefined {
  return asyncLocalStorage.getStore();
}

Context-aware Logger

// lib/logger.ts
import pino from 'pino';
import { getContext } from './context';

const baseLogger = pino({ /* config */ });

export const logger = {
  info: (obj: object, msg?: string) => {
    const ctx = getContext();
    baseLogger.info({ ...obj, ...ctx }, msg);
  },
  error: (obj: object, msg?: string) => {
    const ctx = getContext();
    baseLogger.error({ ...obj, ...ctx }, msg);
  },
  // ... other levels
};

로깅 패턴

API 요청 로깅

// middleware/logging.ts
export async function loggingMiddleware(req: Request, handler: Function) {
  const startTime = Date.now();
  const requestId = randomUUID();

  logger.info({
    requestId,
    method: req.method,
    url: req.url,
    userAgent: req.headers.get('user-agent'),
  }, 'Request started');

  try {
    const response = await handler(req);

    logger.info({
      requestId,
      statusCode: response.status,
      duration: Date.now() - startTime,
    }, 'Request completed');

    return response;
  } catch (error) {
    logger.error({
      requestId,
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
      },
      duration: Date.now() - startTime,
    }, 'Request failed');

    throw error;
  }
}

비즈니스 이벤트 로깅

// 사용자 활동
logger.info({
  event: 'user.login',
  userId,
  method: 'google_oauth',
  ip: request.ip,
}, 'User logged in');

// 결제
logger.info({
  event: 'payment.success',
  userId,
  amount: 9900,
  currency: 'KRW',
  paymentId,
}, 'Payment completed');

// 에러
logger.error({
  event: 'payment.failed',
  userId,
  amount: 9900,
  errorCode: 'CARD_DECLINED',
  paymentId,
}, 'Payment failed');

성능 로깅

async function fetchData() {
  const startTime = Date.now();

  try {
    const result = await db.query(/* ... */);

    logger.info({
      operation: 'db.query',
      table: 'users',
      duration: Date.now() - startTime,
      rowCount: result.length,
    }, 'Database query completed');

    return result;
  } catch (error) {
    logger.error({
      operation: 'db.query',
      table: 'users',
      duration: Date.now() - startTime,
      error: error.message,
    }, 'Database query failed');

    throw error;
  }
}

금지 패턴

// ❌ BAD: 민감 정보 로깅
logger.info({ password, creditCard, ssn }, 'User data');

// ❌ BAD: 과도한 로깅 (성능 저하)
for (const item of items) {
  logger.debug({ item }, 'Processing item');  // 수천 번 호출
}

// ❌ BAD: 구조화되지 않은 로그
logger.info(`User ${userId} logged in at ${timestamp}`);

// ✅ GOOD: 구조화된 로그
logger.info({ userId, timestamp }, 'User logged in');

민감 정보 제거

// lib/logger.ts
const sensitiveFields = ['password', 'token', 'apiKey', 'creditCard'];

function redactSensitiveData(obj: object): object {
  const redacted = { ...obj };

  for (const key of Object.keys(redacted)) {
    if (sensitiveFields.some(f => key.toLowerCase().includes(f))) {
      redacted[key] = '[REDACTED]';
    }
  }

  return redacted;
}

// Pino redact 옵션
const logger = pino({
  redact: ['password', 'creditCard', '*.token', 'headers.authorization'],
});

Log Aggregation 연동

ELK Stack (Elasticsearch)

// filebeat.yml에서 JSON 파싱
// 또는 직접 Elasticsearch로 전송
import { Client } from '@elastic/elasticsearch';

const esClient = new Client({ node: 'http://localhost:9200' });

const esTransport = new winston.transports.Stream({
  stream: {
    write: async (log: string) => {
      await esClient.index({
        index: 'app-logs',
        document: JSON.parse(log),
      });
    },
  },
});

Datadog

npm install dd-trace
// tracer.ts
import tracer from 'dd-trace';

tracer.init({
  service: 'my-app',
  env: process.env.NODE_ENV,
});

// 로그에 trace ID 포함
logger.info({
  dd: {
    trace_id: tracer.scope().active()?.context().toTraceId(),
    span_id: tracer.scope().active()?.context().toSpanId(),
  },
}, 'Event with trace');

Checklist

설정

  • 구조화된 로깅 라이브러리 설치 (Pino/Winston)
  • 로그 레벨 환경변수 설정
  • 기본 필드 (service, environment) 설정
  • Request ID 미들웨어 적용
  • 민감 정보 redaction 설정

로깅 표준

  • JSON 포맷 사용
  • 적절한 로그 레벨 사용
  • 비즈니스 이벤트 로깅
  • 에러에 스택 트레이스 포함
  • 성능 측정 로깅

운영

  • 로그 집계 시스템 연동
  • 로그 기반 알림 설정
  • 로그 보관 정책 수립

References