logger

Write readable logs in development and structured logs in production.

Developer toolsClient Server

In development you want logs you can read at a glance: a coloured level, the time, the message. In production you want one JSON object per line, so your log service can search and filter them. kitr/logger does both from the same calls, with no dependencies.

Install

kitr add logger

First time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.

The server package adds it for you. It writes config/logger.ts:

import { defineConfig } from 'kitr/logger'

export default defineConfig({
  name: 'app',
  level: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') ?? 'info',
})
Option Default Description
level info The lowest level that is written: trace, debug, info, warn, error, fatal or silent.
name none Shown in brackets before each message, and as name in JSON.
format pretty in development, json in production How each line is written.
bindings none Fields added to every record.
write(line, record) the console Where lines go. Errors go to stderr.

Write logs

The message comes first and the fields second:

logger.info('project created', { projectId: project.id })
logger.warn('slow query', { ms: 1840 })
logger.error(error, { projectId })

In development that prints:

INFO  14:03:22.418 [app] project created {"projectId":"p_42"}

In production the same call writes:

{"level":"info","time":"2026-11-07T14:03:22.418Z","name":"app","projectId":"p_42","msg":"project created"}

Pass an Error and its name, message, stack and code are kept under err.

Where the logger comes from

On the server, every request has its own child logger that adds the request id to each line. In a controller it's ctx.logger. Anywhere else during a request, useRequest().logger from kitr/server returns the same one. Outside a request, use app.container.use('logger').

Child loggers and timing

const log = logger.child({ jobId }, 'billing')
log.info('started')

const invoices = await log.time('loaded invoices', () => db.invoices.pending())
// INFO  [billing] loaded invoices {"jobId":"j_1","durationMs":212}

Development tools

With devtools installed, every log line also appears in the Log panel under "logger", so you can read server and client logs in one place.

Tests

silentLogger drops everything. createLogger({ write }) lets a test collect records and check them:

import { createLogger } from 'kitr/logger'

const records: unknown[] = []
const logger = createLogger({ format: 'json', write: (_line, record) => records.push(record) })