Skip to content

Request Tracker ​

Automatic request logging middleware that assigns a UUID request ID to every request. It logs method, path, client IP, and timing on the way in and out.

IMPORTANT

This component is auto-registered by BaseApplication during initialize(). No manual registration is needed.

Quick Reference ​

ItemValue
Package@venizia/ignis
ComponentRequestTrackerComponent
MiddlewareRequestSpyMiddleware
UtilityNetworkUtility.getIncomingIp()
RuntimesBoth (Bun and Node.js)

Import Paths ​

typescript
import { RequestTrackerComponent } from '@venizia/ignis';

In one example ​

Nothing to configure - once the application starts, every request is logged automatically.

[SpyMW] [<request-id>][127.0.0.1][=>] GET      /hello | query: {} | body: null
[SpyMW] [<request-id>][127.0.0.1][<=] GET      /hello | Took: 1.23 (ms)

Bodies are logged only in a development environment. Everywhere else - including when NODE_ENV is unset - the body is omitted and query is still logged:

[SpyMW] [<request-id>][127.0.0.1][=>] GET      /hello | query: {}
[SpyMW] [<request-id>][127.0.0.1][<=] GET      /hello | Took: 1.23 (ms)
DirectionFormat
Incoming (=>)[requestId][clientIp][=>] METHOD path | query: {...} | body: {...}
Outgoing (<=)[requestId][clientIp][<=] METHOD path | Took: X.XX (ms)

The HTTP method is padded to 8 characters for consistent alignment.

How it works ​

  • One middleware, and an ID it does not install. requestId() comes from RestApplication.registerDefaultMiddlewares(), which runs before any component, so it is already in place when binding() resolves RequestSpyMiddleware from the DI container and registers it. The generator is IGNIS's RequestIdGenerator, not hono's crypto.randomUUID default - that keeps a server and a browser-Worker BFF stamping the same format.
  • IP resolution is best-effort, never fatal. The middleware falls through several sources before giving up - see the resolution order below. An unresolved IP never fails the request; this middleware observes traffic, it does not gate it.
  • Body logging is environment-gated, and it fails closed. RequestSpyMiddleware reads NODE_ENV once in its constructor and logs the body only when the value is one of local, debug, development, dev, sit. Anything else - staging, uat, production, or NODE_ENV unset entirely - logs query only. Query is always logged in every environment.

WARNING

This rule used to be "anything that is not production", which logged full request bodies in staging, uat and with NODE_ENV unset. If you relied on bodies appearing outside a development environment, set NODE_ENV to one of the five values above.

  • Body parsing follows Content-Type, but only in a development environment. There, value() calls parseBody for every request - see the outcomes table below for what each Content-Type resolves to. Outside development, value() calls parseBody only when the Content-Type includes application/json; every other body reaches the route unread. A parse failure throws 'Malformed Body Payload' (HTTP 400).

WARNING

A form body (multipart/form-data or application/x-www-form-urlencoded) is not parsed here outside development. A route that declares a form body gets its own formBodyReader middleware, and parseMultipartBody maps its own parse failure - both land on the same 400 core.request.body_malformed. Application code that calls context.req.formData() itself, ahead of any framework reader, gets a raw TypeError outside development - a 500, not a 400. Call readFormBody({ req: context.req }) from @venizia/ignis-helpers instead; see Request Utility.

  • The middleware is an IProvider, not a plain function. RequestSpyMiddleware implements IProvider<MiddlewareHandler> from @venizia/ignis-inversion. The container instantiates the class, so it can hold isDebugMode state as an instance field. It then calls .value() to obtain the actual Hono handler.

TIP

The request ID is also available in the framework's error handlers (notFoundHandler, AppErrorMiddleware) - the same ID correlates error logs with the original request.

Common tasks ​

Correlate logs with a request ​

Read the requestId context value inside your own handlers or middleware - the same ID appears in every [SpyMW] log line for that request.

typescript
const requestId = context.get('requestId');

Reuse parseBody for your own middleware ​

parseBody is a public method - reuse it wherever you need the same Content-Type-aware parsing.

typescript
async parseBody(opts: { req: TContext['req'] }): Promise<unknown>

Understand the client IP resolution order ​

PrioritySourceNotes
1NetworkUtility.getIncomingIp(context)Native connection info - hono/bun on Bun, @hono/node-server/conninfo on Node.js
2x-real-ip headerSet by reverse proxies (e.g., Nginx proxy_set_header X-Real-IP)
3x-forwarded-for headerStandard proxy header
4'unknown'Logged when none of the above resolve - the request still proceeds

Understand body-parsing outcomes ​

parseBody itself behaves the same regardless of environment; what changes is whether value() calls it. In a development environment it is called for every request. Outside development it is called only when Content-Type includes application/json - every other row below is reachable only in development.

ConditionResult
No Content-Type headernull
Content-Length is '0', or no body stream presentnull
Content-Type includes application/jsonreq.json()
Content-Type includes multipart/form-data or application/x-www-form-urlencodedreq.parseBody()
Content-Type is application/octet-streamRaw body stream
Content-Type starts with text/Text of a clone - the handler still reads the original stream
Any other Content-Type (images, spreadsheets, PDF, etc.)<N bytes, content-type> - never read
Parsing throws for any content type'Malformed Body Payload' (HTTP 400)

Reference ​

Configuration ​

No user-configurable options - behavior is fully automatic.

Binding keys ​

KeyConstantTypeRequiredDefault
middlewares.RequestSpyMiddlewareRequestTrackerComponent.REQUEST_TRACKER_MW_BINDING_KEYMiddlewareHandlerAutoSingleton provider, registered by the constructor

The key is built as BindingNamespaces.MIDDLEWARE ('middlewares') + . + RequestSpyMiddleware.name.

RequestSpyMiddleware ​

typescript
class RequestSpyMiddleware extends BaseHelper implements IProvider<MiddlewareHandler> {
  static readonly REQUEST_ID_KEY = 'requestId';
  private isDebugMode: boolean;
  // ...
}
  • Extends BaseHelper with scope 'SpyMW'
  • Constructor sets isDebugMode by MEMBERSHIP, never by inequality against production:
typescript
const env = Environment.ambient?.toLowerCase();
this.isDebugMode = !!env && EnvironmentNames.DEVELOPMENT_ENVS.has(env);
  • value() returns a Hono middleware built via createMiddleware() from hono/factory

Component lifecycle ​

  1. constructor() - Receives BaseApplication via DI. Defines the middleware binding as a singleton provider.
  2. binding() - Resolves the RequestSpyMiddleware binding, throwing if it cannot be resolved. Registers the resolved middleware on the server. The request ID is already installed by the application's default stack.

Troubleshooting ​

SymptomCauseFix
Invalid middleware to init request tracker | Please check again binding valueRequestSpyMiddleware binding was unbound or overwritten before binding() ranDon't unbind or replace middlewares.RequestSpyMiddleware; extend the component instead of removing its binding
Malformed Body Payload (400)Body content didn't match its declared Content-Type (e.g., invalid JSON with application/json)Ensure clients send body content that matches the Content-Type header

See also ​

Files: