Traces
The @hono/otel middleware automatically traces all HTTP requests. Database queries from CJS libraries (pg, mysql2, mongodb, ioredis) are also auto-instrumented. This page covers manual spans for operations that aren't covered by auto-instrumentation.
Manual Spans
Use @opentelemetry/api to create custom spans for operations that aren't auto-instrumented. These spans automatically become children of the @hono/otel root span:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-hono-app");
app.get("/api/orders/:id", async (c) => {
const orderId = c.req.param("id");
return tracer.startActiveSpan("fetch-order", async (span) => {
try {
span.setAttribute("order.id", orderId);
const order = await db.orders.findById(orderId);
if (!order) {
span.setStatus({ code: SpanStatusCode.ERROR, message: "Order not found" });
span.end();
return c.json({ error: "Not found" }, 404);
}
span.end();
return c.json(order);
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.end();
throw error;
}
});
});Tracing Middleware
Create a Hono middleware that adds attributes to the current span:
import { trace } from "@opentelemetry/api";
import { createMiddleware } from "hono/factory";
const addUserContext = createMiddleware(async (c, next) => {
const span = trace.getActiveSpan();
const userId = c.get("userId");
if (span && userId) {
span.setAttribute("user.id", userId);
}
await next();
});
app.use("/api/*", addUserContext);Exception Recording
The @hono/otel middleware automatically records thrown errors as exception events on the span — they appear as Issues in Traceway with full stack traces.
For cases where you catch an error and want to record it without re-throwing:
import { trace, SpanStatusCode } from "@opentelemetry/api";
app.get("/api/checkout", async (c) => {
const span = trace.getActiveSpan();
try {
await processPayment();
} catch (error) {
if (span) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
}
return c.json({ error: "Payment failed" }, 500);
}
return c.json({ status: "ok" });
});Nested Spans
Spans created inside an active span are automatically linked as children:
const tracer = trace.getTracer("my-hono-app");
async function processPayment(orderId: string) {
return tracer.startActiveSpan("process-payment", async (span) => {
try {
await validateCard(orderId);
await chargeAmount(orderId);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}Outgoing HTTP Requests (Auto-Instrumented)
Outgoing fetch() calls are automatically traced by @opentelemetry/instrumentation-undici — no manual spans needed:
app.get("/api/external", async (c) => {
// This fetch automatically creates a child span with:
// url.full, http.response.status_code, server.address, etc.
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return c.json(data);
});The resulting trace in Traceway:
GET /api/external ← Endpoint (from @hono/otel)
└─ GET ← Span (from instrumentation-undici)
url.full = https://api.example.com/data
http.response.status_code = 200SQLite / Custom Database Spans (Manual)
SQLite has no OTel auto-instrumentation. Wrap queries in manual spans:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-hono-app");
function dbSpan(name, query, fn) {
return tracer.startActiveSpan(name, (span) => {
span.setAttribute("db.system", "sqlite");
span.setAttribute("db.statement", query);
try {
const result = fn();
span.end();
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.end();
throw error;
}
});
}
app.get("/api/users", (c) => {
const users = dbSpan("db.query", "SELECT * FROM users", () =>
db.prepare("SELECT * FROM users").all()
);
return c.json(users);
});What Gets Auto-Instrumented vs Manual
| Operation | Auto-Instrumented? | Notes |
|---|---|---|
| HTTP requests (incoming) | Yes | Via @hono/otel middleware |
Outgoing fetch() calls | Yes | Via instrumentation-undici |
Database queries (pg, mysql2, mongodb) | Yes | CJS packages — patched by require-in-the-middle |
Cache (redis, ioredis) | Yes | CJS packages — patched automatically |
| DNS lookups | Yes | Via instrumentation-dns |
| TCP connections | Yes | Via instrumentation-net |
SQLite (better-sqlite3) | No | Use manual spans (see above) |
| Custom business logic | No | Use tracer.startActiveSpan() |
Next Steps
- Quick Start — setup and installation
- Node.js Traces — more on manual spans and context propagation
- Node.js Metrics — custom metrics with OTel