Pino로 Next.js 고성능 JSON 로깅 마스터하기




지난 Winston 포스트에서 풍부한 기능의 엔터프라이즈급 로깅을 다뤘다면, 이번에는 성능에 특화된 Pino를 알아보겠습니다.

“속도가 생명인” 현대 웹 애플리케이션에서 로깅 오버헤드는 무시할 수 없는 요소입니다. Pino는 다른 로깅 라이브러리 대비 5-10배 빠른 성능을 자랑하며, JSON 구조화 로깅의 표준을 제시합니다.

Pino란?

Pino는 이탈리아어로 “소나무”를 의미하며, “매우 낮은 오버헤드의 Node.js 로거”라는 철학을 가지고 있습니다. 2016년 Matteo Collina에 의해 개발되었으며, Fastify 웹 프레임워크의 기본 로거로도 사용됩니다.

Pino의 핵심 원칙

JSON First: 모든 로그를 JSON 형태로 출력하여 구조화된 로깅을 기본으로 합니다.

최소 오버헤드: 로깅 자체가 애플리케이션 성능에 미치는 영향을 최소화합니다.

스트림 기반: Node.js 스트림을 활용한 효율적인 로그 처리를 제공합니다.

분리된 처리: 로그 생성과 포맷팅을 분리하여 메인 프로세스의 부담을 줄입니다.

성능 벤치마크

실제 성능 차이가 얼마나 날까요? 공식 벤치마크 결과를 보면:

✓ pino v8.14.1 ✓
✓ winston v3.10.0 ✓
✓ bunyan v1.8.15 ✓
✓ console v16.20.1 ✓

│ logger               │ ops/sec │ ±%     │
├─────────────────────┼─────────┼────────┤
│ pino                 │ 15,413  │ ±0.84% │
│ winston              │ 2,847   │ ±1.45% │  
│ bunyan               │ 3,234   │ ±2.12% │
│ console              │ 4,526   │ ±1.98% │

Pino가 Winston보다 5배 이상 빠른 성능을 보여줍니다!

설치 및 기본 설정

npm install pino
# 예쁜 출력을 위한 pino-pretty도 함께 설치
npm install pino-pretty

기본 로거 설정

// lib/logger.js
import pino from 'pino';

const logger = pino({
  name: 'nextjs-app',
  level: process.env.LOG_LEVEL || 'info',
  
  // 개발 환경에서는 pretty 출력 사용
  transport: process.env.NODE_ENV === 'development' ? {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'yyyy-mm-dd HH:MM:ss',
      ignore: 'pid,hostname'
    }
  } : undefined,
  
  // 프로덕션에서는 JSON 출력
  formatters: {
    level: (label) => {
      return { level: label };
    },
    bindings: (bindings) => {
      return {
        pid: bindings.pid,
        hostname: bindings.hostname,
        node_version: process.version
      };
    }
  }
});

export default logger;

로그 출력 형태와 사용법

기본 사용법

import logger from '../lib/logger';

// 기본 로깅
logger.info('서버가 시작되었습니다');
logger.warn('메모리 사용량이 높습니다');
logger.error('데이터베이스 연결 실패');

// 구조화된 데이터와 함께 로깅
logger.info({
  userId: 123,
  action: 'login',
  ip: '192.168.1.1',
  userAgent: 'Mozilla/5.0...'
}, '사용자 로그인');

// 에러 객체 로깅
try {
  // 일부 코드
} catch (error) {
  logger.error({ err: error }, '처리 중 오류 발생');
}

개발 환경 출력 (pino-pretty)

[10:30:45.123] INFO (12345): 서버가 시작되었습니다
[10:30:46.456] WARN (12345): 메모리 사용량이 높습니다  
[10:30:47.789] ERROR (12345): 데이터베이스 연결 실패

[10:30:48.123] INFO (12345): 사용자 로그인
    userId: 123
    action: "login"
    ip: "192.168.1.1"
    userAgent: "Mozilla/5.0..."

프로덕션 환경 출력 (Raw JSON)

