Hono
createHonoMiddleware() from logixia/hono gives a Hono app a trace ID per request and one log entry when the request completes. It uses structural types only, so it adds no dependency on hono and works with any version.
import { Hono } from 'hono';
import { createLogger } from 'logixia';
import { createHonoMiddleware } from 'logixia/hono';
const logger = createLogger({ appName: 'api' });
const app = new Hono<{ Variables: { traceId: string } }>();
app.use(createHonoMiddleware(logger));
app.get('/orders/:id', async (c) => {
await logger.info('loading order', { orderId: c.req.param('id') });
return c.json({ traceId: c.get('traceId') });
});For each request the middleware:
- Reads the trace ID from the first present header of
traceparent,x-trace-id,x-request-id,x-correlation-id,trace-id(validated like everywhere else in logixia: Tracing), or generates a UUID. - Sets it on the response as
x-trace-idand on the context asc.get('traceId'). - Runs the rest of the request inside
runWithTraceId(), so logs written through the Node logger in a handler carry it as theirtraceId. - Logs one entry when the request completes.
The completion entry is request completed with traceId, method, path, status and duration (milliseconds). path includes the query string with secret parameters replaced by [REDACTED] (token, code, password, api_key and the other defaults).
| Outcome | Message | Level |
|---|---|---|
| Response below 500 | request completed | info |
| Response of 500 or above | request completed | error |
| Handler threw, final status 500 or above | request failed | error |
| Handler threw, final status below 500 | request failed | warn |
A failed request also has an error field with the name, message, stack and cause chain, already serialized so it works with any logger. A client error such as HTTPException(403) is expected traffic and logs at warn.
Hono passes a handler's error to app.onError() and then resolves next(), so the middleware reads c.error instead of relying on a try/catch. An error that does escape next() is logged and rethrown.
Options
| Option | Type | Default | Description |
|---|---|---|---|
skip | (c) => boolean | none | Skip tracing and logging, for example for /health. |
traceIdHeader | string | readonly string[] | DEFAULT_TRACE_HEADERS | Header or ordered list to read the trace ID from. |
responseHeader | string | false | 'x-trace-id' | Response header carrying the trace ID. false sets none. |
redactQueryParams | string[] | none | Extra query parameter names redacted from path. |
extraFields | (c) => Record<string, unknown> | none | Fields added to the entry, for example a tenant ID. |
A log call that throws or rejects never fails the request.
Node
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { createLogger } from 'logixia';
import { createHonoMiddleware } from 'logixia/hono';
const logger = createLogger({
appName: 'hono-node',
redact: { paths: ['**.authorization', '**.cookie', '**.password'] },
});
const app = new Hono();
app.use(createHonoMiddleware(logger, { skip: (c) => c.req.url.endsWith('/health') }));
app.get('/health', (c) => c.text('ok'));
app.get('/', (c) => c.json({ ok: true }));
serve({ fetch: app.fetch, port: 3000 });Bun
Bun implements node:async_hooks and node:crypto, so the same code runs there: export the app and Bun serves its fetch.
import { Hono } from 'hono';
import { createLogger } from 'logixia';
import { createHonoMiddleware } from 'logixia/hono';
const logger = createLogger({ appName: 'hono-bun' });
const app = new Hono();
app.use(createHonoMiddleware(logger));
app.get('/', (c) => c.text('hello'));
export default { port: 3000, fetch: app.fetch };Cloudflare Workers
logixia/hono imports only node:async_hooks and node:crypto, which Workers provide when the nodejs_compat compatibility flag is on. Add it to wrangler.toml:
compatibility_date = "2025-01-01"
compatibility_flags = ["nodejs_compat"]The Node logger (createLogger) needs Node APIs that Workers do not have, so pass the browser logger instead. The middleware accepts it because it only needs info, warn and error methods. Entries from the middleware include traceId explicitly, since the browser logger cannot read it from the async context. Use c.get('traceId') to put it on your own entries:
import { Hono } from 'hono';
import { createBrowserLogger } from 'logixia/browser';
import { createHonoMiddleware } from 'logixia/hono';
const logger = createBrowserLogger({ appName: 'edge-api', json: true });
const app = new Hono<{ Variables: { traceId: string } }>();
app.use(createHonoMiddleware(logger));
app.get('/', (c) => {
logger.child({ traceId: c.get('traceId') }).info('handling index');
return c.json({ ok: true });
});
export default app;Two Workers behaviours to know. Timers (Date.now()) only advance across I/O, so duration shows the time spent waiting on fetch and storage, not CPU time. And to ship logs off the Worker, create a BrowserRemoteTransport and flush it in ctx.waitUntil(); Browser and edge runtimes has the full pattern.
Importing logixia/hono does not load the Node logger, file transports or any other Node API, and the built file is checked in CI inside a sandbox that has only those two modules.