npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rlawncks125/otel-sdk

v0.3.1

Published

이 저장소의 Collector 규약에 맞춘 단일 패키지입니다. 기본 SDK와 Elysia/Nuxt adapter를 subpath export로 제공합니다.

Readme

Shared OpenTelemetry SDK

이 저장소의 Collector 규약에 맞춘 단일 패키지입니다. 기본 SDK와 Elysia/Nuxt adapter를 subpath export로 제공합니다.

SDK를 불러오는 시점, preload/bootstrap 선택, Elysia 요청 계측과 종료 순서는 SDK application lifecycle에 정리되어 있습니다.

책임 범위

  • startTelemetry(): NodeSDK, OTLP/HTTP protobuf exporter, 자동 계측, 종료 hook
  • createApplicationTelemetry(): 공통 request lifecycle, HTTP server span, 업무 span/metric, 구조화 application log helper
  • 현재 활성 span 조회와 attribute, event, exception 추가 helper
  • OtelDrizzleLogger: 현재 trace에 Drizzle SQL query 기록
  • OtelTypeOrmLogger: 현재 trace에 TypeORM SQL query 기록
  • @rlawncks125/otel-sdk/elysia: Elysia server span과 request lifecycle plugin
  • @rlawncks125/otel-sdk/nuxt: Nitro plugin을 등록하는 Nuxt module
  • @rlawncks125/otel-sdk/nuxt/runtime: traced handler와 Nitro telemetry singleton
  • config 검증과 resource 규약은 Node와 Bun에서 동일하게 적용

기본 export는 프레임워크 코드를 불러오지 않습니다. Elysia/Nuxt를 사용하는 앱만 해당 subpath와 optional peer dependency를 사용합니다. DB별 instrumentation은 앱이 선택합니다.

Subpath 사용

Elysia:

import { createApplicationTelemetry } from '@rlawncks125/otel-sdk'
import { elysiaTelemetry } from '@rlawncks125/otel-sdk/elysia'

Nuxt module:

export default defineNuxtConfig({
  modules: [['@rlawncks125/otel-sdk/nuxt', { apiPrefix: '/api' }]],
})

Nuxt server runtime:

import {
  defineTracedEventHandler,
  useNitroTelemetry,
} from '@rlawncks125/otel-sdk/nuxt/runtime'

애플리케이션 singleton

startTelemetry()와 createApplicationTelemetry()의 lifecycle은 다릅니다.

  • instrumentation.ts: 프로세스 시작 시 startTelemetry() 한 번
  • telemetry/application-telemetry.ts: 앱에서 사용할 handle 한 번 생성
  • controller/middleware: HTTP 입력, route template, 완료 log 처리
  • service: recordOperation()으로 업무 구간 계측
// src/telemetry/application-telemetry.ts
import { createApplicationTelemetry } from '@rlawncks125/otel-sdk'

export const applicationTelemetry = createApplicationTelemetry({
  scopeName: process.env.OTEL_SERVICE_NAME ?? 'orders-api',
  scopeVersion: process.env.OTEL_SERVICE_VERSION ?? '0.1.0'
})
// src/services/order.service.ts
import { applicationTelemetry } from '../telemetry/application-telemetry'

export const createOrder = () =>
  applicationTelemetry.recordOperation(
    'order.create',
    { entityType: 'order', route: '/orders' },
    () => repository.create()
  )

ES module은 같은 경로의 모듈을 캐시하므로 controller와 service가 import해도 createApplicationTelemetry()는 프로세스당 한 번만 평가됩니다. 요청 handler 안에서 생성하거나 각 레이어에서 별도로 생성하지 않습니다. 테스트에서 모듈 cache를 격리하는 runner는 테스트 bootstrap에서 한 번 생성해 의존성으로 전달할 수 있습니다.

공통 request lifecycle

각 프레임워크의 hook 이름은 달라도 요청 처리는 시작, 응답 시작, 에러, 응답 완료 단계로 연결합니다.

applicationTelemetry.beginRequestTelemetry(request, {
  method: request.method,
  route: '/orders/:id'
})

applicationTelemetry.recordRequestError(request, error, {
  route: '/orders/:id',
  statusCode: 500
})

applicationTelemetry.beginResponseTelemetry(request, {
  route: '/orders/:id',
  statusCode: response.statusCode
})

applicationTelemetry.finishRequestTelemetry(request, {
  route: '/orders/:id',
  statusCode: response.statusCode
})

