For the complete documentation index, see llms.txt. Markdown versions are available by appending .md to documentation URLs.

Bun and ElysiaJS OpenTelemetry Instrumentation Guide

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

Overview

This guide shows you how to instrument a Bun application with OpenTelemetry and send traces and metrics to SigNoz. The main path uses ElysiaJS, a web framework for Bun. The Instrument Bun without Elysia section covers apps that use Bun.serve directly.

Prerequisites

  • Bun installed
  • An instance of SigNoz (either Cloud or Self-Hosted)
  • An Elysia app. To start from scratch, run bun create elysia my-app. The command creates the app entry file at src/index.ts.

Tested with Bun v1.4.2, Elysia 1.4.30, and @elysia/opentelemetry 1.4.12.

Send telemetry from an Elysia app

Step 1. Install the packages

Run this command in the root of your project:

bun add @elysia/opentelemetry \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/sdk-metrics \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto \
  @opentelemetry/instrumentation-runtime-node \
  @opentelemetry/instrumentation-host-metrics

Step 2. Add OpenTelemetry to your app

Add the plugin to your Elysia app. The exporters send data over OTLP/HTTP and read the endpoint and the ingestion key from environment variables, which you set in the next step.

src/index.ts
import { Elysia } from 'elysia'
import { opentelemetry } from '@elysia/opentelemetry'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node'
import { HostMetricsInstrumentation } from '@opentelemetry/instrumentation-host-metrics'
 
const app = new Elysia()
  .use(
    opentelemetry({
      serviceName: process.env.OTEL_SERVICE_NAME ?? 'bun-elysia-app',
      spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
      metricReader: new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter(),
        exportIntervalMillis: 15000,
      }),
      instrumentations: [
        new RuntimeNodeInstrumentation(),
        new HostMetricsInstrumentation(),
      ],
    })
  )
  .get('/', () => 'Hello from Elysia')
  .listen(3000)

Call .use(opentelemetry(...)) before you add your routes. Elysia does not trace routes that you add before the plugin.

Step 3. Set environment variables and run

Set the variables in the shell that starts your app, then run it:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
export OTEL_SERVICE_NAME="<service-name>"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=<environment>"
 
bun run src/index.ts

Verify these values:

  • <region>: Your SigNoz Cloud region.
  • <your-ingestion-key>: Your SigNoz ingestion key.
  • <service-name>: The name that SigNoz shows for your service, for example bun-elysia-app.
  • <environment>: The deployment environment, for example production. SigNoz dashboards filter on it.

Send a few requests to your app, for example curl http://localhost:3000/.

Instrument Bun without Elysia

Bun.serve is not auto-instrumented, so you create one span for each request in your fetch handler. You get traces and runtime metrics, but not http.server.request.duration, which comes from the Elysia plugin.

Install the packages:

bun add @opentelemetry/api \
  @opentelemetry/sdk-node \
  @opentelemetry/sdk-metrics \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto \
  @opentelemetry/instrumentation-runtime-node \
  @opentelemetry/instrumentation-host-metrics

Create index.ts in your project root. It starts the SDK and wraps each request in a span. Set the same environment variables as in Step 3, then run bun run index.ts.

index.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node'
import { HostMetricsInstrumentation } from '@opentelemetry/instrumentation-host-metrics'
import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'
 
const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 15000,
  }),
  instrumentations: [
    new RuntimeNodeInstrumentation(),
    new HostMetricsInstrumentation(),
  ],
})
sdk.start()
 
