# Logger Usage

Authoritative source: [`docs/adr/016-structured-logging-and-observability-shape.md`](../adr/016-structured-logging-and-observability-shape.md).
Implementation: [`lib/logger.ts`](../../lib/logger.ts).

## Why a facade

Every `console.log` in the codebase is a future migration. The logger facade
emits **OpenTelemetry-shaped JSON** in production and pretty-prints in dev,
so when we wire up an OTel collector in Phase 2, no application code changes.

## API

```ts
import { logger } from '@/lib/logger'

logger.info('order.created', { order_id, tenant_id, user_id })
logger.warn('cache.miss',   { key, durationMs })
logger.error('export.failed', err, { project_id })

// Bind context for a request scope:
const log = logger.child({ request_id, user_id })
log.info('handler.start')
log.error('handler.failed', err)
```

## Field shape

Every record contains:

| Field          | Source                                     |
| -------------- | ------------------------------------------ |
| `timestamp`    | ISO 8601, set automatically                |
| `level`        | `debug` / `info` / `warn` / `error`        |
| `message`      | The first argument — keep it event-shaped (`order.created`, not a sentence) |
| `service`      | `LOG_SERVICE` env, defaults to `kadi-web`  |
| `env`          | `NODE_ENV`                                 |
| `release`      | `LOG_RELEASE` env (Vercel sets this)       |
| `trace_id`, `span_id`, `tenant_id`, `user_id`, `request_id` | From `child()` context |
| `err`          | `{ name, message, stack }` if you pass an Error |
| `context`      | The remaining bag of structured fields, with sensitive keys redacted |

## Redaction

`SENSITIVE_KEYS` in `lib/logger.ts` includes: `password`, `token`,
`access_token`, `refresh_token`, `api_key`, `service_role_key`, `cookie`,
`authorization`, `secret`. Any key matching (case-insensitive) is replaced
with `[REDACTED]` before emit. **Update the list when you add new sensitive
fields.**

## Configuration

| Env var          | Purpose                                           | Default  |
| ---------------- | ------------------------------------------------- | -------- |
| `LOG_LEVEL`      | `debug` / `info` / `warn` / `error`               | `info`   |
| `LOG_SERVICE`    | Service name in every record                      | `kadi-web` |
| `LOG_RELEASE`    | Build/release identifier                          | (unset)  |
| `LOG_PRETTY`     | Force pretty output (auto on in dev)              | (unset)  |

## Console discipline

`console.*` is now **banned by ESLint** inside application code
(`app/`, `components/`, `lib/`, `hooks/`, `config/`). The rule is
`no-console: error`. See [`eslint.config.mjs`](../../eslint.config.mjs).

### Allowed zones (explicit, narrow)

| Zone | Why it's allowed | How |
| ---- | --------------- | --- |
| `lib/logger.ts` (the `emit()` function) | This is the single transport that writes to stdout/stderr. It is the whole point of the facade. | Inline `/* eslint-disable no-console */` inside `emit()`. Nowhere else in the file. |
| `__tests__/**`, `*.test.{ts,tsx}`, `*.spec.{ts,tsx}` | Tests routinely need raw console for fixture output and debug assertions. | Per-glob override in `eslint.config.mjs` (`no-console: off`). |
| `scripts/**`, top-level `.cjs` / `.mjs` (e.g. `migrate-colors.mjs`) | Build/check tooling. Not shipped, not runtime. | These paths are in `globalIgnores` in `eslint.config.mjs` — ESLint never sees them. |

Everything else — server components, API route handlers, `'use server'`
actions, client components — **must go through `logger.*`**.

### Migration status

- **R-2026-04-19-04 (console → logger):** closed 2026-04-19. All 45
  `console.*` call sites in application code migrated to the facade
  (server components, 7 owner API routes, `lib/duplicates/merge.ts`,
  `lib/repository/enrichment.ts`, login page, planning/repository/
  projektanlage client components). The ESLint rule is now `error`.
- The legacy `logError(context, error)` helper still works (it calls
  `logger.error` under the hood) so existing callers keep functioning.

## What NOT to log

- Request bodies in full — log identifiers and shapes, not payloads.
- Auth headers, cookies, JWTs — already redacted, but don't structure them
  into your message string either.
- PII you don't strictly need.

## Adding a new allowed zone

Don't, unless there is a concrete operational reason that cannot be served
by `logger.*`. If there is:

1. Add the narrow glob override in `eslint.config.mjs` — the smaller the
   better.
2. Record the reason in `governance/scorecard/bypass-log.md`.
3. Link the bypass entry from a code comment in the allowed zone so the
   next contributor knows why it exists.