request는 Elysia Request, Hono Context, NestJS/Express Request, Nuxt H3Event처럼 요청마다 생성되는 안정적인 객체입니다. SDK singleton은 이를 WeakMap<object, state>의 key로 사용하며 객체 자체를 수정하지 않습니다. 중복 begin은 최초 시작 시각을 유지하면서 method/route만 갱신합니다. 각 단계는 활성 request span에 http.request.started, http.request.error, http.response.started, http.response.completed event를 기록합니다. finishRequestTelemetry()는 최종 status, duration, error type으로 trace 연계 HTTP 완료 log를 한 번 생성한 뒤 상태를 삭제하며 중복 finish는 아무 작업도 하지 않습니다.

프레임워크 plugin/자동 계측이 안정적인 server span을 제공하면 공통 lifecycle event를 그 span에 기록합니다. Nuxt처럼 incoming server span을 직접 생성하는 adapter는 수신 traceparent를 부모 context로 추출하고 실제 응답 전송 뒤 span을 끝냅니다. 같은 요청에 server span을 중복 생성하지 않습니다.

로컬 패키지 연결

포함된 Elysia 소비 예제는 SDK를 독립 workspace package로 참조합니다.

{
  "dependencies": {
    "@rlawncks125/otel-sdk": "workspace:*"
  }
}

npm registry에 배포한 뒤에는 workspace:*를 ^1.0.0 같은 배포 버전으로 변경합니다.

SDK를 먼저 build합니다.

cd otel-sdk
bun install
bun run build

preload

Node와 Bun은 같은 bootstrap 코드를 사용합니다.

import 'dotenv/config'
import { startTelemetry } from '@rlawncks125/otel-sdk'

startTelemetry({
  serviceName: 'orders-api',
  serviceVersion: '0.1.0'
})

SDK는 globalThis.Bun 존재 여부로 runtime 이름만 자동 판별합니다. exporter, resource, sampling, metric/log 설정은 완전히 동일합니다.

Node ESM 실행:

node --import @rlawncks125/otel-sdk/register \
  --import ./instrumentation.mjs \
  ./server.mjs

ESM 앱은 register preload가 OTel loader hook을 등록해야 import로 불러온 HTTP/DB 모듈을 자동 계측할 수 있습니다. CommonJS 앱은 register preload 없이 SDK bootstrap만 사용합니다.

Bun 실행:

preload = ["./src/instrumentation.ts"]

환경 변수의 OTEL_SERVICE_NAME은 코드 기본값보다 우선합니다. SDK는 모든 신호의 resource에 juchan-kind=server를 기본으로 추가하며, 필요하면 OTEL_RESOURCE_ATTRIBUTES의 juchan-kind 또는 startTelemetry({ juchanKind: '...' })로 변경할 수 있습니다. SDK는 http/protobuf만 허용하며 host 기본 endpoint는 http://localhost:4318입니다.

업무 계측

import { createApplicationTelemetry } from '@rlawncks125/otel-sdk'

const telemetry = createApplicationTelemetry({
  scopeName: 'orders-api',
  scopeVersion: '0.1.0'
})

const order = await telemetry.recordOperation(
  'order.create',
  {
    entityType: 'order',
    route: '/orders',
    spanAttributes: {
      'app.item.count': items.length
    }
  },
  async () => createOrder(items)
)

recordOperation은 다음을 함께 생성합니다.

  • span: order.create
  • histogram: app.operation.duration (s)
  • counter: app.operation.completed
  • 실패 counter: app.error

operation과 entity type은 제한된 상수 집합이어야 합니다. ID, URL, 검색어, 예외 메시지를 metric attribute에 넣지 않습니다. route에는 실제 URL 대신 /orders/:id 같은 제한된 route template만 사용합니다.

현재 span에 데이터 추가

서비스 로직 중간에서 현재 활성 span을 직접 가져오거나 공통 helper로 데이터를 추가할 수 있습니다.

import {
  addCurrentSpanEvent,
  getCurrentSpan,
  recordCurrentSpanException,
  setCurrentSpanAttribute,
  setCurrentSpanAttributes,
} from '@rlawncks125/otel-sdk'

const result = await telemetry.recordOperation(
  'order.create',
  { entityType: 'order', route: '/orders' },
  async () => {
    getCurrentSpan()?.setAttribute('app.item.count', items.length)
    setCurrentSpanAttribute('app.order.validated', true)
    setCurrentSpanAttributes({ 'app.payment.method': 'card' })
    addCurrentSpanEvent('order.validation.completed')

    try {
      return await repository.create(items)
    } catch (error) {
      // 처리한 오류를 현재 span의 exception event로 남깁니다.
      recordCurrentSpanException(error)
      return null
    }
  }
)
  • getCurrentSpan()은 활성 context의 Span | undefined를 반환합니다.
  • 나머지 helper는 활성 span을 수정하면 true, 활성 span이 없으면 아무 작업 없이 false를 반환합니다.
  • recordOperation() callback 안에서는 해당 업무 span이 현재 span입니다. callback 밖에서는 프레임워크 계측이 context를 유지한 경우 server span일 수 있습니다.
  • SDK 또는 프레임워크가 소유한 현재 span을 애플리케이션에서 직접 end()하지 않습니다.
  • 다시 던질 오류는 recordOperation()이 exception과 error status를 처리하므로 중복 기록하지 않습니다.
  • recordCurrentSpanException()은 기본적으로 exception event만 추가합니다. 외부 helper가 status를 관리하지 않는 span을 실패 처리할 때만 { markAsError: true }를 사용합니다.
  • 일반 application attribute와 event에는 ID, 실제 URL, query, 토큰, 개인정보를 넣지 않습니다. SQL query를 의도적으로 수집하는 경우에는 아래 Drizzle logger의 보안 주의를 따릅니다.

