@crossdelta/telemetry
v0.13.1
Published
OpenTelemetry instrumentation for CrossDelta services
Maintainers
Readme
@crossdelta/telemetry
OpenTelemetry instrumentation for TypeScript services on Bun and Node.js.
Getting Started
1. Install
bun add @crossdelta/telemetry2. 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
