opentelemetry
OpenTelemetry tracing and HTTP metrics plugin for the Elysia web framework
Repository Health
Technical Analysis
@elysiajs/opentelemetry wires distributed tracing and request metrics into an Elysia application with a single .use(opentelemetry()) call. It hooks into every stage of Elysia’s request lifecycle - parsing, transform, before/after handle, error, and response mapping - and turns each into a properly parented OpenTelemetry span under one root span per request, so a trace viewer shows exactly where time was spent inside the framework’s own pipeline, not just inside the route handler.
Beyond spans, it records an http.server.request.duration histogram with the standard OpenTelemetry HTTP semantic-convention buckets, and exposes helper functions (getTracer, startSpan, startActiveSpan, setAttributes, getCurrentSpan) so application code can add custom spans using the same tracer the plugin already configured. It boots a NodeSDK automatically only when no tracer provider is already registered, so teams that preload their own OpenTelemetry SDK configuration are not overridden.
Privacy is a first-class concern rather than an afterthought: query strings and URLs are redacted for a fixed set of sensitive keys (tokens, passwords, API keys, credentials) by default, credentials embedded in URLs are stripped, and request/response body or header capture is opt-in and disabled unless explicitly configured. This makes it usable in production HTTP services without manually auditing every span for leaked secrets.
What You Get
- Automatic per-request root span plus child spans for every Elysia lifecycle hook (onRequest, onParse, onTransform, onBeforeHandle, onHandle, onAfterHandle, onError, onAfterResponse, onMapResponse)
- An
http.server.request.durationhistogram metric recorded with OpenTelemetry HTTP semantic-convention bucket boundaries and attributes - Automatic bootstrap of a
NodeSDKwhen no tracer provider is registered yet, or transparent pass-through when the host app already preloads its own OpenTelemetry SDK - Manual instrumentation helpers -
getTracer,startSpan,startActiveSpan,setAttributes,getCurrentSpan- for adding custom spans and attributes from application code - Default redaction of sensitive query-string parameters (tokens, passwords, API keys, credentials) and stripped userinfo in captured URLs
- Opt-in capture of request/response bodies and specific request/response headers via
recordBodyandheadersToSpanAttributes, with a wildcard"*"option for debugging - A
checkIfShouldTracepredicate to skip tracing for specific requests (e.g. health checks)
Common Use Cases
- Production observability for an Elysia API - export traces and HTTP duration metrics to an OTLP collector (Jaeger, Grafana Tempo, Honeycomb, etc.) without hand-writing span instrumentation for every route.
- Debugging slow requests - inspect per-hook child spans (parse, transform, handle) to see exactly which stage of the request pipeline is adding latency.
- Compliance-conscious logging - rely on default query/URL redaction so access tokens and credentials in request URLs never end up in trace backends.
- Custom span enrichment - use
startActiveSpan/setAttributesinside route handlers or other plugins to attach domain-specific spans and attributes to the same trace the plugin already started. - GraphQL or RPC services built on Elysia - combine with
@envelop/opentelemetryor Eden clients (as shown in the repo’s example app) to trace GraphQL resolvers alongside HTTP-level spans.
Under The Hood
Architecture
The entire plugin lives in a single src/index.ts module that exports one factory function, opentelemetry(), plus a handful of standalone tracer helpers (getTracer, startSpan, startActiveSpan, setAttributes, getCurrentSpan). The factory registers an Elysia .wrap() handler that extracts propagated trace context from incoming request headers and opens a SpanKind.SERVER root span named Root for the whole request, then registers a global .trace({ as: 'global' }, ...) hook that taps every one of Elysia’s internal lifecycle events (onRequest, onParse, onTransform, onBeforeHandle, onHandle, onAfterHandle, onError, onAfterResponse, onMapResponse) and turns each into a correctly parented child span via a hand-rolled setParent/createContext bridge, because Elysia’s own trace-hook API does not carry an OpenTelemetry Context object natively. Ending of the root span, duration recording, and span-parent bookkeeping are all guarded against already-ended spans to tolerate early aborts (the module also listens for the request’s AbortSignal).
Tech Stack
Written in TypeScript, depending directly on @opentelemetry/api, @opentelemetry/instrumentation, and @opentelemetry/sdk-node, with elysia >= 1.4.0 as a peer dependency. The package builds dual CJS/ESM output plus type declarations via a custom build.ts script driving tsup, and is tested with bun test against an extensive devDependencies list of OpenTelemetry exporters (Jaeger, OTLP proto/http, metrics) used only in test/example scenarios, alongside @elysiajs/eden and @elysiajs/graphql-yoga for testing RPC/GraphQL interop.
Code Quality
The test suite is unusually extensive for a plugin of this size - ten dedicated test files (core, metrics, span-privacy, span-headers, abort, integration, advanced, single-hook-span) totaling over 2,200 lines, covering NodeSDK bootstrap detection, span parenting, redaction behavior, abort handling, and header/body capture opt-ins. A test-setup.ts provides shared span-capture utilities and lifecycle hooks (beforeEach/afterEach) to reset state between tests. Beyond the Bun test run, npm run test:node additionally installs and runs separate CJS and ESM smoke-test projects under test/node/, verifying the published package resolves correctly under both module systems - a level of interop testing not common in similarly sized plugins. CI runs build and test on every push and PR via GitHub Actions.
What Makes It Unique Most OpenTelemetry HTTP instrumentation packages either wrap a generic server framework generically or require the application to manually create spans per route. This plugin instead taps Elysia’s fine-grained internal lifecycle hooks directly, giving span-level visibility into parsing, validation/transform, and response-mapping phases specifically - not just “request in, response out” - while treating data privacy as a default-on concern (query redaction, no body/header capture unless opted in) rather than something the integrator must remember to configure separately.