Skip to content

CMS structured logger

Problem

LMS had a structured JSON logger (apps/lms/src/lib/log.ts) with redaction, while CMS still used raw console.* calls in request-path code (src/lib/auth/*, src/app/api/**, and shared API error handlers), creating inconsistent observability and violating the redaction expectations in .cursor/rules/090-error-handling.mdc.

Normative contract after implementation: .cursor/rules/091-structured-logging.mdc.

Proposal

Option A (selected): Duplicate LMS log.ts into CMS as apps/cms/src/lib/log.ts, then mature both copies in lockstep to avoid drift.

  • CMS import adaptation: use import "server-only"; directly (CMS has no @/lib/server-only shim).
  • Keep the logger API identical across apps: log.info, log.warn, log.error.
  • Keep output as one JSON line per log entry.
  • Emit through console.log, console.warn, and console.error only so the canonical logger remains safe in Edge-reachable graphs.

Architecture + SDLC maturity updates

Redaction contract

The logger must redact, at minimum:

  • Credentials/secrets: passwords, password hashes, tokens, token hashes, proofs, secrets, authorization headers.
  • Runtime/platform secrets: API keys, AUTH_SECRET, DATABASE_URL/connection strings, CMS_API_UUID, CMS_WEBHOOK_SECRET.
  • Session/user-sensitive fields: cookies, session tokens/IDs.
  • PII: emails (masked to domain) and person-name fields (name, firstName, lastName, displayName, fullName).

To preserve observability while redacting generic name, non-PII display labels should use explicit keys such as tenantName, courseName, or resourceName.

Error serialization contract

redactValue must special-case Error values and emit a safe object shape so non-enumerable fields are preserved:

  • { name, message, stack }
  • message is redaction-aware
  • stack is retained for server debugging

Parity / anti-drift

CMS and LMS logger implementations must stay byte-identical except for the server-only import line (@/lib/server-only in LMS vs server-only package import in CMS). Divergence is a review-blocking regression.

Correlation now vs later

This ticket supports correlation fields (traceId, tenantId, userId) via context. Promoting those to required top-level fields and attaching them to PostHog scope is tracked in T-015.

Testability

Both apps must ship co-located logger tests (src/lib/log.test.ts) covering:

  • JSON output shape
  • console routing
  • redaction behavior
  • Error serialization

Adoption scope

In scope

  1. Add apps/cms/src/lib/log.ts.
  2. Replace request-path console.error call sites in CMS auth/API handlers with log.error.
  3. Update NextAuth logger hooks in CMS auth to use structured logger methods.
  4. Backport the same logger maturity improvements to LMS for parity.
  5. Add logger tests in both apps.

Out of scope

  • PostHog observability integration (T-015)
  • Runtime log-level configurability
  • Log shipping/aggregation infrastructure
  • CLI script logging migration (seed, migrate, reset scripts remain human-readable for now)
  • Client-side console.* cleanup

Acceptance criteria

  • CMS has apps/cms/src/lib/log.ts with API parity to LMS.
  • Redaction includes credentials/secrets, environment secrets, session identifiers, emails, and person-name PII.
  • Error values serialize as redaction-safe { name, message, stack }.
  • Co-located logger tests exist in both apps and cover shape/routing/redaction/error handling.
  • No console.error remains in apps/cms/src/lib/auth/ or apps/cms/src/app/api/.
  • Logger adopted in at least five CMS request-path call sites.
  • npm run check passes at repo root.

Notes / decisions log

  • 2026-07-07: Proposed. Option A preferred for speed; extraction to shared package deferred until T-006 is reactivated.
  • 2026-07-09: Deferred — moved to tickets/deferred/; no active implementation planned.
  • 2026-07-10: Reactivated under Option A with implementation in CMS and parity backport in LMS.
  • 2026-07-10: Decision — redact generic name fields; use explicit non-PII keys (tenantName, courseName, resourceName) when observability requires clear labels.
  • 2026-07-10: Decision — exclude CLI/seed/migrate script logging from this ticket; focus on request-path server logs.
  • 2026-07-10: Implemented — CMS logger added, CMS request-path console.error replaced, LMS logger matured for parity, tests added in both apps, and npm run check passed.
  • 2026-07-10: Follow-up — consolidated the logger contract in .cursor/rules/091-structured-logging.mdc, made both app loggers edge-safe via universal console.* emit, removed interim log-core/log-edge split files, and eliminated process.stdout/process.stderr references from src/lib/log.ts.