예제

Sophonz Node.js SDK로 Express, Koa, Fastify, NestJS 서버를 계측하는 완전한 실행 예제 — 초기화 순서, 요청 보강기, 로깅, 에러 핸들러.

지원하는 네 가지 프레임워크의 엔드투엔드 엔트리포인트로, 공식 sophonz-express-server 레퍼런스 앱을 본떴습니다. 가장 중요한 규칙은 순서입니다. 계측 대상 모듈(express, koa, fastify, @nestjs/core, winston 등)이 로드되기 전에 SDK를 초기화하세요. 모든 예제는 파일 최상단에서 init()을 호출하고 앱을 동적 import()로 지연 로딩해 이를 보장합니다. 덕분에 그냥 node main.js만으로 충분하며 -r/프리로드 파일이 필요 없습니다.

CAUTION — 가장 먼저 초기화

계측 대상 라이브러리가 init() 실행 전에 import되면 자동 계측이 일부를 놓칠 수 있습니다. init()을 프로세스에서 가장 먼저 실행하고 나머지는 그 뒤에 로드하세요(bootstrap() 안의 동적 import()가 이를 보장하는 가장 간단한 방법입니다).

전체 서버 엔트리포인트

프레임워크를 선택하세요. 각 탭은 node main.js(또는 bun main.js)로 바로 실행할 수 있는 완전한 main.ts이며 프리로드 플래그가 필요 없습니다.

// main.ts
import { enableDebugPayloadExporters, init } from '@sophonz/node-sdk';
 
// 1. 계측 대상 모듈이 로드되기 전에 SDK를 초기화합니다.
process.env.OTEL_EXPORTER_OTLP_ENDPOINT =
  process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'https://in-otel.sophonz.com';
process.env.OTEL_PROPAGATORS =
  process.env.OTEL_PROPAGATORS ?? 'tracecontext,baggage';
 
init({
  service: process.env.OTEL_SERVICE_NAME ?? 'sophonz-express-server',
  apiKey: process.env.SOPHONZ_API_KEY,
  consoleCapture: true,
  betaMode: true,
  advancedNetworkCapture: true,
  experimentalExceptionCapture: true,
});
 
if (process.env.SOPHONZ_DEBUG_PAYLOAD === 'true') {
  void enableDebugPayloadExporters();
}
 
// 2. SDK 초기화 이후에만 앱을 부팅합니다.
async function bootstrap(): Promise<void> {
  const { default: express } = await import('express');
  const { default: winston } = await import('winston');
  const {
    getWinstonTransport,
    setupExpressErrorHandler,
    sophonzExpressMiddleware,
  } = await import('@sophonz/node-sdk');
 
  const PORT = parseInt(process.env.PORT ?? '7788', 10);
 
  const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
      new winston.transports.Console(),
      getWinstonTransport('info', { detectResource: true }),
    ],
  });
 
  const app = express();
  app.use(express.json());
 
  // 모든 요청 스팬에 baggage, 클라이언트 IP, user-agent 추가.
  app.use(sophonzExpressMiddleware());
 
  app.get('/health', (_req, res) => {
    res.json({ status: 'ok', uptime: process.uptime() });
  });
 
  app.get('/api/users/:id', (req, res) => {
    logger.info('GET /api/users/:id', { id: req.params.id });
    res.json({ id: Number(req.params.id), name: 'Alice' });
  });
 
  app.get('/api/error', () => {
    throw new Error('Intentional error from /api/error');
  });
 
  // 상태 코드 >= 500인 오류를 기록. 라우트 뒤에 등록.
  setupExpressErrorHandler(app);
 
  // 선택: 직접 작성한 폴백 핸들러.
  app.use((err, _req, res, next) => {
    logger.error('Unhandled error', { message: err.message });
    if (res.headersSent) return next(err);
    res.status(500).json({ error: err.message });
  });
 
  app.listen(PORT, () => {
    logger.info(`listening on http://localhost:${PORT}`);
  });
}
 
void bootstrap();

NOTE — 서비스 간 baggage 전파

OTEL_PROPAGATORS=tracecontext,baggage를 설정하면, 들어오는 baggage 항목이 보강기에 의해 활성 스팬과 트레이스 컨텍스트에 baggage. 접두사로 복사됩니다. 호출자별 메타데이터(예: 브라우저 SDK가 설정한 값)가 백엔드 스팬으로 흘러드는 방식입니다. service.name은 시작 시 고정되는 리소스 속성이라 각 서비스가 자신의 값을 유지하며, 트레이스는 서비스 이름 공유가 아니라 W3C traceparent 헤더로 연결됩니다.

보강기 옵션(미들웨어·플러그인·인터셉터)은 미들웨어, 모든 init() 설정은 설정을 참고하세요.