Skip to content

feat(structured-logger): add @hono/structured-logger middleware#1782

Open
gabry-ts wants to merge 6 commits intohonojs:mainfrom
gabry-ts:feat/structured-logger
Open

feat(structured-logger): add @hono/structured-logger middleware#1782
gabry-ts wants to merge 6 commits intohonojs:mainfrom
gabry-ts:feat/structured-logger

Conversation

@gabry-ts
Copy link

@gabry-ts gabry-ts commented Mar 3, 2026

Summary

Adds a new structured logging middleware for Hono that provides a request scoped logger on c.var.logger.

This middleware is library agnostic and works with pino, winston, bunyan, console, or any logger implementing the BaseLogger interface. It has zero dependencies and is compatible with all runtimes supported by Hono.

Core features:

  • Request scoped logger instance via c.var.logger (configurable key)
  • Automatic response time measurement using performance.now()
  • Native integration with hono/request-id
  • Customizable onRequest, onResponse, and onError hooks
  • Full type safety with generics
  • Works on Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda, Vercel Edge, Fastly Compute

Related: honojs/hono#3963

Test plan

  • 20 unit tests covering all options, hooks, error handling, custom context keys, and default behaviors
  • TypeScript compiles with no errors

@changeset-bot
Copy link

changeset-bot bot commented Mar 3, 2026

🦋 Changeset detected

Latest commit: ca5e4f2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/structured-logger Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

| `createLogger` | `(c: Context) => L` | Yes | | Factory that creates a request scoped logger instance |
| `contextKey` | `string` | No | `'logger'` | Key used to store the logger on `c.var` |
| `onRequest` | `(logger: L, c: Context) => void \| Promise<void>` | No | Logs method + path at info level | Called before handler execution |
| `onResponse` | `(logger: L, c: Context, elapsedMs: number) => void \| Promise<void>` | No | Logs status + elapsed at info level | Called after handler execution |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elapsed time word missing

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `createLogger` | `(c: Context) => L` | Yes | | Factory that creates a request scoped logger instance |
| `contextKey` | `string` | No | `'logger'` | Key used to store the logger on `c.var` |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would the API maybe be simpler if you merge contextKey option into createLogger? like:
createLogger: (c) => ({ logger: rootLogger.child({ requestId: c.var.requestId }) })

could maybe be more flexible, but also more verbose. But I think I prefer your approach...

}

function defaultOnError<L extends BaseLogger>(logger: L, err: Error, c: Context): void {
logger.error({ err, method: c.req.method, path: c.req.path }, 'request error')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also log the status code here as it could be modified by hono's onError handler

}

function defaultOnResponse<L extends BaseLogger>(logger: L, c: Context, elapsedMs: number): void {
logger.info({ status: c.res.status, elapsedMs }, 'request end')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to include method+path also here, so one does not need to always combine this log line with the 'request start' log line to do analytics. This would also more align defaultOnResponse and defaultOnError. But it could also be a personal preference and likely everyone will modify those settings for a production setup.

@gabry-ts
Copy link
Author

gabry-ts commented Mar 3, 2026

Thanks for the review.

  • Fixed "elapsed time" wording in the README
  • Keeping contextKey as a separate option since you also prefer that approach
  • Added status to defaultOnError
  • Added method and path to defaultOnResponse

Let me know if you have any other feedback.

@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.77%. Comparing base (e762ac0) to head (678eecf).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1782      +/-   ##
==========================================
+ Coverage   91.71%   91.77%   +0.05%     
==========================================
  Files         113      114       +1     
  Lines        3779     3804      +25     
  Branches      956      964       +8     
==========================================
+ Hits         3466     3491      +25     
  Misses        281      281              
  Partials       32       32              
Flag Coverage Δ
structured-logger 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding the introduction for adding type support for c.var.logger?

Like this:

import { Hono } from 'hono'
import { structuredLogger } from '@hono/structured-logger'

import pino from 'pino'

const rootLogger = pino()

const app = new Hono<{
  Variables: {
    logger: ReturnType<typeof rootLogger.child>
  }
}>()

app.use(
  structuredLogger({
    createLogger: (c) => rootLogger.child({ foo: 'bar' })
  })
)

app.get('/', (c) => {
  c.var.logger.info('hello') // typed!
  return c.json({ foo: 'bar' })
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabry-ts Ahh, sorry! I missed the Type safe context section you wrote. Passing pino.Logger is better than ReturnType<typeof rootLogger.child>. My request is not necessary. Can you revert the change?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants