Thanks to visit codestin.com
Credit goes to docs.tracewayapp.com

Hono
Traces

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 = 200

SQLite / 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

OperationAuto-Instrumented?Notes
HTTP requests (incoming)YesVia @hono/otel middleware
Outgoing fetch() callsYesVia instrumentation-undici
Database queries (pg, mysql2, mongodb)YesCJS packages — patched by require-in-the-middle
Cache (redis, ioredis)YesCJS packages — patched automatically
DNS lookupsYesVia instrumentation-dns
TCP connectionsYesVia instrumentation-net
SQLite (better-sqlite3)NoUse manual spans (see above)
Custom business logicNoUse tracer.startActiveSpan()

Next Steps