{"level":"info","time":1642248645123,"pid":12345,"hostname":"server-01","name":"nextjs-app","msg":"서버가 시작되었습니다"}
{"level":"warn","time":1642248646456,"pid":12345,"hostname":"server-01","name":"nextjs-app","msg":"메모리 사용량이 높습니다"}
{"level":"error","time":1642248647789,"pid":12345,"hostname":"server-01","name":"nextjs-app","err":{"type":"Error","message":"Connection failed","stack":"Error: Connection failed\n    at..."},"msg":"처리 중 오류 발생"}

Next.js 특화 설정

환경별 설정 분리

// lib/logger.js
import pino from 'pino';

const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';

const pinoConfig = {
  name: 'nextjs-app',
  level: process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info'),
  
  // 개발 환경 설정
  ...(isDevelopment && {
    transport: {
      target: 'pino-pretty',
      options: {
        colorize: true,
        translateTime: 'yyyy-mm-dd HH:MM:ss',
        ignore: 'pid,hostname',
        singleLine: false
      }
    }
  }),
  
  // 프로덕션 환경 설정
  ...(isProduction && {
    redact: {
      paths: ['password', 'token', 'apiKey', 'creditCard'],
      censor: '[REDACTED]'
    },
    serializers: {
      err: pino.stdSerializers.err,
      req: pino.stdSerializers.req,
      res: pino.stdSerializers.res
    }
  })
};

const logger = pino(pinoConfig);

export default logger;

로그 파일 출력 설정

// lib/logger.js 수정
import pino from 'pino';
import fs from 'fs';

// 로그 디렉토리 생성
if (!fs.existsSync('logs')) {
  fs.mkdirSync('logs');
}

const streams = [
  // 모든 로그를 파일에 저장
  {
    level: 'info',
    stream: pino.destination({
      dest: 'logs/app.log',
      sync: false,
      mkdir: true
    })
  },
  // 에러 로그만 별도 파일에 저장
  {
    level: 'error', 
    stream: pino.destination({
      dest: 'logs/error.log',
      sync: false,
      mkdir: true
    })
  }
];

// 개발 환경에서는 콘솔 출력도 추가
if (process.env.NODE_ENV === 'development') {
  streams.push({
    level: 'debug',
    stream: pino.transport({
      target: 'pino-pretty',
      options: {
        colorize: true,
        translateTime: 'yyyy-mm-dd HH:MM:ss'
      }
    })
  });
}

const logger = pino({
  name: 'nextjs-app',
  level: 'debug'
}, pino.multistream(streams));

export default logger;

실제 프로젝트 적용 예시

1. API 라우트 로깅

// pages/api/users/[id].js
import logger from '../../../lib/logger';

export default async function handler(req, res) {
  const startTime = Date.now();
  const { id } = req.query;
  const { method } = req;
  
  // 요청 시작 로깅
  const requestLogger = logger.child({
    requestId: generateRequestId(), // UUID 등 고유 ID
    method,
    url: req.url,
    userId: id,
    userAgent: req.headers['user-agent'],
    ip: getClientIp(req)
  });
  
  requestLogger.info('API 요청 시작');
  
  try {
    let result;
    const responseTime = Date.now() - startTime;
    
    switch (method) {
      case 'GET':
        result = await getUserById(id);
        
        requestLogger.info({
          responseTime,
          resultCount: result ? 1 : 0
        }, '사용자 조회 완료');
        
        if (!result) {
          requestLogger.warn('존재하지 않는 사용자');
          return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
        }
        
        return res.status(200).json(result);
        
      case 'PUT':
        result = await updateUser(id, req.body);
        
        requestLogger.info({
          responseTime,
          updatedFields: Object.keys(req.body)
        }, '사용자 정보 업데이트 완료');
        
        return res.status(200).json(result);
        
      default:
        requestLogger.warn({ allowedMethods: ['GET', 'PUT'] }, '지원하지 않는 HTTP 메서드');
        return res.status(405).json({ error: '허용되지 않는 메서드' });
    }
    
  } catch (error) {
    const responseTime = Date.now() - startTime;
    
    requestLogger.error({
      err: error,
      responseTime,
      requestBody: req.body
    }, 'API 요청 처리 중 오류');
    
    return res.status(500).json({ error: '서버 내부 오류' });
  }
}

function generateRequestId() {
  return Math.random().toString(36).substr(2, 9);
}

