195 lines
6.6 KiB
Markdown
195 lines
6.6 KiB
Markdown
# Logging Conventions
|
|
|
|
> Owner: Phase-02 Observability. Last update: 2026-05-17 (Task #34).
|
|
|
|
This document is the source of truth for **how to log in this codebase**.
|
|
Phase-02 Task #34 migrated every server-side `console.*` call to Pino
|
|
and locked the rule in via ESLint. Read this before adding new log
|
|
statements.
|
|
|
|
## TL;DR
|
|
|
|
- Use the Pino logger appropriate to your runtime — never `console.*`.
|
|
- Pino argument order: **object first, message string second**.
|
|
- Never log raw PII (email, password, token, full prompt body, full
|
|
webhook payload, Stripe card data).
|
|
- Errors go in the `err` field so Pino's built-in serializer kicks in.
|
|
|
|
## Which logger?
|
|
|
|
| Runtime | Logger | Location |
|
|
|---|---|---|
|
|
| React Router web (SSR / loaders / actions / `.server.ts`) | `webLogger` (or `createScopedLogger("scope")`) | `apps/web/app/utils/logger.server.ts` |
|
|
| Hono API service | `apiLogger` | `apps/api/src/logger.ts` |
|
|
| BullMQ worker | `workerLogger` | `apps/worker/src/logger.ts` |
|
|
| Package code (`packages/*/src/**`) | `createChildLogger(defaultLogger, { component: "<pkg-name>", scope: "<feature>" })` from `@toolctrl/logger` | `packages/logger/src/index.ts` |
|
|
|
|
For larger files / modules, scope your logger:
|
|
|
|
```ts
|
|
// apps/web/app/modules/users/services/AuthService.ts
|
|
import { createScopedLogger } from "~/utils/logger.server";
|
|
const authLogger = createScopedLogger("auth");
|
|
```
|
|
|
|
The scope name appears as a `scope: "auth"` field on every record, so
|
|
Loki/Grafana queries like `{service="web"} | json | scope="auth"` work
|
|
without having to grep through unrelated messages.
|
|
|
|
## Pino argument order
|
|
|
|
```ts
|
|
// ❌ VORHER (console.* style — backwards)
|
|
console.log("User signed in", { userId, tenantId });
|
|
console.error("Failed to send email", error);
|
|
|
|
// ✅ NACHHER (Pino style — object first, msg second)
|
|
authLogger.info({ userId, tenantId, op: "user.signin" }, "User signed in");
|
|
authLogger.error({ err: error, userId, op: "user.signin" }, "Failed to send email");
|
|
```
|
|
|
|
The `err` key is special: Pino's built-in serializer flattens it to
|
|
`{ type, message, stack }` and auto-redacts `error.config.headers.authorization`.
|
|
If you log the error under any other key, the serializer does NOT run.
|
|
|
|
## Log levels
|
|
|
|
| Level | Use for |
|
|
|---|---|
|
|
| `fatal` | Process is about to exit / data was lost. Never reached normally. |
|
|
| `error` | An operation failed and we cannot recover. Send to Sentry. |
|
|
| `warn` | Unexpected condition, but operation continued (fallback used, missing config). |
|
|
| `info` | Significant business event (subscription created, user signed in, seed run started). |
|
|
| `debug` | Verbose diagnostic for development. Hidden in prod by default. |
|
|
| `trace` | Per-iteration / hot-path debug. Almost never used. |
|
|
|
|
Production default level is `info` (env: `LOG_LEVEL=info`). Bump to
|
|
`debug` per service with `LOG_LEVEL=debug` if you need to capture a
|
|
specific flow.
|
|
|
|
## Forbidden fields
|
|
|
|
Never log these — even if the user "doesn't think they matter":
|
|
|
|
- Raw `email` — use `emailFingerprint(email)` from the seed/auth utils
|
|
if you need correlation.
|
|
- Raw `password`, `passwordHash`, `apiKey`, `secret`, `token`,
|
|
`sessionToken`, `bearer`.
|
|
- AI **prompt bodies** — only log `{ promptHash, promptLength, model, tenantId }`.
|
|
See `apps/web/app/modules/promptBuilder/services/.server/PromptBuilderService.ts`
|
|
`hashPrompt()` for the SHA-256 helper.
|
|
- Stripe **webhook bodies** — log only `{ eventType, eventId }`.
|
|
- ConvertKit / CRM API **response bodies** (contain subscriber email).
|
|
- Request **headers** containing `authorization`, `cookie`, `set-cookie`.
|
|
|
|
`packages/logger/src/index.ts` defines a defensive **redact list** at
|
|
the Pino instance level — fields named `password`, `token`, `secret`,
|
|
`apiKey`, `authorization`, `cookie` (plus their `*.<field>` variants)
|
|
are replaced with `[Redacted]` even if a developer forgets. Do not
|
|
rely on this as your primary defence — redaction does NOT cascade
|
|
into nested objects with non-matching key names.
|
|
|
|
## Pretty vs JSON
|
|
|
|
| Environment | Mode | Why |
|
|
|---|---|---|
|
|
| Dev (`NODE_ENV !== "production"`) | Pretty-printed (auto-detect via `pino-pretty` if installed) | Readable in your terminal. |
|
|
| Prod / Staging | JSON (default) | Coolify → Loki / Grafana / Sentry-Breadcrumb pipeline consumes JSON. Pretty-print would break ingestion. |
|
|
|
|
Override with `LOG_PRETTY=1` if you need pretty in prod (only for a
|
|
short debug session — never permanent).
|
|
|
|
## ESLint enforcement
|
|
|
|
`no-console: 'error'` is active for:
|
|
|
|
- `**/*.server.ts` and `**/*.server.tsx`
|
|
- `apps/api/src/**/*.ts`, `apps/worker/src/**/*.ts`
|
|
- `packages/*/src/**/*.ts`
|
|
- `apps/web/app/utils/**/*.ts`
|
|
- `apps/web/app/modules/**/services/**/*.ts`
|
|
- `apps/web/app/modules/**/db/**/*.ts`
|
|
|
|
Override `'off'` for: `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `scripts/**`,
|
|
and client-bundle files (`apps/web/app/components/**/*.tsx`,
|
|
`apps/web/app/routes/**/*.tsx` without `.server.` suffix).
|
|
|
|
The lefthook pre-commit hook runs `scripts/eslint-staged.mjs` which
|
|
fails the commit on any new `console.*` in server code. Verified
|
|
2026-05-17 by injecting `console.log` into `apps/api/src/test-tmp.ts`
|
|
and observing the hook block the commit.
|
|
|
|
## Migration inventory script
|
|
|
|
```bash
|
|
pnpm exec tsx scripts/observability/scan-console.ts
|
|
# generates docs/observability/console-migration-inventory.md
|
|
# exit 1 with --check if any live console.* remain in server paths
|
|
```
|
|
|
|
Wire this into CI eventually (already runs locally; CI run is tracked
|
|
in Task #36 follow-ups).
|
|
|
|
## Examples
|
|
|
|
### Standard info event
|
|
|
|
```ts
|
|
authLogger.info(
|
|
{ userId, tenantId, op: "user.signin", method: "password" },
|
|
"User signed in",
|
|
);
|
|
```
|
|
|
|
### Error with err serializer
|
|
|
|
```ts
|
|
try {
|
|
await sendEmail(...);
|
|
} catch (error) {
|
|
emailLogger.error(
|
|
{ err: error, recipientFingerprint: emailFingerprint(to), op: "email.send" },
|
|
"Failed to send email",
|
|
);
|
|
}
|
|
```
|
|
|
|
### AI prompt (PII-safe)
|
|
|
|
```ts
|
|
promptLogger.info(
|
|
{
|
|
op: "executeTemplate.createChatCompletion",
|
|
templateId,
|
|
model,
|
|
promptHash: hashPrompt(prompt), // SHA-256, first 16 hex chars
|
|
promptLength: prompt.length,
|
|
tenantId,
|
|
},
|
|
"Creating AI chat completion",
|
|
);
|
|
```
|
|
|
|
### Stripe / webhook (PII-safe)
|
|
|
|
```ts
|
|
pricingLogger.info(
|
|
{
|
|
op: "stripe.webhook.received",
|
|
eventType: event.type,
|
|
eventId: event.id,
|
|
livemode: event.livemode,
|
|
},
|
|
"Stripe webhook received",
|
|
);
|
|
```
|
|
|
|
## See also
|
|
|
|
- `docs/improvement-plan/02-observability/logging-migration.md` — full
|
|
task spec (Task #34) including PII rationale.
|
|
- `packages/logger/src/index.ts` — Pino setup + redact paths.
|
|
- `scripts/observability/scan-console.ts` — inventory script.
|
|
- `docs/observability/console-migration-inventory.md` — auto-generated
|
|
per-file inventory (0 hits after Phase-02 #34).
|