Knead

Search every guide and package page, down to section headings.

↑↓ move Enter open Esc close

Packages

@knead-js/logging

API reference for LoggingModule and JsonLogger, which write redacted JSON structured logs, one record per line.

On this page 13 sections
Terminal
bun add @knead-js/core @knead-js/logging

@knead-js/core is a peer dependency.

LoggingModule.forRoot() makes a JsonLogger the application's LOGGER in every module, so the framework and your providers all write NDJSON (one JSON object per line) to stdout and stderr, redacted with @knead-js/core's rules. The logging guide covers the LOGGER token, the default ConsoleLogger, redaction and logger precedence in depth.

#LoggingModule

#LoggingModule.forRoot()

Output
LoggingModule.forRoot(options?: JsonLoggerOptions): DynamicModule

Returns a global module that provides and exports LOGGER as new JsonLogger({ ...options, development: options.development ?? inject(DEVELOPMENT) }), so development defaults to the application's development flag (KneadFactory.create()'s development option, itself defaulting to NODE_ENV === 'development').

  • Invalid options throw LoggingConfigError from forRoot() itself, while the module tree is being declared.
  • Importing LoggingModule without forRoot() fails startup with LoggingConfigError: Import LoggingModule.forRoot(), not LoggingModule.
  • Importing two different forRoot() results fails startup with LoggingModule.forRoot() is imported <n> times; import it once, in the root module. Importing the same result from several modules is one import.
  • JsonLogger is not registered as a token: inject LOGGER, so code keeps working when the logger is replaced (the KneadFactory logger option, @knead-js/testing).

#JsonLogger

Output
class JsonLogger implements Logger {
  constructor(options?: JsonLoggerOptions);
  child(fields: Readonly<Record<string, unknown>>): JsonLogger;
  debug(message: string, ...meta: unknown[]): void;
  info(message: string, ...meta: unknown[]): void;
  warn(message: string, ...meta: unknown[]): void;
  error(message: string, ...meta: unknown[]): void;
}

Writes each entry as one line of JSON. It implements core's Logger, so it also works on its own, outside any module. The constructor validates its options (see validateJsonLoggerOptions()). Logging never throws.

#JsonLoggerOptions

OptionTypeDefaultDescription
levelLogLevel'debug' when development is true, otherwise 'info'The lowest level written.
namestring'knead'The logger field of every record. Must be non-empty.
fieldsReadonly<Record<string, unknown>>{}Fields added to every record. Must be a plain object without toJSON. Redacted once, when the logger is created.
developmentbooleanNODE_ENV === 'development' for new JsonLogger(); the application's development flag under LoggingModule.forRoot()Whether errors are written in full (redacted stack, own properties and causes) rather than as Name: message. Any other value than a boolean is a LoggingConfigError.
write(line: string, level: LogLevel) => unknownstdout for debug and info, stderr for warn and errorReceives each record as one line of JSON without the trailing newline. A throw or a rejected promise is ignored.
now() => Date() => new Date()The clock for the time field.

The default sink appends \n to each line, and on first use adds a no-op 'error' listener to process.stdout and process.stderr, so a write error such as EPIPE never crashes the process.

#Record shape

JSON
{"time":"2026-10-17T09:30:00.000Z","level":"info","logger":"orders-api","msg":"order placed","service":"orders","orderId":"ord_1042","totalCents":4999}

Keys appear in this order:

KeyContent
timenow().toISOString().
level'debug', 'info', 'warn' or 'error'.
loggerThe name option.
msgThe message, redacted with redactCredentials().
logger fieldsThe fields option, then fields added by child().
meta fieldsThe keys of every plain-object meta argument, later arguments winning.
errThe first Error meta argument, as a string.
metaEvery other meta argument, in order. Present only when there is one.

Fields and meta never set time, level, logger, msg, err or meta, and __proto__, constructor and prototype keys are dropped at every depth.

#Serialization

Every value is copied into plain JSON data and redacted: strings and object keys with redactCredentials(), objects with redactValue() (the values of sensitive keys such as authorization and password become ***). The full rules are in the logging guide.

