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).
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.
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
User
Pain Point
App developer
Copies the same Pino config across apps, including redaction paths, required fields, and transport setup.
App developer
Forgets to include required fields, leading to logs that cannot be queried in the aggregation system.
App developer
No standard way to correlate logs across a single HTTP request.
Platform maintainer
Inconsistent log formats make cross-app querying fragile and alert rules unreliable.
Security reviewer
Sensitive data (tokens, passwords) appears in logs because redaction is not applied by default.
Definitions
Term
Definition
Wide event
A log entry that includes all relevant context fields in a single JSON object, rather than relying on log message parsing.
Redaction
Replacing sensitive field values with a placeholder ([REDACTED]) before the log entry is serialized.
Request correlation
Binding a unique requestId to all log entries produced during a single HTTP request lifecycle.
Audit event
A structured log entry specifically documenting a security-relevant action (who did what to which resource).
ADR-023
The platform’s logging policy ADR that this library implements.
ADR-014
Open 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 var
Pino option
Notes
LOG_LEVEL
level
Defaults to info if unset.
SERVICE_NAME
base.service
Required in production.
APP_VERSION
base.version
Required 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.
// 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
ID
Priority
Requirement
LOG-01
P0
@dmwd-io/logger re-exports pino and pino-http with service, version, and env defaults applied from env vars.
LOG-02
P0
Every log entry includes timestamp (ISO 8601), level (string), msg, and event.
LOG-03
P0
Redaction defaults mask password, token, accessToken, refreshToken, authorization, creditCard, ssn fields at any nesting depth.
LOG-04
P0
pinoHttp integration produces request-lifecycle logs with requestId, method, url, statusCode, and duration.
LOG-05
P0
logger.child({ requestId }) creates a child logger that includes the request ID in all entries.
LOG-06
P0
Browser logger outputs structured JSON to console methods.
LOG-07
P1
logger.audit() produces a validated audit event entry.
LOG-08
P0
Contract tests verify required fields and redaction.
LOG-09
P1
Custom redaction paths can be added via config without overriding the defaults.
LOG-10
P1
Optional remote log drain for browser logger.
Functional Requirements
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.
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.
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.
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.
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.
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.
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
Category
Requirement
Performance
Logger creation is synchronous. Logging a single entry adds less than 0.1ms overhead above raw Pino.
Bundle size
Browser build tree-shakes server-only code (pino-http, redaction internals). Browser bundle adds less than 5 KB gzipped over Pino itself.
Compatibility
Works with Pino v9+. Does not depend on Node-specific APIs in browser mode.
Security
Redaction is applied before serialization, so sensitive data never reaches the transport layer.
Testability
createLogger accepts a destination option (Pino destination stream) for capturing log output in tests.
// 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
Dependency
Type
Notes
pino
New (runtime, peer)
Structured JSON logger. Re-exported as the platform standard per ADR-014.
pino-http
New (runtime, peer, server-only)
HTTP request logging middleware. Re-exported. Tree-shaken from browser builds.
zod
Existing
Audit-event validation.
ADR-023
Governance
Logging policy that this library implements.
ADR-014
Governance
Open Source First: designates pino / pino-http as standard rather than building a custom interface.
Risks and Tradeoffs
Risk
Likelihood
Impact
Mitigation
Pino’s redaction does not cover dynamically-keyed objects (e.g., data[unknownKey].password).
Medium
Medium
Document 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.
Medium
Low
Use 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.
Low
Low
Pino’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.
Medium
Low
Provide logger.debug(msg) as a shorthand that sets event: "debug" automatically. Only info and above enforce explicit events.
Open Questions
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.
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.
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:
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).
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().
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.
The audit method is added via a Pino mixin or by wrapping the returned logger with Object.assign(logger, { audit(event) { ... } }).
The auditEventSchema in audit.ts should be exported so the audit-log library (FRD-014) can import and extend it.
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.
Run pnpm typecheck and pnpm vitest run --project unit before declaring complete.
Decision Log
Date
Decision
Rationale
2026-05-26
Use 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-26
Audit 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-26
Redaction paths use Pino’s built-in redact option with wildcards.
Native Pino feature with negligible performance cost.
2026-05-26
Browser remote drain uses fetch with sendBeacon fallback on unload.
No additional dependency. sendBeacon is supported in all modern browsers.
2026-06-02
Reframed 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
Version
Date
Author
Changes
0.1
2026-05-26
David Holmes
Initial draft.
0.2
2026-06-02
David Holmes
Reframed per ADR-014: ship as thin re-export of pino / pino-http, drop custom interface.