Skip to content

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.

ts
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:

  1. 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.
  2. Sets it on the response as x-trace-id and on the context as c.get('traceId').
  3. Runs the rest of the request inside runWithTraceId(), so logs written through the Node logger in a handler carry it as their traceId.
  4. 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).

OutcomeMessageLevel
Response below 500request completedinfo
Response of 500 or aboverequest completederror
Handler threw, final status 500 or aboverequest failederror
Handler threw, final status below 500request failedwarn

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 ​

OptionTypeDefaultDescription
skip(c) => booleannoneSkip tracing and logging, for example for /health.
traceIdHeaderstring | readonly string[]DEFAULT_TRACE_HEADERSHeader or ordered list to read the trace ID from.
responseHeaderstring | false'x-trace-id'Response header carrying the trace ID. false sets none.
redactQueryParamsstring[]noneExtra query parameter names redacted from path.
extraFields(c) => Record<string, unknown>noneFields added to the entry, for example a tenant ID.

A log call that throws or rejects never fails the request.

Node ​

ts
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.

ts
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:

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:

ts
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.

Released under the MIT License.