Skip to content

Structured Logging (Canonical)

This is the single normative rule for structured server logging in the Open Learning Hub monorepo. Other docs should link here instead of restating logging policy.

Requirement

  • Both apps use src/lib/log.ts as the canonical structured server logger.
  • Import it as @/lib/log.
  • Keep the public API identical across apps: log.info, log.warn, and log.error.
  • Emit one JSON line per entry with { level, message, timestamp, context? }.
  • Do not use raw console.* in server request paths such as src/lib/auth/, src/app/api/, shared API error handlers, import/export pipelines, or instrumentation.ts call chains. Use @/lib/log instead.

Emit Contract

  • The logger always emits through console.log, console.warn, and console.error. This console emit is unconditional and is the source of truth for the JSON contract and logger tests.
  • Never reference process.stdout or process.stderr in src/lib/log.ts. Turbopack statically flags those Node APIs in Edge Runtime graphs, even behind runtime guards.
  • @/lib/log must remain safe to import from code that can be compiled for Edge, including instrumentation.ts dependencies, proxy.ts dependencies, Auth.js logger hooks, and route handlers.
  • The structured level field is the source of truth for log severity. Do not depend on stdout/stderr stream routing for application semantics.

Log-sink seam (OpenTelemetry forwarding)

  • After the mandatory console.* emit, the logger forwards the already-redacted record to an optional sink via forwardToLogSink from @open-learning-hub/observability/log-sink.
  • The log-sink module is intentionally dependency-free (no OpenTelemetry, no posthog-node, no Node built-ins), so importing it from src/lib/log.ts preserves Edge-safety. Its default is a no-op — nothing is forwarded until a sink is registered.
  • A sink is registered only in the Node runtime from instrumentation.ts (guarded by process.env.NEXT_RUNTIME === "nodejs"), which builds the OpenTelemetry log bridge and calls registerLogSink(...). The logger core must never import OpenTelemetry or posthog-node directly.
  • Sink failures must never break the request path; forwardToLogSink swallows errors. See 092-analytics-observability.mdc for the PostHog/OTel wiring.

Redaction Contract

The logger boundary must scrub sensitive values before emitting a record.

Redact at minimum:

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

Email values may be masked to preserve the domain, for example ***@example.com.

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

Deeply nested context should be truncated rather than emitted unbounded.

Error values must serialize as a safe object shape so non-enumerable fields are preserved:

  • name
  • message (redaction-aware)
  • stack

Parity And Anti-Drift

  • CMS and LMS src/lib/log.ts implementations must stay byte-identical except for the server-only import line:
    • CMS: import "server-only";
    • LMS: import "@/lib/server-only";
  • Do not introduce log-edge, log-core, or per-import-site logger splits. Keep one canonical logger per app.
  • If one app's logger behavior changes, update the other app and both logger test files in the same change.

Scope Boundaries

In scope:

  • Server request paths.
  • Auth.js logger hooks.
  • Shared server boundaries such as env.ts, errors.ts, import pipelines, export pipelines, and health checks.

Out of scope:

  • CLI, seed, migrate, and reset scripts that intentionally write human-readable terminal output.
  • Client-side console.* cleanup.
  • PostHog error-tracking, product analytics, and alerting configuration, which are governed by 092-analytics-observability.mdc. This rule only owns the console emit contract and the Edge-safe log-sink seam.

Testing

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

  • JSON output shape.
  • console.log / console.warn / console.error routing.
  • Redaction behavior.
  • Error serialization.
  • Log-sink forwarding: no forwarding by default (no-op), and the redacted record is forwarded to a registered sink.