Structured logging
Skill aiskillstore/marketplace/skills/doyajin174/structured-logging
Security-audited skills for Claude, Codex & Claude Code. One-click install, quality verified.
npx -y skills add aiskillstore/marketplace --skill structured-loggingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
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.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.3 KB, as published. Nobody here has run it
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 포맷 사용
- 적절한 로그 레벨 사용
- 비즈니스 이벤트 로깅
- 에러에 스택 트레이스 포함
- 성능 측정 로깅
운영
- 로그 집계 시스템 연동
- 로그 기반 알림 설정
- 로그 보관 정책 수립