function getClientIp(req) {
  return req.headers['x-forwarded-for'] || 
         req.connection.remoteAddress || 
         req.socket.remoteAddress ||
         (req.connection.socket ? req.connection.socket.remoteAddress : null);
}

2. 성능 모니터링 로깅

// lib/performance-logger.js
import logger from './logger';

export class PerformanceLogger {
  constructor(operation) {
    this.operation = operation;
    this.startTime = process.hrtime.bigint();
    this.logger = logger.child({ operation });
  }
  
  addContext(context) {
    this.context = { ...this.context, ...context };
    return this;
  }
  
  end(message = '작업 완료') {
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - this.startTime) / 1000000; // nanoseconds to milliseconds
    
    this.logger.info({
      duration: `${duration.toFixed(2)}ms`,
      ...this.context
    }, message);
    
    return duration;
  }
  
  checkpoint(checkpointName) {
    const currentTime = process.hrtime.bigint();
    const duration = Number(currentTime - this.startTime) / 1000000;
    
    this.logger.debug({
      checkpoint: checkpointName,
      duration: `${duration.toFixed(2)}ms`,
      ...this.context
    }, `체크포인트: ${checkpointName}`);
  }
}

// 사용 예시
export default async function handler(req, res) {
  const perf = new PerformanceLogger('user-creation')
    .addContext({ userId: req.body.email });
  
  perf.checkpoint('validation-start');
  await validateUserData(req.body);
  
  perf.checkpoint('database-write-start');
  const user = await createUser(req.body);
  
  perf.checkpoint('email-send-start');
  await sendWelcomeEmail(user.email);
  
  const totalTime = perf.end('사용자 생성 완료');
  
  res.status(201).json({ user, responseTime: totalTime });
}

3. 구조화된 에러 로깅

// lib/error-logger.js
import logger from './logger';

export class ErrorLogger {
  static logApiError(error, context = {}) {
    const errorLogger = logger.child({
      errorType: 'api_error',
      ...context
    });
    
    errorLogger.error({
      err: error,
      errorCode: error.code,
      statusCode: error.statusCode || 500,
      stack: error.stack
    }, `API 오류: ${error.message}`);
  }
  
  static logDatabaseError(error, query, params = {}) {
    logger.error({
      err: error,
      errorType: 'database_error',
      query: query?.text || query,
      queryParams: params,
      database: process.env.DATABASE_URL?.split('@')[1] // URL에서 호스트 부분만
    }, `데이터베이스 오류: ${error.message}`);
  }
  
  static logValidationError(errors, context = {}) {
    logger.warn({
      errorType: 'validation_error',
      validationErrors: errors,
      ...context
    }, '유효성 검사 실패');
  }
  
  static logExternalApiError(error, apiName, endpoint) {
    logger.error({
      err: error,
      errorType: 'external_api_error',
      externalService: apiName,
      endpoint,
      responseStatus: error.response?.status,
      responseData: error.response?.data
    }, `외부 API 호출 실패: ${apiName}`);
  }
}

// 사용 예시
try {
  const user = await User.findById(userId);
} catch (error) {
  ErrorLogger.logDatabaseError(error, 'SELECT * FROM users WHERE id = $1', [userId]);
  throw error;
}

고급 기능 활용

1. 로그 스트리밍과 로테이션

npm install pino-roll
// lib/logger.js 수정
import pino from 'pino';
import roll from 'pino-roll';

const logRotation = roll({
  file: 'logs/app.log',
  frequency: 'daily',    // 매일 로테이션 
  size: '100m',          // 또는 100MB마다
  limit: {
    count: 10            // 최대 10개 파일 유지
  }
});

const logger = pino({
  name: 'nextjs-app'
}, logRotation);

export default logger;

2. 실시간 로그 모니터링

npm install pino-elasticsearch
// lib/logger.js에 추가
import pino from 'pino';

const streams = [];

// 기본 파일 스트림
streams.push({
  level: 'info',
  stream: pino.destination('logs/app.log')
});

// 프로덕션에서 Elasticsearch로 스트리밍
if (process.env.NODE_ENV === 'production' && process.env.ELASTICSEARCH_URL) {
  streams.push({
    level: 'info',
    stream: pino.transport({
      target: 'pino-elasticsearch',
      options: {
        index: 'nextjs-logs',
        node: process.env.ELASTICSEARCH_URL,
        auth: {
          username: process.env.ELASTICSEARCH_USER,
          password: process.env.ELASTICSEARCH_PASSWORD
        }
      }
    })
  });
}

