Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fajarnugraha37/drizzle-castor/llms.txt

Use this file to discover all available pages before exploring further.

Drizzle Castor’s built-in logger wraps pino with a PatternFormatter that supports Quarkus-style format tokens. Every log entry is automatically enriched with the traceId from the current ExecutionContext, so log lines from concurrent requests stay correctly attributed even in high-throughput environments. You configure logging once on the schema builder and it applies uniformly to all repository operations.

Enabling the logger

Call builder.withLogger({ level, format }) before calling .build(). Both fields are optional; omitting level defaults to WARN and omitting format falls back to the library default format string.
import { createSchemaBuilder } from "@fajarnugraha37/drizzle-castor";

const builder = createSchemaBuilder(db, [usersTable, postsTable] as const, "lenient")
  .profiles(["admin", "user", "public"] as const)
  .withLogger({
    level: "INFO",
    format: "%d{HH:mm:ss} %p [%c] (%t) %s",
  });

Log levels

Captures the most granular internal detail: query parsing steps, alias resolution, individual middleware dispatch calls. Use only during local debugging — extremely verbose.
builder.withLogger({ level: "TRACE", format: "%d{HH:mm:ss} %-5p [%c] (%t) %s" });

Format string tokens

The format string is interpreted by the PatternFormatter class in src/helper/logger-helper.ts. Tokens are substituted at runtime for each log entry.
TokenDescriptionExample output
%d{format}Current datetime using the given format string%d{HH:mm:ss}14:32:07
%pLog level in uppercaseINFO, WARN, ERROR
%-5pLog level left-padded to 5 charactersINFO , WARN , ERROR
%cLogger category (always castor.core for the built-in logger)castor.core
%iCurrent process PID12345
%tTrace ID from the current ExecutionContext3f2a1b9c-…
%zSystem timezoneAsia/Jakarta
%sThe log message stringExecuting read on users
%eError stack trace (empty string when no error)Error: …\n at …
%nNewline character(line break)
%{key.path}Context injection — dot-notation path into the ExecutionContext%{params.filter}

Datetime format specifiers

The %d{format} token supports the following sub-tokens inside the braces:
Sub-tokenValue
yyyyFour-digit year
MMTwo-digit month (01–12)
ddTwo-digit day (01–31)
HHTwo-digit hour, 24-hour clock (00–23)
mmTwo-digit minute (00–59)
ssTwo-digit second (00–59)
SSSThree-digit milliseconds (000–999)

Context injection with %{key.path}

The %{key.path} token extracts any value from the ExecutionContext at log time using dot-notation. Array indices are also supported via bracket notation (e.g., params.projection[0]).
// Format string using context injection
builder.withLogger({
  level: "DEBUG",
  format: "%d{HH:mm:ss} %p [%c] (%t) action=%{action} table=%{tableName} profile=%{profile} %s",
});

// A DEBUG log line for a read operation would produce:
// 14:32:07 DEBUG [castor.core] (3f2a1b9c-…) action=read table=users profile=admin Executing searchMany
Access nested fields inside ctx.params — for example, the first element of the projection array:
builder.withLogger({
  level: "TRACE",
  format: "%d{HH:mm:ss} %p [%c] (%t) first_projection=%{params.query.projection[0]} %s",
});
If a middleware writes to ctx.state.tenantId, you can surface it in every log line:
builder.withLogger({
  level: "INFO",
  format: "%d{HH:mm:ss} %p [%c] tenant=%{state.tenantId} (%t) %s",
});
If the dot-notation path does not resolve to a value (because the key is absent or the context is not available), the token is replaced with an empty string — no exception is thrown.

How the logger integrates with pino

Drizzle Castor creates a CastorLogger instance that wraps pino internally (src/helper/logger-helper.ts). Pino handles log-level filtering — if the configured level is INFO, pino’s isLevelEnabled check skips the formatting step entirely for DEBUG and TRACE calls, keeping the hot path cost close to zero. The formatted string is written directly to process.stdout using process.stdout.write, preserving pino’s zero-copy write semantics while giving you full control over the output format.
// Internal CastorLogger method (simplified)
private log(level: string, message: string, ...args: any[]) {
  if (!this.pinoInstance.isLevelEnabled(level)) return;  // Fast no-op

  const formatted = this.formatter.format(level, this.category, message, error);
  process.stdout.write(formatted);  // Zero-copy write
}
The global logger singleton (exported from src/helper/logger-helper.ts) uses AsyncLocalStorage to pull the active ExecutionContext at the moment of each log call — so you never need to pass a logger instance through function arguments.
export const logger: ILogger = {
  info: (msg, ...args) =>
    (executionContextStorage.getStore()?.translatorContext?.logger || internalLogger)
      .info(msg, ...args),
  // ... same pattern for all levels
};
The context-aware logger is used by all internal Drizzle Castor modules. You do not need to import or call it manually; configuring it via builder.withLogger() is sufficient.

Example format strings

// Compact development format: time + level + traceId + message
builder.withLogger({ level: "DEBUG", format: "%d{HH:mm:ss} %p [%c] (%t) %s" });
// Output: 14:32:07 DEBUG [castor.core] (3f2a1b9c) Executing read on users

// Full production format with milliseconds, PID, and error stack
builder.withLogger({
  level: "WARN",
  format: "%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (pid:%i) (%t) %s%e%n",
});
// Output: 2026-05-12 14:32:07,142 WARN  [castor.core] (pid:12345) (3f2a1b9c) Field 'passwordHash' trimmed by RBAC

// Context-enriched format for multi-tenant debugging
builder.withLogger({
  level: "INFO",
  format: "%d{HH:mm:ss} %p table=%{tableName} action=%{action} tenant=%{state.tenantId} (%t) %s",
});
// Output: 14:32:07 INFO table=users action=read tenant=acme-corp (3f2a1b9c) searchOne completed

TraceId correlation for concurrent requests

The %t token always resolves to ctx.traceId — a UUID generated at the start of each repository call and threaded through all middleware, log calls, and telemetry events for that single operation. In a server handling concurrent requests, this lets you grep your log output by traceId to reconstruct the exact sequence of events for a given request without any extra instrumentation.
# Filter logs for a single trace in production
grep "3f2a1b9c-4d5e-6f7a-8b9c-0d1e2f3a4b5c" app.log
Combine the logger with the execution telemetry event for a two-layer observability strategy: the logger captures inline diagnostic detail (query structure, RBAC decisions), while events push structured metrics to your monitoring system.

Middleware pipeline

Add custom middleware that reads from the same ExecutionContext the logger uses.

Telemetry & Events

Subscribe to structured events that carry the same traceId for cross-system correlation.

Schema builder methods

Full reference for withLogger() and all other builder configuration methods.

Quickstart

See a minimal working example with logging configured alongside a repository.

Build docs developers (and LLMs) love