const tracer = trace.getTracer('bun-serve-app')
 
Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url)
    const parentContext = propagation.extract(context.active(), req.headers, {
      get: (headers, key) => headers.get(key) ?? undefined,
      keys: (headers) => [...headers.keys()],
    })
    return tracer.startActiveSpan(`${req.method} ${url.pathname}`, { kind: SpanKind.SERVER }, parentContext, (span) => {
      span.setAttribute('http.request.method', req.method)
      span.setAttribute('url.path', url.pathname)
      try {
        const res = new Response('Hello from Bun')
        span.setAttribute('http.response.status_code', res.status)
        return res
      } catch (err) {
        span.recordException(err as Error)
        span.setStatus({ code: SpanStatusCode.ERROR })
        span.setAttribute('http.response.status_code', 500)
        return new Response('Internal Server Error', { status: 500 })
      } finally {
        span.end()
      }
    })
  },
})

The propagation.extract call reads the incoming traceparent header, so the span joins the trace of the calling service. For an async handler, make the callback async and call span.end() after you await the response. The span name uses the raw URL path, so routes with IDs create many span names. Use the route pattern from your router instead.

Validate

After you run your instrumented application, check that traces and metrics reach SigNoz:

  1. Send a few requests to your app.
  2. Open the Services tab and find the service name you set in OTEL_SERVICE_NAME.
  3. Open the Metrics Explorer and look for http.server.request.duration (Elysia only) and v8js.memory.heap.used.
SigNoz Traces Explorer list view with Elysia request spans named by method and route, and their Handle child spans
Elysia request spans in the Traces Explorer
SigNoz Metrics Explorer summary for a Bun service listing http.server.request.duration.bucket and runtime metrics
Bun and Elysia metrics in the Metrics Explorer

Limitations

These limits apply to @elysia/opentelemetry version 1.4.12.

  • http.server.request.duration has no http.route attribute. Query traces to break down traffic by route.
  • http.server.request.duration records a duration much shorter than the real request time, because the plugin records it before your route handler finishes. Use the span duration in traces for latency.
  • Bun always reports nodejs.eventloop.utilization as 0. The nodejs.eventloop.delay.* metrics work.
  • Bun does not report process.memory.usage. Use v8js.memory.heap.used for memory.

Troubleshooting

Still no data? See Debug missing traces, logs, and metrics.

Traces appear but http.server.request.duration is missing

  • Likely cause: your project uses @elysiajs/opentelemetry, which has no HTTP metric, or the plugin has no metricReader. Without a metricReader, the app sends no metrics at all.
  • Fix: install @elysia/opentelemetry and pass metricReader to opentelemetry() as shown in Step 2.
  • Verify: after 15 to 30 seconds of traffic, search for http.server.request.duration in the Metrics Explorer.

Traces appear but no metrics

  • Likely cause: the app stopped before the first metric export. The app exports metrics every 15 seconds. Traces can arrive sooner.
  • Fix: keep the app running for at least 30 seconds.
  • Verify: v8js.memory.heap.used appears in the Metrics Explorer for your service.

Elysia routes have no spans

  • Likely cause: the .use(opentelemetry(...)) call comes after the routes.
  • Fix: move .use(opentelemetry(...)) before your .get() and .post() calls.
  • Verify: send a request to each route and open the traces in SigNoz.

No data at all

  • Likely cause: the ingestion key or the region in the endpoint is wrong. The app prints no error by default.
  • Fix: set OTEL_LOG_LEVEL=info and restart the app. An invalid key prints Export failed with non-retryable error: OTLPExporterError: Unauthorized. An invalid region prints getaddrinfo ENOTFOUND. Correct OTEL_EXPORTER_OTLP_HEADERS or OTEL_EXPORTER_OTLP_ENDPOINT.
  • Verify: the error no longer appears, and the service shows on the Services tab.

Setup OpenTelemetry Collector (Optional)

To send telemetry through an OpenTelemetry Collector instead of directly to SigNoz Cloud, set OTEL_EXPORTER_OTLP_ENDPOINT to the OTLP/HTTP endpoint of your Collector, for example http://localhost:4318, and remove the OTEL_EXPORTER_OTLP_HEADERS variable.

See Switch from direct export to Collector for the steps.

Next steps

Get Help

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Is this page helpful

Last updated—October 03, 2026

Edit on GitHub