const logger = pino({
  name: 'nextjs-app'
}, pino.multistream(streams));

3. 커스텀 직렬화

// lib/logger.js 수정
const logger = pino({
  name: 'nextjs-app',
  serializers: {
    // HTTP 요청 직렬화
    req: (req) => ({
      method: req.method,
      url: req.url,
      headers: {
        'user-agent': req.headers['user-agent'],
        'content-type': req.headers['content-type']
      },
      remoteAddress: req.connection?.remoteAddress,
      remotePort: req.connection?.remotePort
    }),
    
    // HTTP 응답 직렬화  
    res: (res) => ({
      statusCode: res.statusCode,
      headers: res.getHeaders ? res.getHeaders() : res.headers
    }),
    
    // 사용자 정보 직렬화 (민감한 정보 제외)
    user: (user) => ({
      id: user.id,
      email: user.email,
      role: user.role,
      createdAt: user.createdAt
      // password, token 등은 제외
    })
  }
});

성능 최적화 전략

1. 비동기 로깅 활용

// lib/logger.js
import pino from 'pino';

const logger = pino({
  name: 'nextjs-app'
}, pino.destination({
  dest: 'logs/app.log',
  sync: false,        // 비동기 쓰기
  minLength: 4096,    // 버퍼 크기 설정
  mkdir: true
}));

// 프로세스 종료 시 로그 플러시
process.on('exit', () => {
  logger.flush();
});

process.on('SIGINT', () => {
  logger.flush();
  process.exit(0);
});

2. 조건부 로깅과 레벨 체크

// 비용이 큰 작업은 레벨 체크 후 실행
if (logger.isLevelEnabled('debug')) {
  const expensiveDebugData = computeExpensiveData();
  logger.debug({ debugData: expensiveDebugData }, '디버그 정보');
}

// 또는 함수형 접근
logger.debug(() => ({
  expensiveData: computeExpensiveData()
}), '디버그 정보');

3. 메모리 사용량 모니터링

// lib/system-monitor.js
import logger from './logger';

export function startSystemMonitoring() {
  setInterval(() => {
    const memUsage = process.memoryUsage();
    const cpuUsage = process.cpuUsage();
    
    logger.info({
      system: {
        memory: {
          rss: Math.round(memUsage.rss / 1024 / 1024),
          heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
          heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
          external: Math.round(memUsage.external / 1024 / 1024)
        },
        cpu: {
          user: cpuUsage.user,
          system: cpuUsage.system
        },
        uptime: Math.round(process.uptime())
      }
    }, '시스템 상태');
  }, 60000); // 1분마다
}

ELK 스택과의 통합

Elasticsearch, Logstash, Kibana와 함께 사용하는 완전한 로그 분석 시스템:

// docker-compose.yml과 함께 사용
const logger = pino({
  name: 'nextjs-app',
  // ELK 스택을 위한 추가 필드
  base: {
    service: 'nextjs-app',
    version: process.env.APP_VERSION || '1.0.0',
    environment: process.env.NODE_ENV || 'development'
  }
}, pino.transport({
  target: 'pino-elasticsearch',
  options: {
    index: 'nextjs-logs-%{+YYYY.MM.DD}',
    node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200'
  }
}));

// 구조화된 로그로 Kibana 대시보드 생성 가능
logger.info({
  event: 'user_action',
  action: 'purchase',
  userId: 123,
  productId: 456,
  amount: 99.99,
  currency: 'USD',
  paymentMethod: 'credit_card'
}, '구매 완료');

Pino vs Winston 실전 비교

성능 테스트

// benchmark.js
import winston from 'winston';
import pino from 'pino';

const winstonLogger = winston.createLogger({
  transports: [new winston.transports.File({ filename: 'winston.log' })]
});

const pinoLogger = pino(pino.destination('pino.log'));

console.time('Winston 10000 logs');
for (let i = 0; i < 10000; i++) {
  winstonLogger.info('Test message', { iteration: i, data: { foo: 'bar' } });
}
console.timeEnd('Winston 10000 logs');

