For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/routing/logging.md.

Logging and error hooks

Dotpress logs through one logger, given to the app, and gives each request a logger of its own, bound to its request ID.

The logger

By default, the app logs to the console, from the info level. Pass your logger to createApp():

import pino from 'pino'

const app = await createApp({ adapter, logger: pino() })

The Logger interface is the one of Pino: a Pino instance works as is. Any logger with debug, info, warn, error and child() methods, accepting (message) or (data, message), works too.

The default logger can be configured:

import { createConsoleLogger } from '@dotpress/core'

const logger = createConsoleLogger({ level: 'debug', bindings: { service: 'billing-api' } })

The request logger

ctx.logger is a child logger: every line it writes carries the requestId of the request.

defineRoute<"/payments/:id/refund", EmptyObject, false, unknown[]>(route: RouteConfig<"/payments/:id/refund", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/payments/:id/refund", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/payments/:id/refund"
path
: '/payments/:id/refund',
RouteConfig<"/payments/:id/refund", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {
    id: string;
}, false, unknown>
handler
: async ({
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
,
logger: Logger

Logger bound to the request (requestId field).

logger
}) => {
const
const refund: {
    amount: number;
}
refund
= await
const payments: {
    refund(id: string): Promise<{
        amount: number;
    }>;
}
payments
.
function refund(id: string): Promise<{
    amount: number;
}>
refund
(
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
)
logger: Logger

Logger bound to the request (requestId field).

logger
.
Logger.info: LogFn
(data: object, message?: string, ...args: unknown[]) => void (+1 overload)
info
({
paymentId: string
paymentId
:
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
,
amount: number
amount
:
const refund: {
    amount: number;
}
refund
.
amount: number
amount
}, 'Payment refunded')
return
const refund: {
    amount: number;
}
refund
}, })

Pass ctx.logger to the services a request calls: their log lines are tied to the request. ctx.events and ctx.mailer use the request logger for you.

Request IDs

Each request gets an ID, crypto.randomUUID() by default. It is:

  • ctx.requestId in handlers and middlewares;
  • bound to every line of ctx.logger;
  • sent back in the X-Request-ID header of every response, errors included.

To reuse the ID set by a gateway or a load balancer, use generateRequestId. Accept it only from a proxy you trust, and check its format:

const 
const app: App<unknown>
app
= await
createApp<HttpAdapter<unknown>>(options: AppOptions<HttpAdapter<unknown>>): Promise<App<unknown>>

Creates a Dotpress application and mounts its routes on the adapter.

createApp
({
AppOptions<HttpAdapter<unknown>>.adapter: HttpAdapter<unknown>

HTTP adapter (Express, Hono, Fastify…).

adapter
,
AppOptions<HttpAdapter<unknown>>.generateRequestId?: ((request: NormalizedRequest) => string) | undefined

Defaults to crypto.randomUUID().

generateRequestId
: (
request: NormalizedRequest
request
) => {
const
const id: string | string[] | undefined
id
=
request: NormalizedRequest
request
.
NormalizedRequest.headers: IncomingHeaders

Request headers, with lowercase names.

headers
['x-request-id']
return typeof
const id: string | string[] | undefined
id
=== 'string' && /^[\w-]{8,64}$/.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.
test
(
const id: string
id
) ?
const id: string
id
:
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
()
}, })

Logging every request

A small plugin logs every response with the onResponse hook, including the 404 and 405 of the router:

export const 
const httpLogger: {
    readonly name: "http-logger";
    readonly setup: (api: PluginAPI) => void;
}
httpLogger
=
definePlugin<{
    readonly name: "http-logger";
    readonly setup: (api: PluginAPI) => void;
}>(plugin: {
    readonly name: "http-logger";
    readonly setup: (api: PluginAPI) => void;
}): {
    readonly name: "http-logger";
    readonly setup: (api: PluginAPI) => void;
}

Declares a plugin. Extra properties are allowed, and exposed to other plugins through api.getPlugin().

@exampleexport const healthPlugin = (path = '/health') => definePlugin({ name: 'health', setup(api) { api.addRoute({ path, handler: () => ({ status: 'ok' }) }) }, })
definePlugin
({
name: "http-logger"
name
: 'http-logger',
function setup(api: PluginAPI): void
setup
(
api: PluginAPI
api
) {
api: PluginAPI
api
.
PluginAPI.hooks: PluginHooks
hooks
.
PluginHooks.onResponse(hook: ResponseHook): void
onResponse
(({
request: NormalizedRequest
request
,
response: NormalizedResponse
response
,
requestId: string
requestId
,
duration: number

Time spent producing the response, in milliseconds. Streamed bodies are not included.

duration
}) => {
api: PluginAPI
api
.
PluginAPI.logger: Logger

Logger bound to the plugin (plugin field).

logger
.
Logger.info: LogFn
(data: object, message?: string, ...args: unknown[]) => void (+1 overload)
info
(
{
requestId: string
requestId
,
status: number
status
:
response: NormalizedResponse
response
.
NormalizedResponse.status: number
status
,
duration: number
duration
:
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.round(x: number): number

Returns a supplied numeric expression rounded to the nearest integer.

@paramx The value to be rounded to the nearest integer.
round
(
duration: number

Time spent producing the response, in milliseconds. Streamed bodies are not included.

duration
) },
`${
request: NormalizedRequest
request
.
NormalizedRequest.method: string

HTTP method, case-insensitive.

method
} ${
request: NormalizedRequest
request
.
NormalizedRequest.path: string

Request path, without the query string.

path
}`
) }) }, })
const app = await createApp({ adapter, plugins: [httpLogger] })

