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 atsrc/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-metricsStep 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.
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.tsAdd these variables to the container spec in your deployment manifest:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "https://ingest.<region>.signoz.cloud:443"
- name: OTEL_EXPORTER_OTLP_HEADERS
value: "signoz-ingestion-key=<your-ingestion-key>"
- name: OTEL_SERVICE_NAME
value: "<service-name>"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "deployment.environment.name=<environment>"To send a test request from your workstation, forward a local port to the deployment. Replace <deployment-name> with the name of your deployment:
kubectl port-forward deploy/<deployment-name> 3000:3000Then run curl http://localhost:3000/ in another terminal.
Create a Dockerfile in your project root:
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "src/index.ts"]Build the image and pass the variables when you run it:
docker build -t bun-elysia-app .
docker run \
-e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443" \
-e OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>" \
-e OTEL_SERVICE_NAME="<service-name>" \
-e OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=<environment>" \
-p 3000:3000 \
bun-elysia-appSet the variables in PowerShell, then run your app:
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "https://ingest.<region>.signoz.cloud:443"
$env:OTEL_EXPORTER_OTLP_HEADERS = "signoz-ingestion-key=<your-ingestion-key>"
$env:OTEL_SERVICE_NAME = "<service-name>"
$env:OTEL_RESOURCE_ATTRIBUTES = "deployment.environment.name=<environment>"
bun run src/index.tsVerify 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 examplebun-elysia-app.<environment>: The deployment environment, for exampleproduction. 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-metricsCreate 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.
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:
- Send a few requests to your app.
- Open the Services tab and find the service name you set in
OTEL_SERVICE_NAME. - Open the Metrics Explorer and look for
http.server.request.duration(Elysia only) andv8js.memory.heap.used.


Limitations
These limits apply to @elysia/opentelemetry version 1.4.12.
http.server.request.durationhas nohttp.routeattribute. Query traces to break down traffic by route.http.server.request.durationrecords 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.utilizationas0. Thenodejs.eventloop.delay.*metrics work. - Bun does not report
process.memory.usage. Usev8js.memory.heap.usedfor 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 nometricReader. Without ametricReader, the app sends no metrics at all. - Fix: install
@elysia/opentelemetryand passmetricReadertoopentelemetry()as shown in Step 2. - Verify: after 15 to 30 seconds of traffic, search for
http.server.request.durationin 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.usedappears 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=infoand restart the app. An invalid key printsExport failed with non-retryable error: OTLPExporterError: Unauthorized. An invalid region printsgetaddrinfo ENOTFOUND. CorrectOTEL_EXPORTER_OTLP_HEADERSorOTEL_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
- Use the Bun and ElysiaJS dashboard template to see request rate, errors, latency, and runtime health in one view.
- Explore your traces in SigNoz
- Set up alerts for your Bun service
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.