Skip to content

FRD: Logging Library

FieldValue
IDFRD-013
OwnerDavid Holmes
StatusDraft
Last Updated2026-06-02
Open Source Librariespino, pino-http, pino-pretty (dev)
DocumentationPino Docs · API Reference · Redaction · pino-http
RelatedADR-027 (Default Tech Stack), ADR-014 (Open Source First)
Target Releasev2.0.0
TypeLibrary
ComplexityM

Document Summary

This FRD ships @dmwd-io/logger as a thin re-export package that designates pino / pino-http as the platform standard for structured JSON logging and bakes in ADR-023 conventions (required fields, redaction defaults, env var names, request correlation). The value is drift prevention and a single import that guarantees consistent wide-event logs across every application — not a custom abstraction over the library.


Introduction

Per ADR-014 (Open Source First), when a mature community library satisfies the requirement, the platform should designate it as the standard rather than build a custom interface. For structured logging, pino and pino-http are that standard. @dmwd-io/logger re-exports pino and pino-http with platform conventions applied: LOG_LEVEL, SERVICE_NAME, and APP_VERSION are the canonical env var names; redaction defaults and required field names match ADR-023. No custom provider interface or adapter layer is built — the library’s own API is the interface.

Today each application either configures Pino from scratch (duplicating redaction paths, field names, and transport setup) or uses console.log with no structure at all. This package eliminates that duplication without hiding the underlying library.


Scope

In scope

  • @dmwd-io/logger: a thin re-export of pino / pino-http with platform defaults pre-applied.
  • Platform env var conventions: LOG_LEVEL, SERVICE_NAME, APP_VERSION read automatically at startup.
  • Required fields always present: timestamp (ISO 8601), level, msg, event (structured event name), service (app identifier), version (semver), env (development/staging/production).
  • Redaction defaults: passwords, tokens, authorization headers, credit card numbers, SSN patterns.
  • Request correlation: withRequestId(requestId) creates a child logger with the request ID bound.
  • Audit-event helpers: logger.audit(event, actor, resource, action, details) that produces a structured audit entry (consumed by the audit-log library, FRD-014).
  • Contract tests that verify required fields are present and redacted fields are masked.

Out of scope

  • Log aggregation infrastructure (ELK, Datadog, Loki).
  • Log rotation or file transport configuration (handled by the deployment platform).
  • Custom Pino transports beyond the built-in browser and server defaults.
  • A custom provider interface or adapter layer over pino (per ADR-014, the library’s API is the interface).

Users and Pain Points

UserPain Point
App developerCopies the same Pino config across apps, including redaction paths, required fields, and transport setup.
App developerForgets to include required fields, leading to logs that cannot be queried in the aggregation system.
App developerNo standard way to correlate logs across a single HTTP request.
Platform maintainerInconsistent log formats make cross-app querying fragile and alert rules unreliable.
Security reviewerSensitive data (tokens, passwords) appears in logs because redaction is not applied by default.

Definitions

TermDefinition
Wide eventA log entry that includes all relevant context fields in a single JSON object, rather than relying on log message parsing.
RedactionReplacing sensitive field values with a placeholder ([REDACTED]) before the log entry is serialized.
Request correlationBinding a unique requestId to all log entries produced during a single HTTP request lifecycle.
Audit eventA structured log entry specifically documenting a security-relevant action (who did what to which resource).
ADR-023The platform’s logging policy ADR that this library implements.
ADR-014Open Source First: designate the community library as the standard; re-export rather than build a custom interface.

Current State

  • ADR-023 defines the logging policy but no shared package implements it.
  • No files exist under src/lib/logging/.
  • Applications that use Pino configure it inline with varying levels of completeness.
  • Some applications use plain console.log with unstructured messages.
  • The audit viewer widget exists but has no canonical write-side log format to consume.

Proposed Solution

@dmwd-io/logger re-exports pino and pino-http directly. Platform conventions (env var names, default redaction paths, required field types) are applied at the re-export boundary. No custom interface is built — the library’s API is the interface.

Import

import { pino, pinoHttp } from '@dmwd-io/logger';

The package reads LOG_LEVEL, SERVICE_NAME, and APP_VERSION from the environment and passes them as Pino defaults so applications don’t need to repeat that wiring.

Platform env var conventions

Env varPino optionNotes
LOG_LEVELlevelDefaults to info if unset.
SERVICE_NAMEbase.serviceRequired in production.
APP_VERSIONbase.versionRequired in production.

Server integration

import { pinoHttp } from '@dmwd-io/logger';
// Astro middleware or Express/Hono integration
const httpLogger = pinoHttp({ logger });
// Automatically logs request start, request completion with duration, and attaches requestId.

Request correlation

