Search docs

Jump between documentation pages.

Browse docs

Structured logging

Think of it like… a flight recorder that automatically bleeps out anything that sounds like a password before it writes to the tape. You get a faithful record of what happened, minus the secrets you never wanted on disk.

DaloyJS ships a tiny, zero-dependency structured logger. Every app gets one by default at app.log, and every request handler gets a request-scoped child logger at ctx.state.log that is already bound to the request id. Records are emitted as single-line JSON so they drop straight into Loki, Datadog, CloudWatch, or any log aggregator.

The headline feature is secure-by-default redaction: common credential keys (authorization, cookie, password, token, provider API keys, and more) are replaced with [REDACTED] at any depth, and string values shaped like a JWT or an opaque provider token are scrubbed even when they appear under an innocent key.

The default logger

You do not need to wire anything up. Construct an App and a createLogger({ level: "info" }) instance is attached automatically.

ts
import { App } from "@daloyjs/core";

const app = new App();

app.route({
  method: "GET",
  path: "/orders/:id",
  operationId: "getOrder",
  responses: { 200: { description: "ok" } },
  handler: async ({ params, state }) => {
    // Request-scoped child logger, already bound to the request id.
    state.log.info({ orderId: params.id }, "fetching order");
    return { status: 200 as const, body: { id: params.id } };
  },
});

The app-level logger is also available directly when you are outside a request (startup, scheduled jobs, shutdown):

ts
app.log.info({ port: 3000 }, "server starting");
app.log.warn({ retries: 3 }, "upstream slow");

Choosing a level

Levels follow the familiar pino ordering: trace (10), debug (20), info (30), warn (40), error (50), fatal (60). Records below the configured level are dropped. Set the level when you construct the app:

ts
const app = new App({ logger: { level: "debug" } });

// Disable logging entirely (uses the built-in noopLogger):
const silent = new App({ logger: false });

Calling the logger

Every level method accepts either a message string, or an object of structured fields followed by an optional message. Prefer the object form so your fields stay queryable.

ts
log.info("plain message");
log.info({ userId: 42, action: "login" }, "user signed in");

// Errors: pass the error under an `err` key for consistent serialization.
try {
  await chargeCard();
} catch (err) {
  log.error({ err, paymentId }, "charge failed");
}

Child loggers

Use child() to bind fields that should appear on every subsequent record. This is how the request id is attached to ctx.state.log, and it is the right tool for per-component or per-job context.

ts
const jobLog = app.log.child({ component: "billing-worker", runId });
jobLog.info("starting nightly invoice run");
jobLog.info({ processed: 1280 }, "done");

Redaction (secure by default)

Redaction is on by default. Keys are matched case-insensitively at any depth and replaced with [REDACTED]. The built-in DEFAULT_REDACT_KEYS list covers the usual suspects plus AI / LLM provider credential headers. In addition, any string value shaped like a JWT (eyJ…) or an opaque provider token (GitHub ghp_…, AWS AKIA…, Stripe sk_live_…, OpenAI sk-…, and more) is scrubbed regardless of its key.

ts
log.info(
  {
    userId: 7,
    authorization: "Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig",
    note: "token is sk-ant-0123456789abcdef0123456789",
  },
  "request",
);
// => {"level":"info","userId":7,"authorization":"[REDACTED]",
//     "note":"token is [REDACTED]","msg":"request",...}

Extend the defaults with your own keys, change the replacement string, or opt out entirely:

ts
import { createLogger } from "@daloyjs/core";

const log = createLogger({
  level: "info",
  redact: {
    keys: ["ssn", "card_number"], // merged with DEFAULT_REDACT_KEYS
    censor: "***",
  },
});

// Disable redaction (not recommended outside local debugging):
const raw = createLogger({ redact: false });

Do not turn redaction off in production. The default list exists because these exact keys are the ones most commonly observed leaking secrets into log aggregators in real-world incidents.

Bring your own logger (pino, winston)

The logger option accepts any object implementing the Logger interface (the trace/debug/info/warn/error/fatal methods plus child()). pino already matches this shape, so you can pass it directly:

ts
import pino from "pino";
import { App } from "@daloyjs/core";

const app = new App({ logger: pino({ level: "info" }) });

When you bring your own logger, redaction becomes that logger's responsibility, so configure pino's redact option to match the protection DaloyJS gives you for free.

Customizing the output sink

By default records are written to stdout. Provide a write function to redirect them (for example, to a buffer in tests):

ts
import { createLogger } from "@daloyjs/core";

const lines: string[] = [];
const log = createLogger({ write: (line) => lines.push(line) });

When to reach for it

  • Inside handlers: use ctx.state.log so every line is correlated to the request id automatically.
  • Background work: derive a app.log.child({ component }) so jobs are easy to filter.
  • Security-sensitive payloads: rely on the default redaction rather than hand-stripping fields before you log them.