The hook receives the request, the final response, the request ID, the duration in milliseconds, and the route when one matched (route.path is the declared path, such as /users/:id: a good label for metrics).

Reporting errors

Unexpected errors are logged with the request logger, then passed to the onError hooks: the one of createApp(), and those of the plugins (api.onError()). See Errors.

const app = await createApp({
  adapter,
  onError: (error, ctx) => errorTracker.capture(error, { requestId: ctx?.requestId }),
})

Tracing and request-scoped storage

The wrapRequest hook of a plugin runs the whole pipeline of a request inside a function: a tracing span, or an AsyncLocalStorage context that services read without receiving ctx.

export const 
const requestStore: AsyncLocalStorage<{
    requestId: string;
}>
requestStore
= new
new AsyncLocalStorage<{
    requestId: string;
}>(): AsyncLocalStorage<{
    requestId: string;
}>

This class creates stores that stay coherent through asynchronous operations.

While you can create your own implementation on top of the node:async_hooks module, AsyncLocalStorage should be preferred as it is a performant and memory safe implementation that involves significant optimizations that are non-obvious to implement.

The following example uses AsyncLocalStorage to build a simple logger that assigns IDs to incoming HTTP requests and includes them in messages logged within each request.

import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
  const id = asyncLocalStorage.getStore();
  console.log(`${id !== undefined ? id : '-'}:`, msg);
}

let idSeq = 0;
http.createServer((req, res) => {
  asyncLocalStorage.run(idSeq++, () => {
    logWithId('start');
    // Imagine any chain of async operations here
    setImmediate(() => {
      logWithId('finish');
      res.end();
    });
  });
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// Prints:
//   0: start
//   0: finish
//   1: start
//   1: finish

Each instance of AsyncLocalStorage maintains an independent storage context. Multiple instances can safely exist simultaneously without risk of interfering with each other's data.

@sincev13 .10.0, v12.17.0
AsyncLocalStorage
<{
requestId: string
requestId
: string }>()
export const
const requestStorePlugin: {
    readonly name: "request-store";
    readonly setup: (api: PluginAPI) => void;
}
requestStorePlugin
=
definePlugin<{
    readonly name: "request-store";
    readonly setup: (api: PluginAPI) => void;
}>(plugin: {
    readonly name: "request-store";
    readonly setup: (api: PluginAPI) => void;
}): {
    readonly name: "request-store";
    readonly setup: (api: PluginAPI) => void;
}

Declares a plugin. Extra properties are allowed, and exposed to other plugins through api.getPlugin().

@exampleexport const healthPlugin = (path = '/health') => definePlugin({ name: 'health', setup(api) { api.addRoute({ path, handler: () => ({ status: 'ok' }) }) }, })
definePlugin
({
name: "request-store"
name
: 'request-store',
function setup(api: PluginAPI): void
setup
(
api: PluginAPI
api
) {
api: PluginAPI
api
.
PluginAPI.hooks: PluginHooks
hooks
.
PluginHooks.wrapRequest(hook: RequestWrapper): void

Runs every request matching a route inside the hook (see RequestWrapper). The hooks of the first plugins run outermost.

wrapRequest
((
ctx: RequestContext
ctx
,
next: () => Promise<T>
next
) =>
const requestStore: AsyncLocalStorage<{
    requestId: string;
}>
requestStore
.
AsyncLocalStorage<{ requestId: string; }>.run<Promise<T>>(store: {
    requestId: string;
}, callback: () => Promise<T>): Promise<T> (+1 overload)

Runs a function synchronously within a context and returns its return value. The store is not accessible outside of the callback function. The store is accessible to any asynchronous operations created within the callback.

The optional args are passed to the callback function.

If the callback function throws an error, the error is thrown by run() too. The stacktrace is not impacted by this call and the context is exited.

Example:

const store = { id: 2 };
try {
  asyncLocalStorage.run(store, () => {
    asyncLocalStorage.getStore(); // Returns the store object
    setTimeout(() => {
      asyncLocalStorage.getStore(); // Returns the store object
    }, 200);
    throw new Error();
  });
} catch (e) {
  asyncLocalStorage.getStore(); // Returns undefined
  // The error will be caught here
}
@sincev13 .10.0, v12.17.0
run
({
requestId: string
requestId
:
ctx: RequestContext
ctx
.
RequestContext.requestId: string

Unique identifier of the request, also sent as X-Request-ID.

requestId
},
next: () => Promise<T>
next
))
}, })

The hook must call next() once, and return its result.

Coming from v1

Pino is no longer loaded automatically: pass it to createApp({ logger }). enableHttpLogging and onRequestComplete are replaced by the onResponse hook of a plugin, and onException by onError.