console.time('Pino 10000 logs');
for (let i = 0; i < 10000; i++) {
  pinoLogger.info({ iteration: i, data: { foo: 'bar' } }, 'Test message');
}
console.timeEnd('Pino 10000 logs');

// 결과:
// Winston 10000 logs: 1.2초
// Pino 10000 logs: 0.2초 (6배 빠름!)

기능 비교표

기능WinstonPino
성능보통최고
JSON 로깅지원기본
커스텀 포맷매우 유연제한적
Transport다양함스트림 기반
학습 곡선가파름완만함
메모리 사용량높음낮음

실전 팁과 베스트 프랙티스

1. 구조화된 로깅 패턴

// 일관된 로그 구조 사용
const logContext = {
  userId: req.user?.id,
  sessionId: req.sessionID,
  requestId: req.id,
  ip: getClientIp(req)
};

// 모든 로그에 공통 컨텍스트 적용
const requestLogger = logger.child(logContext);
requestLogger.info('API 호출 시작');
requestLogger.warn('캐시 미스 발생');
requestLogger.error('처리 실패');

2. 로그 레벨 전략

// 환경별 로그 레벨 설정
const LOG_LEVELS = {
  development: 'debug',
  test: 'warn', 
  staging: 'info',
  production: 'warn'
};

const logger = pino({
  level: LOG_LEVELS[process.env.NODE_ENV] || 'info'
});

// 상황별 로그 레벨 가이드
logger.debug('개발 중 상세 정보');           // 개발환경에서만
logger.info('일반적인 정보성 메시지');        // 모든 환경
logger.warn('주의가 필요한 상황');           // 경고 상황
logger.error('오류 발생, 즉시 확인 필요');    // 심각한 오류
logger.fatal('치명적 오류, 서비스 중단');     // 서비스 중단급

3. 민감한 정보 보호

// lib/logger.js에 추가
const logger = pino({
  redact: {
    paths: [
      'password',
      'token', 
      'apiKey',
      'creditCard',
      'ssn',
      'req.headers.authorization',
      'req.headers.cookie'
    ],
    censor: '[REDACTED]'
  }
});

// 또는 커스텀 redactor 함수
const customRedact = (obj, path) => {
  if (path.includes('email')) {
    const email = obj[path];
    return email ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') : '[REDACTED]';
  }
  return '[REDACTED]';
};

4. 에러 추적 향상

// lib/error-context.js
export function enhanceError(error, context = {}) {
  return {
    ...error,
    message: error.message,
    stack: error.stack,
    name: error.name,
    code: error.code,
    ...context,
    timestamp: new Date().toISOString(),
    nodeVersion: process.version,
    platform: process.platform
  };
}

// 사용 예시
try {
  await riskyOperation();
} catch (error) {
  logger.error({
    err: enhanceError(error, {
      operation: 'user-registration',
      userId: newUser.id,
      step: 'email-verification'
    })
  }, '사용자 등록 중 오류');
}

프로덕션 배포 체크리스트

1. 환경 설정

# .env.production
LOG_LEVEL=warn
NODE_ENV=production
ELASTICSEARCH_URL=https://elasticsearch.company.com
ELASTICSEARCH_USER=app-logger
ELASTICSEARCH_PASSWORD=secure-password

2. 로그 수집 파이프라인

// lib/production-logger.js
import pino from 'pino';

const streams = [];

// 파일 로깅 (로컬 백업용)
streams.push({
  level: 'info',
  stream: pino.destination({
    dest: '/var/log/nextjs/app.log',
    sync: false,
    mkdir: true
  })
});

// 에러 로그 분리
streams.push({
  level: 'error',
  stream: pino.destination({
    dest: '/var/log/nextjs/error.log', 
    sync: false,
    mkdir: true
  })
});

// Elasticsearch 스트리밍
if (process.env.ELASTICSEARCH_URL) {
  streams.push({
    level: 'info',
    stream: pino.transport({
      target: 'pino-elasticsearch',
      options: {
        index: `nextjs-${process.env.NODE_ENV}-%{+YYYY.MM.DD}`,
        node: process.env.ELASTICSEARCH_URL,
        auth: {
          username: process.env.ELASTICSEARCH_USER,
          password: process.env.ELASTICSEARCH_PASSWORD
        },
        'bulk-size': 200,
        'bulk-bytes': 1000000
      }
    })
  });
}

