Hono
Instrument your Hono application with OpenTelemetry and export traces and metrics to Traceway. Hono runs on multiple runtimes — this guide covers the Node.js setup. For Cloudflare Workers, see the Cloudflare guide.
Installation
npm install hono @hono/node-server @hono/otel \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-httpSetup
Create an instrumentation.ts file at the root of your project:
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "https://your-traceway-instance.com/api/otel/v1/traces",
headers: { Authorization: "Bearer your-project-token" },
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: "https://your-traceway-instance.com/api/otel/v1/metrics",
headers: { Authorization: "Bearer your-project-token" },
}),
exportIntervalMillis: 30_000,
}),
// Disable instrumentation-http — @hono/otel handles HTTP spans at the middleware level
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-http": { enabled: false },
}),
],
});
sdk.start();Why disable
instrumentation-http? Hono uses@hono/node-server, which imports Node'shttpmodule via ESM. On Node.js 22+, OTel'simport-in-the-middlehook cannot intercept ESM imports of built-in modules, soinstrumentation-httpnever patches the server — no spans are created. The@hono/otel(opens in a new tab) middleware solves this by creating spans at the Hono middleware level instead, which works on all Node.js versions and runtimes.
Application Code
The @hono/otel middleware handles everything automatically — endpoint grouping via http.route, status codes, and exception recording:
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { otel } from "@hono/otel";
const app = new Hono();
app.use(otel());
app.get("/api/users", (c) => {
return c.json({ users: [] });
});
app.get("/api/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ id, name: "John" });
});
serve({ fetch: app.fetch, port: 3000 });Running Your App
Load the instrumentation file before your application code:
# CommonJS
node --require ./instrumentation.js server.js
# ESM / TypeScript (with tsx)
node --import ./instrumentation.ts server.tsOr set via environment variable:
export NODE_OPTIONS="--require ./instrumentation.js"
node server.jsWhat Gets Captured
With @hono/otel and the auto-instrumentations enabled, the following is captured automatically:
| Layer | What's Captured |
|---|---|
@hono/otel middleware | Incoming requests with method, route, status code, duration |
@hono/otel middleware | http.route for endpoint grouping (e.g., GET /api/users/:id) |
@hono/otel middleware | Exceptions recorded as Issues with stack traces |
Database (pg, mysql2, mongodb) | Query spans with SQL statements, connection info |
Cache (redis, ioredis) | Cache operation spans |
Outgoing fetch() | Outgoing HTTP request spans (auto via instrumentation-undici) |
| DNS | DNS lookup spans |
Root SERVER spans become Endpoints in Traceway, child spans become Spans, and exception events become Issues.
Database Auto-Instrumentation
Database libraries like pg, mysql2, mongodb, and ioredis are CommonJS packages. Even in an ESM Hono app, OTel's require-in-the-middle hook patches them successfully. Database queries automatically appear as child spans under the @hono/otel root span — no manual instrumentation needed:
import pg from "pg";
import { otel } from "@hono/otel";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const app = new Hono();
app.use(otel());
app.get("/api/users/:id", async (c) => {
const id = c.req.param("id");
// This query automatically creates a child span:
// pg.query → SELECT * FROM users WHERE id = $1
const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [id]);
return c.json(rows[0]);
});The resulting trace in Traceway:
GET /api/users/:id ← Endpoint (from @hono/otel)
└─ pg.query ← Span (from instrumentation-pg)
├─ dns.lookup ← Span (from instrumentation-dns)
└─ tcp.connect ← Span (from instrumentation-net)Note: This works because
pg,mysql2, and similar packages use CommonJS internally. The ESM import issue that affectshttp(a Node.js built-in) does not affect third-party CJS npm packages.
Environment Variable Alternative
export OTEL_SERVICE_NAME="my-hono-app"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-traceway-instance.com/api/otel"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-project-token"Test Your Integration
Add a test endpoint:
app.get("/test-error", () => {
throw new Error("Test error from Hono");
});Visit /test-error and check your Traceway dashboard — the error should appear under Issues with a full stack trace.
Multi-Runtime Notes
| Runtime | Instrumentation |
|---|---|
| Node.js | OTel Node SDK (this guide) |
| Cloudflare Workers | Native OTLP export — see Cloudflare guide |
| Deno | Use Deno OTel (opens in a new tab) with OTLP/HTTP export to Traceway |
| Bun | Use @opentelemetry/sdk-node (Bun supports most Node.js OTel packages) |
Next Steps
- Traces — manual spans, middleware instrumentation, and exception recording
- Node.js OTel Guide — general Node.js setup details
- OTel Overview — protocol details and authentication