Overview
This guide shows you how to instrument a Hono application with OpenTelemetry and send traces and metrics to SigNoz. The @hono/otel middleware creates one span for each request, named by route, and records HTTP request metrics. The OpenTelemetry Node.js SDK exports the data and traces the calls that your routes make, such as fetch requests and database queries. The main path runs Hono on Node.js. For Bun, see Instrument Hono on Bun.
Prerequisites
- Node.js 22.15 or newer. The current OpenTelemetry SDK packages need this version.
- An instance of SigNoz (either Cloud or Self-Hosted)
- A Hono app on Node.js. To create one, run
npm create hono@latest my-app -- --template nodejs.
Tested with Node.js v22.23.2, Hono 4.13.13, @hono/otel 1.2.0, and @opentelemetry/sdk-node 0.300.0.
Send telemetry from a Hono app
Step 1. Install the packages
Run this command in the root of your project:
npm install @hono/otel \
@opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/instrumentationStep 2. Create the instrumentation file
Create src/instrumentation.ts. This file starts the OpenTelemetry SDK before your app loads:
import { register } from 'node:module'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
// Lets OpenTelemetry patch modules that the ESM app imports
register('@opentelemetry/instrumentation/hook.mjs', import.meta.url)
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
// @hono/otel creates the server span, so skip the duplicate HTTP server span
'@opentelemetry/instrumentation-http': {
disableIncomingRequestInstrumentation: true,
},
}),
],
})
sdk.start()
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0))
})The file does three things:
- The
register()call adds the OpenTelemetry loader hook. The Hono Node.js template is an ECMAScript module (ESM) app, and without the hook the SDK cannot trace outgoingnode:httpcalls. disableIncomingRequestInstrumentationstops the HTTP instrumentation from creating a second server span for each request. Outgoing HTTP calls are still traced.- The
SIGTERMhandler sends the remaining data to SigNoz before the app stops.
The SDK reads the endpoint, the ingestion key, and the service name from environment variables, which you set in Step 4.
Step 3. Add the middleware to your app
Add httpInstrumentationMiddleware() before your routes in src/index.ts:
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { httpInstrumentationMiddleware } from '@hono/otel'
const app = new Hono()
app.use(httpInstrumentationMiddleware())
app.get('/', (c) => {
return c.text('Hello Hono!')
})
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
})Then update the dev and start scripts in package.json, so that Node.js loads the instrumentation file first:
"scripts": {
"dev": "tsx watch --import ./src/instrumentation.ts src/index.ts",
"build": "tsc",
"start": "node --import ./dist/instrumentation.js dist/index.js"
}Step 4. Set environment variables and run
Set the variables in the shell that starts your app, then build and 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>"
npm run build
npm startAdd 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>"Build the container image with the Dockerfile from the Docker tab. 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 node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "--import", "./dist/instrumentation.js", "dist/index.js"]Create a .dockerignore file next to it, so that the build does not copy your local node_modules and dist folders:
node_modules
distBuild the image and pass the variables when you run it:
docker build -t hono-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 \
hono-appSet the variables in PowerShell, then build and 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>"
npm run build
npm startVerify 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 examplehono-app.<environment>: The deployment environment, for exampleproduction.
Send a few requests to your app, for example curl http://localhost:3000/. For local development, npm run dev with the same variables also sends data.
Instrument Hono on Bun
On Bun, the same middleware creates the request spans and the HTTP metrics. Bun does not need the loader hook. Bun's built-in fetch is not traced, so calls that your routes make with fetch do not get their own spans.
Install the packages:
bun add @hono/otel @opentelemetry/api @opentelemetry/sdk-nodeCreate src/instrumentation.ts:
import { NodeSDK } from '@opentelemetry/sdk-node'
const sdk = new NodeSDK()
sdk.start()
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0))
})Add the middleware to src/index.ts:
import { Hono } from 'hono'
import { httpInstrumentationMiddleware } from '@hono/otel'
const app = new Hono()
app.use(httpInstrumentationMiddleware())
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default appSet the same environment variables as in Step 4, then start the app with the instrumentation file preloaded:
bun --preload ./src/instrumentation.ts src/index.tsTested with Bun v1.4.3.
Validate
After you run your instrumented application, make sure that traces and metrics reach SigNoz:
- Send a few requests to your app.
- Open the Services tab and find the service name that you set in
OTEL_SERVICE_NAME. Each operation has the route name, for exampleGET /users/:id. - Wait about a minute for the first metric export, then search for
http.server.request.durationin the Metrics Explorer.


Limitations
@hono/otelcreates one span for the whole request. Individual Hono middleware do not get their own spans.@hono/otelsends only two HTTP metrics:http.server.request.durationandhttp.server.active_requests.- On Cloudflare Workers, the OpenTelemetry Node.js SDK does not run. To trace a Hono app on Workers, see Cloudflare Workers OpenTelemetry.
Troubleshooting
Still no data? See Debug missing traces, logs, and metrics.
Each request has two server spans
- Likely cause: the HTTP instrumentation creates a server span named
GET, without a route, and the Hono span is its child. - Fix: set
disableIncomingRequestInstrumentation: truefor@opentelemetry/instrumentation-http, as shown in Step 2. Do not useignoreIncomingRequestHookfor this. It stops tracing for the whole request, so the Hono span is not sent. - Verify: each request shows one span named by route, for example
GET /.
Outgoing HTTP calls have no spans
- Likely cause: the
register('@opentelemetry/instrumentation/hook.mjs', import.meta.url)call is missing. In an ESM app, the SDK cannot patchnode:httpwithout the loader hook. - Fix: add the
register()call tosrc/instrumentation.ts, as shown in Step 2, and restart the app. - Verify: a request to a route that calls another service shows a
GETclient span under the Hono span.
No data at all
- Likely cause: the app does not load the instrumentation file, or the ingestion key is wrong. The app prints no error by default.
- Fix: make sure that the start command includes
--import ./dist/instrumentation.js. Then setOTEL_LOG_LEVEL=infoand restart the app. An invalid key printsExport failed with non-retryable error: OTLPExporterError: Unauthorized. CorrectOTEL_EXPORTER_OTLP_HEADERS. - 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
- Choose which libraries get traced to cut spans that you do not need.
- Add custom spans and attributes to trace the work inside your routes.
- Send logs from your Node.js app and correlate them with traces.
- Set up alerts for your Hono service on error rate or latency.
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.