export default pino({
  name: 'nextjs-production',
  level: 'info'
}, pino.multistream(streams));

3. 알림 시스템 연동

// lib/alert-logger.js
import pino from 'pino';

const logger = pino();

// 심각한 에러 발생 시 Slack 알림
logger.addHook('logMethod', function(inputArgs, method) {
  const [obj, msg] = inputArgs;
  
  // fatal이나 critical 로그는 즉시 알림
  if (method === 'fatal' || (obj && obj.severity === 'critical')) {
    sendSlackAlert({
      message: msg || obj.msg,
      level: method,
      details: obj,
      timestamp: new Date().toISOString(),
      server: process.env.SERVER_NAME || 'unknown'
    });
  }
  
  return inputArgs;
});

async function sendSlackAlert(alert) {
  try {
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `🚨 프로덕션 알림: ${alert.message}`,
        attachments: [{
          color: 'danger',
          fields: [
            { title: 'Level', value: alert.level, short: true },
            { title: 'Server', value: alert.server, short: true },
            { title: 'Time', value: alert.timestamp, short: false },
            { title: 'Details', value: JSON.stringify(alert.details, null, 2), short: false }
          ]
        }]
      })
    });
  } catch (error) {
    console.error('Slack 알림 전송 실패:', error);
  }
}

모니터링과 분석

1. 로그 메트릭 수집

// lib/metrics-logger.js
import pino from 'pino';

class MetricsLogger {
  constructor() {
    this.logger = pino({ name: 'metrics' });
    this.counters = new Map();
    this.timers = new Map();
  }
  
  // 카운터 메트릭
  increment(metric, tags = {}) {
    const key = `${metric}:${JSON.stringify(tags)}`;
    this.counters.set(key, (this.counters.get(key) || 0) + 1);
    
    this.logger.info({
      metric: 'counter',
      name: metric,
      value: this.counters.get(key),
      tags
    });
  }
  
  // 타이밍 메트릭
  startTimer(metric, tags = {}) {
    const key = `${metric}:${JSON.stringify(tags)}`;
    this.timers.set(key, process.hrtime.bigint());
  }
  
  endTimer(metric, tags = {}) {
    const key = `${metric}:${JSON.stringify(tags)}`;
    const startTime = this.timers.get(key);
    
    if (startTime) {
      const duration = Number(process.hrtime.bigint() - startTime) / 1000000;
      
      this.logger.info({
        metric: 'timer',
        name: metric,
        duration: Math.round(duration * 100) / 100, // 소수점 2자리
        tags
      });
      
      this.timers.delete(key);
      return duration;
    }
  }
  
  // 게이지 메트릭 (현재 값)
  gauge(metric, value, tags = {}) {
    this.logger.info({
      metric: 'gauge',
      name: metric,
      value,
      tags
    });
  }
}

export const metrics = new MetricsLogger();

// 사용 예시
export default function handler(req, res) {
  metrics.increment('api.requests', { 
    method: req.method, 
    endpoint: req.url 
  });
  
  metrics.startTimer('api.response_time', { 
    endpoint: req.url 
  });
  
  try {
    // API 로직 처리
    const result = await processRequest(req);
    
    metrics.increment('api.success', { 
      endpoint: req.url 
    });
    
    res.json(result);
  } catch (error) {
    metrics.increment('api.errors', { 
      endpoint: req.url,
      error: error.name 
    });
    
    throw error;
  } finally {
    metrics.endTimer('api.response_time', { 
      endpoint: req.url 
    });
  }
}

2. 로그 분석 쿼리 예시

Elasticsearch에 저장된 로그를 분석하는 유용한 쿼리들:

// 최근 1시간 에러 로그 통계
const errorStats = {
  "query": {
    "bool": {
      "must": [
        { "term": { "level": "error" } },
        { "range": { "@timestamp": { "gte": "now-1h" } } }
      ]
    }
  },
  "aggs": {
    "error_types": {
      "terms": { "field": "err.name" }
    },
    "error_timeline": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "5m"
      }
    }
  }
};

