# Logging

RestroAgent uses a single shared [Pino](https://getpino.io) logger for **all
server-side** logging. The frontend continues to use `console.*` (out of
scope for the logging migration).

## Quick reference

```ts
import { childLogger } from '@server/logger';

const log = childLogger('svc.orders');

log.info({ orderId, total }, 'order placed');
log.warn({ err, orderId }, 'gift card redeem email failed');
log.error({ err }, 'unhandled error');
```

Always use `childLogger('namespace.subnamespace')` rather than the bare
`logger`. The namespace shows up as the `ns` field on every line, which is how
log aggregators (e.g. Logtail, Loki, Datadog) split your traffic.

Naming convention: `<area>.<module>` — e.g. `svc.orders`, `engine.twilio.bridge`,
`route.billing.checkout`, `webhook.stripe`, `db.drizzle`, `middleware.api-key`.

## What gets logged automatically

### Request lifecycle

Every Next.js route handler wrapped in `withErrorHandler` runs inside
`runRequestLifecycle` (see `src/server/logger/request.ts`). Per request you
get:

- `request.start` — `{ method, path, reqId, route }`
- `request.end`   — `{ method, path, status, durationMs, reqId, route }`
- `x-request-id` response header (echoed from the inbound header when present,
  otherwise a fresh UUID)

The static `/uploads/*` branch served by the custom Node server in `server.ts`
is wrapped via `runNodeRequestLifecycle` and emits the same lines.

### Request context

`AsyncLocalStorage` (see `src/server/logger/context.ts`) carries a
`RequestContext` for the lifetime of each request. The Pino `mixin` in
`src/server/logger/index.ts` automatically merges these fields into every log
line emitted from anywhere inside the request:

- `reqId`         — always set
- `route`         — `METHOD /path`
- `restaurantId`  — set by `withAuth` after session resolution
- `userId`        — set by `withAuth` after session resolution

You never have to thread these manually. Any `log.*` call inside a request
handler (or anything it `await`s) inherits them.

If you need to add fields mid-request:

```ts
import { updateRequestContext } from '@server/logger/context';
updateRequestContext({ orderId });
```

### Webhooks

All inbound webhook routes emit a single structured `webhook.received` line
after signature verification:

```jsonc
{
  "ns": "webhook.stripe",
  "type": "webhook.received",
  "signatureValid": true,
  "event": "invoice.payment_succeeded",
  "id": "evt_1Q...",
  "msg": "webhook.received"
}
```

Raw payloads, customer phone numbers, and customer emails are **never**
written by this line — only the event type and the provider's event id.

### Database

Drizzle is wired to the shared logger at `db.drizzle`. SQL queries are
emitted at `debug` level only, so they're free in production
(`LOG_LEVEL=info`). Set `LOG_LEVEL=debug` to see them locally.

## Levels and `LOG_LEVEL`

The log level is read from the `LOG_LEVEL` env var (`fatal` / `error` /
`warn` / `info` / `debug` / `trace` / `silent`). Defaults:

- `NODE_ENV=production` → `info`
- otherwise            → `debug`

| Level | Use for                                                          |
| ----- | ---------------------------------------------------------------- |
| fatal | Process about to crash and exit                                  |
| error | Unexpected failure that needs human attention                    |
| warn  | Recoverable / fail-open conditions, signature mismatches         |
| info  | Lifecycle events (request.start/end, webhook.received, boots)    |
| debug | SQL, payload shapes, intermediate state — verbose, dev-only      |
| trace | Per-token / per-frame engine internals                           |

In dev, output goes through `pino-pretty` for human-friendly colour. Set
`LOG_PRETTY=false` to force JSON locally. In production, output is
newline-delimited JSON on stdout — point your log shipper at that.

## PII redaction

The Pino `redact` config in `src/server/logger/index.ts` strips a fixed set
of paths from every log object before it's serialized. Coverage:

- Auth headers: `authorization`, `cookie`, `x-api-key`
- Credentials: `*.api_key`, `*.token`, `*.password`, `*.secret`,
  `*.webhook_secret`, `*.encrypted_*`, etc.
- Customer PII: `*.phone`, `*.email`, `*.customer_phone`, `*.customer_email`,
  `*.from`, `*.to`
- Plus the same paths nested one level deeper (e.g. `req.body.email`)

Censored values become the literal string `[REDACTED]`. The redaction config
is the safety net — **don't rely on it as your primary defence**:

- Don't put a raw phone or email in the log message string (Pino can't
  redact arbitrary message text).
- Use the helpers in `src/server/logger/redact.ts` when you genuinely need a
  partial value for correlation:

  ```ts
  import { redactPhone, redactEmail, redactToken } from '@server/logger/redact';
  log.info({ phone: redactPhone(customer.phone) }, 'order created');
  // → { phone: "***1234", ... }
  ```

If you discover a new PII or credential field, add its path to
`REDACT_PATHS` in `src/server/logger/index.ts` and write a comment explaining
where it appears.

## Errors

Pass errors as the `err` key in the merge object — Pino has built-in
serialization that produces `{ type, message, stack }`:

```ts
try { /* … */ } catch (err) {
  log.error({ err, orderId }, 'order processing failed');
}
```

Avoid `log.error('order failed', err)` — it bypasses the structured fields.

## What is **out of scope**

These were intentionally *not* included in the logging migration:

- External log shipping (Logtail / Datadog / Loki) — stdout JSON is the
  contract; pick a shipper at the platform level.
- Audit logging — that lives in `audit_logs` (DB), with its own service.
- Frontend `console.*` cleanup — browser logs are unaffected.
- OpenTelemetry tracing — `reqId` plays a similar role for now; OTel can be
  layered on later without touching call sites.