ValueWritten as
bigintIts decimal string.
undefined, function, symbolOmitted from objects; null in arrays.
Object with toJSON() (such as Date)Its toJSON() result, copied the same way.
Class instanceIts own enumerable properties.
Map, HeadersAn object of their entries.
SetAn array.
Typed array, ArrayBuffer"[<constructor name> <n> bytes]".
Cycle"[Circular]".
Throwing getter or toJSON()"[unreadable]".
Error (anywhere)Outside development, summarizeError(): Name: message, redacted, at most 500 characters, control characters escaped. In development, its redacted stack (or Name: message when the stack does not contain the message), an indented line with its own enumerable properties as redacted JSON (with errors for an AggregateError), then Caused by: ... with the same details for each cause (a cause that is not an Error is written as redacted JSON), up to 16 errors, after which the text ends with Caused by: … (more causes omitted). An error's properties are copied one level below it, so nested errors share the depth limit, and an error reached again through its own properties or causes is "[Circular]".

Output is escaped by JSON.stringify, plus U+2028 and U+2029, so a record is always exactly one line.

#Limits

These caps keep the work linear in the size of the input, whatever is logged:

LimitValueResult when exceeded
Depth4 levels below each meta argument"[Object]" or "[Array]"
Items per array or Set1000A final "[<n> more items]" item
Entries per Map or Headers1000A "…": "more entries omitted" entry
Keys per object1000A "…": "<n> more keys omitted" entry
Values per record10000"[Truncated]"
Errors per cause chain (development)16Caused by: … (more causes omitted)
Detailed errors per record (development)100summarizeError(), as outside development

If the clock or the message itself cannot be read, the logger still writes a record, with "meta":"[unloggable meta]" (and "msg":"[unloggable message]" when the message could not be converted to a string).

#child()

logger.child(fields) returns a JsonLogger with the same level, name, sink, clock and development flag, whose fields are this logger's fields plus fields (a key in fields replaces the parent's). The parent is unchanged. fields must be a plain object without toJSON, otherwise LoggingConfigError: JsonLogger child() fields must be a plain object without toJSON.

#childLogger()

Output
childLogger(logger: Logger, fields: Readonly<Record<string, unknown>>): Logger

Adds fields to every entry of any Logger. For a JsonLogger it returns logger.child(fields). For any other logger it returns a wrapper that passes the fields as the first meta argument of every call; wrapping a wrapper merges the fields into one object (later fields win). Either way the fields are redacted once, when the child is created, and the reserved keys (time, level, logger, msg, err, meta) and __proto__, constructor and prototype are dropped. Throws LoggingConfigError: childLogger() fields must be a plain object without toJSON for other values, whatever the logger. Use it as childLogger(inject(LOGGER), { component: 'invoices' }).

#validateJsonLoggerOptions()

Output
validateJsonLoggerOptions(options: JsonLoggerOptions): void

Throws LoggingConfigError for the first invalid option. JsonLogger and LoggingModule.forRoot() call it. The messages:

ProblemMessage
Unknown levelJsonLogger level must be one of debug, info, warn, error, got <value>
Empty or non-string nameJsonLogger name must be a non-empty string
fields not a plain object, or with toJSONJsonLogger fields must be a plain object without toJSON
write not a functionJsonLogger write must be a function
now not a functionJsonLogger now must be a function

#LoggingConfigError

Extends core's KneadError. Thrown for invalid options, a bare LoggingModule import, a repeated forRoot() import and invalid child fields.

These live in @knead-js/core (and are re-exported by @knead-js/http); they are documented on the core reference page:

  • LOGGER, the injection token, and Logger, the interface with debug, info, warn and error, each (message: string, ...meta: unknown[]) => void.
  • LogLevel: 'debug' | 'info' | 'warn' | 'error'.
  • ConsoleLogger (the default) and MemoryLogger (tests only: it keeps entries unredacted).
  • redactCredentials(), redactValue(), summarizeError(), logValue() and logError().

#Logger precedence

With KneadFactory.create(), from highest to lowest: an overrides entry for LOGGER, the logger option (which replaces every module's LOGGER provider), the LOGGER of a global module such as LoggingModule.forRoot(), and the default ConsoleLogger. @knead-js/testing passes a MemoryLogger as the logger option, so tests capture logs even when the application imports LoggingModule. ApplicationContext.create() from core treats its own logger option differently: there it is only the default, used unless a global module provides LOGGER.