// 응답 시간 분석
const responseTimeAnalysis = {
  "query": {
    "bool": {
      "must": [
        { "exists": { "field": "responseTime" } },
        { "range": { "@timestamp": { "gte": "now-24h" } } }
      ]
    }
  },
  "aggs": {
    "response_time_stats": {
      "stats": { "field": "responseTime" }
    },
    "response_time_percentiles": {
      "percentiles": { 
        "field": "responseTime",
        "percents": [50, 90, 95, 99]
      }
    }
  }
};

트러블슈팅 가이드

1. 일반적인 문제들

JSON 파싱 에러

// 잘못된 사용법
logger.info('User data: ' + JSON.stringify(userData)); // ❌

// 올바른 사용법  
logger.info({ userData }, 'User data received'); // ✅

성능 이슈

// 비효율적인 로깅
logger.debug(`Processing ${items.length} items: ${JSON.stringify(items)}`); // ❌

// 효율적인 로깅
if (logger.isLevelEnabled('debug')) {
  logger.debug({ itemCount: items.length, items }, 'Processing items');
} // ✅

메모리 누수

// 문제가 되는 코드
const logger = pino();
setInterval(() => {
  logger.child({ timestamp: Date.now() }); // 새로운 child logger 계속 생성
}, 1000);

// 개선된 코드
const logger = pino();
const intervalLogger = logger.child({ component: 'interval' });
setInterval(() => {
  intervalLogger.info({ timestamp: Date.now() }, 'Periodic task');
}, 1000);

2. 디버깅 도구

// lib/debug-logger.js
import pino from 'pino';

// 디버그 모드에서 모든 로그 출력
const debugLogger = pino({
  level: 'trace',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'hostname,pid'
    }
  }
});

// 프로덕션에서는 로그 비활성화
export const debug = process.env.DEBUG === 'true' ? debugLogger : {
  trace: () => {},
  debug: () => {},
  info: () => {},
  warn: () => {},
  error: () => {},
  fatal: () => {}
};

Pino의 장단점 분석

✅ 장점

탁월한 성능: 다른 로거 대비 5-10배 빠른 속도 구조화된 로깅: JSON 기반으로 로그 분석 도구와 완벽 호환 낮은 메모리 사용량: 메모리 효율적인 설계 스트림 기반: Node.js 스트림 생태계와 자연스러운 통합 활발한 커뮤니티: 지속적인 업데이트와 플러그인 개발

❌ 단점

JSON 중심: 사람이 읽기 어려운 기본 출력 (pino-pretty 필요) 제한적인 포맷팅: Winston만큼 유연한 포맷팅 옵션 부족 학습 곡선: JSON 구조화 로깅에 익숙하지 않으면 적응 필요 Transport 제한: Winston의 Transport 시스템만큼 다양하지 않음

언제 Pino를 선택해야 할까?

✅ Pino 추천 상황

  • 고성능이 중요한 프로덕션 환경
  • 대용량 로그 처리
  • 마이크로서비스 아키텍처
  • 로그 분석 시스템 (ELK, Splunk 등) 사용
  • JSON 기반 모니터링 도구 연동

❌ Pino 비추천 상황

  • 복잡한 로그 포맷팅 필요
  • 레거시 시스템과의 호환성 중요
  • 팀이 구조화된 로깅에 익숙하지 않음
  • 단순한 텍스트 로그 선호

마무리

Pino는 “성능이 곧 경쟁력”인 현대 웹 애플리케이션에 최적화된 로깅 라이브러리입니다. JSON 구조화 로깅과 뛰어난 성능으로 대용량 트래픽을 처리하는 서비스에서 진가를 발휘합니다.

Winston의 풍부한 기능이 필요하지 않고, 빠르고 효율적인 로깅이 우선순위라면 Pino가 최선의 선택입니다. 특히 로그 분석 도구와 함께 사용할 때 그 가치가 극대화됩니다.

다음 포스트에서는 개발자 친화적인 인터페이스로 유명한 Consola를 다뤄보겠습니다. Python의 Loguru와 가장 유사한 “설정 없이 바로 사용” 경험을 제공하는 라이브러리입니다.




“Pino로 Next.js 고성능 JSON 로깅 마스터하기”에 대한 1개의 생각

댓글 남기기