| 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;
level: string;
message: string;
requestId?: string;
userId?: string;
service?: string;
environment?: string;
error?: {
name: string;
message: string;
stack?: string;
};
duration?: number;
metadata?: Record<string, unknown>;
}
Node.js ๊ตฌํ
Pino (๊ถ์ฅ - ๊ณ ์ฑ๋ฅ)
npm install pino pino-pretty
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,
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
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 ์ ํ
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 (๊ถ์ฅ)
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
import pino from 'pino';
import { getContext } from './context';
const baseLogger = pino({ });
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);
},
};
๋ก๊น
ํจํด
API ์์ฒญ ๋ก๊น
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,
}, );
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;
}
}
๊ธ์ง ํจํด
logger.info({ password, creditCard, ssn }, 'User data');
for (const item of items) {
logger.debug({ item }, 'Processing item');
}
logger.info(`User ${userId} logged in at ${timestamp}`);
logger.info({ userId, timestamp }, 'User logged in');
๋ฏผ๊ฐ ์ ๋ณด ์ ๊ฑฐ
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;
}
const logger = pino({
redact: ['password', 'creditCard', '*.token', 'headers.authorization'],
});
Log Aggregation ์ฐ๋
ELK Stack (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
import tracer from 'dd-trace';
tracer.init({
service: 'my-app',
env: process.env.NODE_ENV,
});
logger.info({
dd: {
trace_id: tracer.scope().active()?.context().toTraceId(),
span_id: tracer.scope().active()?.context().toSpanId(),
},
}, 'Event with trace');
Checklist
์ค์
๋ก๊น
ํ์ค
์ด์
References