Marketplace 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.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/doyajin174/structured-logging" ~/.claude/skills/aiskillstore-marketplace-structured-logging-0bf65f && rm -rf "$T"
manifest:
skills/doyajin174/structured-logging/SKILL.mdsource content
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 | 용도 | 예시 |
|---|---|---|
| 시스템 종료 필요 | DB 연결 완전 실패 |
| 에러 발생, 복구 가능 | API 호출 실패 |
| 잠재적 문제 | 지연된 응답 |
| 주요 이벤트 | 사용자 로그인 성공 |
| 디버깅 정보 | 함수 파라미터 |
| 상세 추적 | 실행 흐름 |
프로덕션 로그 레벨
프로덕션: 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 포맷 사용
- 적절한 로그 레벨 사용
- 비즈니스 이벤트 로깅
- 에러에 스택 트레이스 포함
- 성능 측정 로깅
운영
- 로그 집계 시스템 연동
- 로그 기반 알림 설정
- 로그 보관 정책 수립