const requestLogger = logger.child({ requestId: ctx.requestId });
requestLogger.info({ event: "db.query", table: "orders", duration: 12 }, "Query completed");
// All entries from this request share the same requestId.

Audit events

logger.audit({
event: "user.role_changed",
actor: { id: "usr_456", type: "user" },
resource: { id: "usr_789", type: "user" },
action: "update",
details: { from: "member", to: "admin" },
});

The audit method is a thin wrapper that sets level: "info", adds audit: true to the log entry, and validates the event structure with Zod before logging.


Requirements

IDPriorityRequirement
LOG-01P0@dmwd-io/logger re-exports pino and pino-http with service, version, and env defaults applied from env vars.
LOG-02P0Every log entry includes timestamp (ISO 8601), level (string), msg, and event.
LOG-03P0Redaction defaults mask password, token, accessToken, refreshToken, authorization, creditCard, ssn fields at any nesting depth.
LOG-04P0pinoHttp integration produces request-lifecycle logs with requestId, method, url, statusCode, and duration.
LOG-05P0logger.child({ requestId }) creates a child logger that includes the request ID in all entries.
LOG-06P0Browser logger outputs structured JSON to console methods.
LOG-07P1logger.audit() produces a validated audit event entry.
LOG-08P0Contract tests verify required fields and redaction.
LOG-09P1Custom redaction paths can be added via config without overriding the defaults.
LOG-10P1Optional remote log drain for browser logger.

Functional Requirements

  1. Re-export: @dmwd-io/logger exports pino and pinoHttp (from pino-http) with platform defaults pre-applied. service, version, and env are read from SERVICE_NAME, APP_VERSION, and NODE_ENV env vars at import time.
  2. Required fields: The Pino base object includes service, version, env. The timestamp option uses pino.stdTimeFunctions.isoTime. The formatters.level outputs the level label as a string.
  3. Event field: Every log call must include an event property in the first argument. The library enforces this via TypeScript types (the first argument type requires event: string). At runtime, if event is missing, the logger adds event: "unknown" and logs a warning.
  4. Redaction: Pino’s built-in redact option is configured with paths: ["password", "*.password", "token", "*.token", "accessToken", "*.accessToken", "refreshToken", "*.refreshToken", "authorization", "*.authorization", "headers.authorization", "creditCard", "*.creditCard", "ssn", "*.ssn"]. Additional paths from config are merged.
  5. HTTP logger: pinoHttp is re-exported from pino-http. The platform default serializer adds event: "http.request" and ensures requestId propagates from the request context.
  6. Browser mode: When browser: true, the logger uses Pino’s browser transport with asObject: true so structured data goes to console.log/warn/error. If remoteDrain is provided, entries are also batched and sent via fetch to the drain URL.
  7. Audit method: logger.audit(auditEvent) validates the event with auditEventSchema (Zod), then calls logger.info({ ...auditEvent, audit: true }). The schema requires event, actor, resource, action.

Non-Functional Requirements

CategoryRequirement
PerformanceLogger creation is synchronous. Logging a single entry adds less than 0.1ms overhead above raw Pino.
Bundle sizeBrowser build tree-shakes server-only code (pino-http, redaction internals). Browser bundle adds less than 5 KB gzipped over Pino itself.
CompatibilityWorks with Pino v9+. Does not depend on Node-specific APIs in browser mode.
SecurityRedaction is applied before serialization, so sensitive data never reaches the transport layer.
TestabilitycreateLogger accepts a destination option (Pino destination stream) for capturing log output in tests.

API/Interface Requirements

Logger factory

interface LoggerConfig {
service: string;
version: string;
env: "development" | "staging" | "production";
level?: pino.Level;
browser?: boolean;
redactPaths?: string[];
remoteDrain?: { url: string; batchSize?: number; flushIntervalMs?: number };
destination?: pino.DestinationStream;
}
function createLogger(config: LoggerConfig): Logger;

Logger type

interface Logger extends pino.Logger {
audit(event: AuditEvent): void;
}
interface AuditEvent {
event: string;
actor: { id: string; type: string };
resource: { id: string; type: string };
action: string;
details?: Record<string, unknown>;
}

HTTP logger

// Re-exported from pino-http with platform serializer defaults applied.
export { pinoHttp } from 'pino-http';

Accessibility Requirements

This is a headless logging library with no UI. No accessibility requirements apply directly. Log messages should be written in clear English suitable for operator dashboards.


Content and Documentation Requirements

  • TSDoc on every exported function and type with @remarks linking to ADR-023.
  • A docs/best-practices/logging.mdx guide covering: logger setup, required fields, redaction, HTTP request logging, browser logging, audit events, and testing with captured output.
  • Storybook docs page under Docs/Best Practices/Logging.