Drizzle query 추적

Drizzle 생성 시 OtelDrizzleLogger를 logger로 전달하면 현재 활성 span에 실행한 SQL이 기록됩니다.

import { OtelDrizzleLogger } from '@rlawncks125/otel-sdk'
import { drizzle } from 'drizzle-orm/node-postgres'

export const db = drizzle(client, {
  logger: new OtelDrizzleLogger(),
})

각 호출은 현재 span의 db.query.text attribute를 갱신하고 db.query.count를 증가시킵니다. 같은 span에서 실행한 모든 query는 db.query event로 각각 남기 때문에, trace ID로 요청 trace를 열면 실행 순서대로 확인할 수 있습니다. 활성 span이 없는 곳에서 실행된 query는 기록하지 않습니다.

Query parameter는 토큰이나 개인정보를 포함할 수 있어 기본적으로 수집하지 않습니다. 안전한 값만 사용한다는 것이 보장된 환경에서는 명시적으로 활성화할 수 있습니다.

export const db = drizzle(client, {
  logger: new OtelDrizzleLogger({ captureParameters: true }),
})

이 경우 parameter 배열은 db.query.parameters JSON 문자열 attribute로 query event와 현재 span에 기록됩니다. raw SQL에 값을 직접 삽입하면 captureParameters 설정과 관계없이 db.query.text에 포함되므로 민감한 값을 SQL 문자열에 직접 넣지 않습니다.

NestJS TypeORM query 추적

NestJS의 TypeOrmModule 설정에서 OtelTypeOrmLogger를 TypeORM logger로 전달합니다.

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { OtelTypeOrmLogger } from '@rlawncks125/otel-sdk'

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      autoLoadEntities: true,
      logger: new OtelTypeOrmLogger(),
    }),
  ],
})
export class AppModule {}

일반 query는 Drizzle logger와 동일하게 현재 span의 db.query.text, db.query.count attribute와 db.query event로 기록됩니다. 실패 query는 db.query.error, maxQueryExecutionTime을 넘긴 query는 db.query.slow event도 추가합니다. schema build, migration, TypeORM 일반 log는 trace에 추가하지 않으며 활성 span이 없을 때는 아무것도 기록하지 않습니다.

Parameter 수집은 기본적으로 꺼져 있습니다. 안전한 값만 사용한다는 것이 보장될 때만 new OtelTypeOrmLogger({ captureParameters: true })로 활성화합니다. raw SQL에 값을 직접 삽입하면 이 설정과 관계없이 db.query.text에 포함됩니다.

런타임이나 프레임워크가 안정적인 HTTP server 자동 계측을 제공하지 않으면 handler를 recordHttpServerRequest()로 감쌉니다. 이 helper는 SPAN_KIND_SERVER span과 trace 연계 HTTP 완료 로그를 함께 생성합니다.

const result = await telemetry.recordHttpServerRequest(
  request,
  {
    method: 'GET',
    route: '/orders/:id'
  },
  () => telemetry.recordOperation(
    'order.read',
    {
      entityType: 'order',
      route: '/orders/:id'
    },
    () => findOrder()
  )
)

프레임워크 plugin이나 자동 계측이 이미 올바른 server span을 만든다면 이 helper를 중복 적용하지 않습니다. 실패 응답을 예외로 표현하지 않는 handler는 successStatusCode를 명시합니다. 첫 번째 인자는 현재 요청을 식별하는 객체이며 내부 공통 lifecycle의 key로 사용됩니다.

HTTP 완료 로그는 현재 Grafana Loki panel과 호환되는 JSON body와 semantic attributes를 함께 생성합니다.

telemetry.emitHttpLog({
  method: 'GET',
  route: '/orders/:id',
  statusCode: 200,
  durationMs: 18.2
})

에러 메시지는 외부 입력이나 개인정보를 그대로 전달하지 않고 안전한 고정 문구로 정리합니다.