@crossdelta/telemetry
OpenTelemetry instrumentation for TypeScript services on Bun and Node.js.
Getting Started
1. Install
bun add @crossdelta/telemetry
2. Import (first line!)
import '@crossdelta/telemetry'
import { Hono } from 'hono'
The import must come before everything else to properly patch modules.
3. Set environment variables
OTEL_SERVICE_NAME=my-service
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp-gateway.grafana.net/otlp/v1/traces
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://otlp-gateway.grafana.net/otlp/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://otlp-gateway.grafana.net/otlp/v1/metrics
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic base64(instanceId:apiKey)
If no endpoint is configured, telemetry stays disabled with zero overhead.
Usage
Hono services (Bun)
Wire instrumentMiddleware() to get per-request spans, HTTP metrics, and log context. Every console.* call within a request automatically includes requestId, traceId, path, and method in Grafana.
import '@crossdelta/telemetry'
import { instrumentMiddleware } from '@crossdelta/telemetry/middleware'
import { Hono } from 'hono'
const app = new Hono()
app.use('*', instrumentMiddleware())
Per request this creates:
- An OTEL server span (
http.method,http.target,http.status_code) - An
http.server.request.durationhistogram entry - ALS context with
requestId,traceId,spanId,path,methodfor log correlation
The W3C traceparent header is propagated automatically, so all downstream services share the same traceId. NATS CloudEvents carry trace context via @crossdelta/cloudevents.
On Node.js (NestJS/Express), HttpInstrumentation handles spans and metrics automatically. instrumentMiddleware() only sets up the ALS context there.
Logging
For most cases, plain console.* works. The OTEL patch captures everything and attaches trace context automatically.
console.info('[ORDER:CREATED]', orderId)
console.error('[PAYMENT:FAILED]', reason)
For queryable key-value attributes in Grafana:
import { logInfo, logError } from '@crossdelta/telemetry/logger'
await logInfo('Order created', { orderId, total, tenantId })
await logError('Payment failed', { orderId, reason, provider })
Qwik apps
import { initRequestContext } from '@crossdelta/telemetry/logger'
export const onRequest: RequestHandler = async (ev) => {
const requestId = initRequestContext(ev)
ev.headers.set('X-Request-Id', requestId)
}
Custom initialization
For non-default settings, use initTelemetry() instead of the side-effect import:
import { initTelemetry } from '@crossdelta/telemetry'
initTelemetry({
metricsIntervalMs: 30_000,
onShutdown: async () => {
await database.close()
},
})
Runtime Support
All signals (logs, metrics, traces) work on both runtimes. The difference is how HTTP spans and metrics are created:
| Feature | Node.js | Bun |
|---|---|---|
| HTTP spans | auto (HttpInstrumentation) | instrumentMiddleware() |
HTTP metrics (http.server.request.duration) | auto (HttpInstrumentation) | instrumentMiddleware() |
| Express/NestJS tracing | Yes | No |
Supported Backends
Grafana Cloud, Better Stack, Jaeger — any OTLP endpoint works.
API Reference
Essential
| Export | From | Description |
|---|---|---|
import '@crossdelta/telemetry' | . | Auto-initialize SDK (side-effect import) |
instrumentMiddleware() | ./middleware | Hono middleware: server span, HTTP metrics, ALS context, X-Request-Id header |
logInfo(msg, data?) | ./logger | Structured info log with ALS context |
logWarn(msg, data?) | ./logger | Structured warning log with ALS context |
logError(msg, data?) | ./logger | Structured error log with ALS context |
logDebug(msg, data?) | ./logger | Structured debug log with ALS context |
initRequestContext(event) | ./logger | Set up per-request ALS context. Returns requestId. |
logger | ./logger | Universal logger object (server: Pino+ALS, client: console) |
Advanced
| Export | From | Description |
|---|---|---|
initTelemetry(config?) | . | Manual SDK initialization with custom options |
shutdownTelemetry() | . | Flush pending data and shut down the SDK |
getSDK() | . | Get the current NodeSDK instance (or null) |
extractTraceparent(request) | ./traceparent | Parse W3C traceparent header from a Request |
parseTraceparent(header) | ./traceparent | Parse a W3C traceparent string |
buildTraceparent(context) | ./traceparent | Build a W3C traceparent header from trace context |
getCurrentTraceparent() | ./traceparent | Get current traceparent from ALS or OTEL active span |
runWithTraceparent(header, fn) | ./traceparent | Run a function inside a trace context derived from a traceparent header |
runWithLogContext(context, fn) | ./logger | Run a function inside a custom ALS context |
buildRequestLogContext(req, url) | ./logger | Build a LogContext Map from a Request |
getLogContext() | ./logger | Read current ALS context as a plain object |
withLogging(name, fn, opts?) | ./logger | Wrap async function with auto error + slow-call logging |
serializeError(error) | ./logger | Serialize Error to { err, errName, stack, cause } |
In production, set OTEL_* vars via your secrets manager or CI/CD.
License
MIT