Dependencies

DependencyTypeNotes
pinoNew (runtime, peer)Structured JSON logger. Re-exported as the platform standard per ADR-014.
pino-httpNew (runtime, peer, server-only)HTTP request logging middleware. Re-exported. Tree-shaken from browser builds.
zodExistingAudit-event validation.
ADR-023GovernanceLogging policy that this library implements.
ADR-014GovernanceOpen Source First: designates pino / pino-http as standard rather than building a custom interface.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Pino’s redaction does not cover dynamically-keyed objects (e.g., data[unknownKey].password).MediumMediumDocument the limitation. Recommend using known field names for sensitive data. Provide a redactFn escape hatch for custom redaction logic.
Browser remote drain may lose logs if the page unloads before the batch flushes.MediumLowUse navigator.sendBeacon for the final flush on beforeunload. Document that browser logs are best-effort.
Adding Pino as a shared dependency increases the overall bundle.LowLowPino’s browser build is approximately 4 KB gzipped. The structured logging benefits outweigh the cost.
event field enforcement via TypeScript types may be annoying for quick debug logging.MediumLowProvide logger.debug(msg) as a shorthand that sets event: "debug" automatically. Only info and above enforce explicit events.

Open Questions

  1. Should the library ship its own Pino transport for the remote browser drain, or rely on a third-party transport? Leaning toward a simple built-in fetch-based transport with sendBeacon fallback.
  2. Should audit events be logged at info level or a custom audit level? Pino supports custom levels. Leaning toward info with an audit: true flag to avoid custom-level complexity.
  3. Should the library enforce a maximum log entry size to prevent accidental multi-MB log lines? If so, what is the limit?

Acceptance Criteria

  • @dmwd-io/logger re-exports pino and pino-http with service, version, env defaults applied from SERVICE_NAME, APP_VERSION, and NODE_ENV.
  • Every log entry includes timestamp (ISO 8601), level (string name), and msg.
  • Redaction defaults mask password, token, accessToken, refreshToken, authorization, creditCard, ssn at any nesting depth.
  • pinoHttp integration logs request start and completion with method, url, statusCode, duration, and requestId.
  • logger.child({ requestId }) produces a child logger that includes the request ID in all subsequent entries.
  • Browser logger outputs structured JSON to console methods.
  • logger.audit() validates the event with Zod and produces a log entry with audit: true.
  • Contract tests: a test suite that creates a logger with a captured destination, logs entries, and asserts required fields are present and redacted fields are masked.
  • Custom redaction paths from config are merged with defaults (not replacing them).
  • All exports have TSDoc with @remarks referencing ADR-023.

LLM Handoff Instructions

When implementing this FRD:

  1. Create packages/logger/ as a new package directory. Initialize with a package.json that names the package @dmwd-io/logger and lists pino and pino-http as peer dependencies (not bundled).
  2. src/index.ts: re-export pino and pinoHttp from their respective packages. Apply platform defaults (env var reading, redaction paths, formatters.level, timestamp: pino.stdTimeFunctions.isoTime) via a wrapper that merges options before passing to pino().
  3. Read LOG_LEVEL, SERVICE_NAME, and APP_VERSION from process.env at module load time and set them as Pino defaults. Applications can override by passing explicit config.
  4. The audit method is added via a Pino mixin or by wrapping the returned logger with Object.assign(logger, { audit(event) { ... } }).
  5. The auditEventSchema in audit.ts should be exported so the audit-log library (FRD-014) can import and extend it.
  6. Tests go in packages/logger/src/*.test.ts. Use pino.destination({ sync: true }) or a writable stream to capture output. Parse the captured JSON and assert field presence.
  7. Run pnpm typecheck and pnpm vitest run --project unit before declaring complete.

Decision Log

DateDecisionRationale
2026-05-26Use Pino as the logging engine.Pino is the fastest structured JSON logger for Node. It has a browser build. ADR-023 already references it.
2026-05-26Audit events logged at info level with audit: true flag, not a custom Pino level.Avoids custom-level complexity. Aggregation queries filter on audit === true.
2026-05-26Redaction paths use Pino’s built-in redact option with wildcards.Native Pino feature with negligible performance cost.
2026-05-26Browser remote drain uses fetch with sendBeacon fallback on unload.No additional dependency. sendBeacon is supported in all modern browsers.
2026-06-02Reframed per ADR-014 (Open Source First): adopt pino / pino-http as thin re-export rather than building a custom provider interface. Library API is the interface.ADR-014 requires designating the community library as the standard. A custom interface adds maintenance cost with no benefit when pino’s API already satisfies all requirements.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.
0.22026-06-02David HolmesReframed per ADR-014: ship as thin re-export of pino / pino-http, drop custom interface.