Files
HiveGrid-Wireframes/hivegrid-cloud/docs/improvement-plan/02-observability/logging-migration.md
T

17 KiB

Logging-Migration — console.* zu Pino + ESLint-Enforcement

Status: completed Task-ID: #34 Phase: 02 Estimated Effort: 2.5-3.5 Tage (2-3d Migration + 0.5d Rule) Owner: unassigned

Problem

Die Codebase nutzt an ~200 Stellen direktes console.log / console.error / console.warn / console.info, vor allem in apps/web/app/**/*.server.ts(x). Probleme:

  • PII-Leaks: Funktionen wie authService.ts:99, EmailService.ts:29, CrmService.ts:433/445, PromptBuilderService.ts:185/256, SeedService.ts:199-217 printen Email-Adressen, Tenant-IDs, AI-Prompts (mit User-Content), Stripe-Customer-IDs ungefiltert
  • Kein Structured-Logging: Container-Log-Aggregation (Loki, Grafana, Sentry-Breadcrumbs) kann nicht filtern/queryen, weil keine konsistenten Felder
  • Keine Log-Level: alles ist console.log egal ob Info/Warn/Error → kein Severity-Filtering im Production
  • Sentry-Breadcrumbs-Flood: Sentry-SDK fängt console.* als Breadcrumbs ab → bei 50 console.log pro Request landen 50 Breadcrumb-Einträge in jedem Error → 100KB-Issues
  • Kein Tenant/Request-Context automatisch dabei

Wir haben bereits einen sauberen Pino-Logger:

  • apps/web/app/logger.server.ts:6 → webLogger
  • apps/api/src/logger.ts → apiLogger
  • apps/worker/src/logger.ts → workerLogger
  • packages/logger/ → Basis-Pino-Setup mit Pretty-Print in Dev, JSON in Prod

→ Migration + Lint-Rule, sodass kein console.* mehr in Server-Code drift.

Depends on

  • 02-observability/sentry-wiring-api-worker.md — Sentry-Breadcrumb-Capturing soll nicht von console.*-Wildwuchs überrannt werden, sondern von strukturiertem Logger-Output
  • 02-observability/sentry-wiring-web.md — gleiche Begründung für SSR-Logs

Blocks

  • Keine harten Blocks. Aber alle Phase-03+ Refactorings profitieren stark: strukturierte Logs lassen sich in Loki/Grafana korrelieren mit OTel-Traces (trace_id als Log-Field).

Approach

Zwei Phasen:

Phase 1 — Aufräumen (manuelle Migration)

Manuelle Migration aller console.*-Calls in Server-Code auf den jeweils paketlokalen Logger. Manuell, nicht automatisch, weil:

  • Pino-Argument-Order ist umgekehrt: console.log(msg, obj) → logger.info(obj, msg). Codemod wäre fehleranfällig.
  • Wir nutzen die Gelegenheit, PII zu redacten ODER ganz wegzulassen.
  • Log-Level pro Call-Site neu bewerten (console.log ist oft eigentlich Debug, manchmal Warn).

Reihenfolge nach PII-Risiko:

  1. High-PII first (priorisiert, einer pro Commit):
    • apps/web/app/utils/services/.server/authService.ts:99 — Login/Logout-Flow, hat Email + Session-Token
    • apps/web/app/utils/services/.server/EmailService.ts:29 — Recipient-Email + Subject
    • apps/web/app/utils/services/.server/CrmService.ts:433/445 — Contact-Daten (Name, Email, Telefon)
    • apps/web/app/utils/services/.server/PromptBuilderService.ts:185/256 — AI-Prompts mit User-Content
    • apps/web/app/utils/services/.server/SeedService.ts:199-217 — könnte Seed-User-Passwords printen
  2. Rest von apps/web/app/**/*.server.ts(x) — gruppiert nach Service, ~10-15 Files pro Commit
  3. apps/api/src/** — vermutlich wenig console.*, aber prüfen
  4. apps/worker/src/** — gleich
  5. packages/**/src/** — paketlokaler Logger ggf. neu einführen (manche Packages haben noch keinen)

Phase 2 — Enforce (ESLint-Rule)

Nach abgeschlossener Migration: no-console: ['error', { allow: [] }] für Server-Code-Pfade. Client-Bundle-Files (apps/web/app/components/**/*.tsx, apps/web/app/routes/**/*.tsx ohne .server.-Suffix) bleiben erlaubt (Browser-Konsole ist legitim für UX-Debug). Test-Files mit Override allow: ['log', 'warn', 'error'] für Test-Output.

Pino-Argument-Order-Beispiele (zur Verwendung im Migration-Commit-Body):

// VORHER:
console.log('User signed in', { userId, tenantId });
console.error('Failed to send email', error);

// NACHHER:
webLogger.info({ userId, tenantId }, 'User signed in');
webLogger.error({ err: error, userId }, 'Failed to send email');
//                ^^^ Pino-Convention: 'err' für Error-Objekte

Lefthook-Integration: Pre-commit Hook ruft pnpm lint:staged → ESLint mit der neuen Rule blockiert den Commit bei Verstoß.

Implementation Steps

Phase 1 — Migration

  1. Inventar — scripts/observability/scan-console.ts baut eine Liste aller console.*-Calls in Server-Pfaden mit File:Line + Inhalts-Snippet. Output als Markdown-Tabelle in docs/observability/console-migration-inventory.md.

    • Files: scripts/observability/scan-console.ts, docs/observability/console-migration-inventory.md (generiert)
    • Commit: 02-observability/logging: add console-call inventory script and baseline
  2. High-PII Migration: authService — apps/web/app/utils/services/.server/authService.ts — alle console.* auf webLogger.*, PII-Felder ausschließen / redacten.

    • Files: apps/web/app/utils/services/.server/authService.ts
    • Commit: 02-observability/logging: migrate authService.ts console calls to webLogger
  3. High-PII Migration: EmailService — analog.

    • Commit: 02-observability/logging: migrate EmailService.ts console calls to webLogger
  4. High-PII Migration: CrmService — analog.

    • Commit: 02-observability/logging: migrate CrmService.ts console calls to webLogger
  5. High-PII Migration: PromptBuilderService — analog; AI-Prompt-Body NIE volltext loggen (max. Hash + Length).

    • Commit: 02-observability/logging: migrate PromptBuilderService.ts console calls to webLogger
  6. High-PII Migration: SeedService — analog; Seed-Passwords explizit verbieten (Tests dazu, dass webLogger.info keine Passwords printet).

    • Commit: 02-observability/logging: migrate SeedService.ts console calls to webLogger
  7. Bulk Migration: apps/web/app/utils/services/.server/ — Rest der .server.ts-Services, in 2-3 Commits gruppiert nach logischer Cluster (Stripe-related, AI-related, Workflow-related, ...). Pro Commit max 15 Files.

    • Commits: 02-observability/logging: migrate web services chunk N/M
  8. Bulk Migration: apps/web/app/utils/**/*.server.ts — restliche Utils.

    • Commits: 02-observability/logging: migrate web utils to webLogger
  9. Bulk Migration: apps/web/app/routes/**/*.server.ts(x) — Loader/Action-Side, gruppiert nach Route-Bereich.

    • Commits: 02-observability/logging: migrate web routes to webLogger chunk N/M
  10. Bulk Migration: apps/web/app/modules/**/*.server.ts(x) — Module-Side.

    • Commits: 02-observability/logging: migrate web modules to webLogger chunk N/M
  11. API-Migration — apps/api/src/** zu apiLogger.

    • Commits: 02-observability/logging: migrate api console calls to apiLogger
  12. Worker-Migration — apps/worker/src/** zu workerLogger.

    • Commits: 02-observability/logging: migrate worker console calls to workerLogger
  13. Packages-Migration — packages/**/src/** zu paketlokalem Logger:

    • Prüfen ob Package schon eigenen Logger hat (z.B. via packages/logger-Re-Export)
    • Falls nicht: import { createLogger } from '@toolctrl/logger'; const log = createLogger('<package-name>');
    • Commits: 02-observability/logging: migrate <package> to local logger (eins pro Package)

Phase 2 — Enforce

  1. ESLint-Rule — Root-eslint.config.js (oder .eslintrc) erweitern:

    // Files: ['**/*.server.ts', '**/*.server.tsx', 'apps/api/src/**/*.ts', 'apps/worker/src/**/*.ts', 'packages/**/src/**/*.ts']
    rules: { 'no-console': ['error'] }
    // Override für Test-Files:
    // Files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts']
    rules: { 'no-console': 'off' }
    // Override für Client-Bundle-Files (default für apps/web/app/**/*.tsx ohne .server.):
    // bleibt 'no-console': 'off'
    
    • Files: eslint.config.js (oder welcher Config-File aktuell genutzt wird)
    • Commit: 02-observability/logging: enforce no-console on server code via eslint
  2. Lefthook-Hook bestätigen — bestehender lefthook.yml-Pre-commit-Hook ruft pnpm lint:staged. Prüfen ob das die neue Rule miterfasst (sollte automatisch). Test mit absichtlich injectedem console.log in einer .server.ts-Datei → Pre-commit blockt.

    • Files: ggf. lefthook.yml (nur falls Anpassung nötig)
    • Commit: 02-observability/logging: verify pre-commit blocks console in server code
  3. CI-Job — .github/workflows/ci.yml und Gitea-Equivalent: pnpm lint als blocking Step (sollte bereits sein, aber prüfen).

    • Files: ggf. .github/workflows/ci.yml, .gitea/workflows/ci.yml
    • Commit: 02-observability/logging: ensure lint runs in ci (nur wenn nötig)
  4. Doku — docs/observability/logging.md:

    • Welcher Logger wann (webLogger vs apiLogger vs paketlokal)
    • Pino-Convention (err für Error, obj als erstes Arg, msg als zweites)
    • Log-Level-Konventionen (fatal / error / warn / info / debug / trace)
    • Verbotene Felder (kein Plaintext-Password, keine ungehashten Tokens, kein vollständiger AI-Prompt-Body)
    • Pretty-vs-JSON-Modus, env-gesteuert
    • Files: docs/observability/logging.md
    • Commit: 02-observability/logging: document logger conventions

Test Requirements

  • Unit-Tests:
    • Pro migrierten kritischen Service (auth, Email, Crm, PromptBuilder, Seed) ein Test, dass webLogger-Mock NICHT mit PII-Feldern aufgerufen wird (z.B. assertNotCalled mit password/email/token als String-Key in Args). Test-File neben Service.
    • ESLint-Rule-Test: tests/lint/no-console-server.test.ts — synthetic file mit console.log in .server.ts → ESLint-API liefert Error-Diagnostic. Synthetic file mit console.log in Client-File → kein Error.
  • Integration-Test:
    • Smoke: Pre-commit-Hook mit absichtlichem console.log triggern → Commit blockiert
  • Coverage-Target: keine harte %-Zahl; Pflicht: Tests für die 5 High-PII-Services
  • Test-Files: wie oben

NICHT als completed markieren bevor:

  • alle console.*-Calls in Server-Code-Pfaden entfernt (grep -r "console\." apps/api/src apps/worker/src apps/web/app/**/*.server.ts packages/*/src liefert 0 Hits)
  • ESLint-Rule aktiv, Pre-commit blockt
  • alle Tests grün

Acceptance Criteria

  • Inventar-Script existiert + Output committed
  • Alle ~200 console.*-Calls in Server-Code-Pfaden migriert
  • High-PII-Services (5 oben gelistete) haben PII-Schutz-Tests
  • ESLint-Rule no-console: error für Server-Pfade aktiv
  • Override für Test-Files + Client-Files vorhanden
  • Pre-commit-Hook blockt nachweislich neue console.* in Server-Code
  • pnpm lint --max-warnings=0 grün auf dem gesamten Repo
  • pnpm typecheck grün
  • pnpm test grün
  • docs/observability/logging.md angelegt
  • Inventar-Script in docs/observability/console-migration-inventory.md final auf 0 Console-Calls reduziert (Re-Run nach Migration)
  • Detail-MD Progress Log aktualisiert
  • PROGRESS.md Status auf completed

Rollback Strategy

  • Pro-Migration-Commit revertierbar — jeder Migration-Commit ist eigenständig. Falls eine Migration eine Funktion bricht (z.B. Log-Argument-Order verdreht und ein Test bricht): nur diesen Commit revertieren, Rest bleibt.
  • ESLint-Rule deaktivieren via // eslint-disable-next-line no-console an einer Einzelstelle (NICHT global) — als Notlösung wenn ein Hotfix dringend console.error braucht (und kein Logger-Init zur Verfügung steht).
  • Lefthook-Hook umgehen explizit verbieten (Regel 9 aus AGENT-INSTRUCTIONS) — kein --no-verify.
  • Pino-Output zurück auf Pretty in Prod falls JSON-Output kritischen Log-Konsumenten bricht — env LOG_PRETTY=1.

Security Notes

  • PII-Audit ist Kern dieses Tasks — die High-PII-Services sind explizit aufgelistet. Während Migration: aktiv prüfen, ob alte console.log-Aufrufe Email/Token/Password/Stripe-IDs printten und diese Felder nicht 1:1 in den neuen webLogger-Call übernehmen.
  • AI-Prompt-Bodies NIEMALS volltext loggen — User-Content kann Inhalte enthalten, die wir per DSGVO nicht persistieren dürfen. Stattdessen: { promptHash, promptLength, model, tenantId } loggen.
  • Stripe-Webhook-Bodies sind oft im console.log — Body enthält Card-Last4, Customer-IDs. Nur Event-Type + Event-ID loggen.
  • err als Pino-Convention für Error-Objekte — Pino's err-Serializer redactet automatisch error.config.headers.authorization etc. (eingebaut). Wenn man Error als anderes Field-Name loggt, läuft der Serializer NICHT.
  • Pino-Redact-Pfade in packages/logger/ konfigurieren als Backup: redact: ['*.password', '*.token', '*.authorization', 'req.headers.cookie', ...] — fängt auch dann, wenn ein Entwickler vergisst zu filtern.
  • JSON-Logs in Prod → Container-Log-Stream wird automatisch von Coolify zu Sentry-Breadcrumbs aggregiert. Pretty-Logs in Prod würden den Stream verschmutzen.

Subagent Recommendations

  • Planning: Plan-Subagent mit dieser Detail-MD — soll Inventar zuerst bauen und dann Migration-Reihenfolge der apps/web-Bulk-Commits vorschlagen (welche Files in welchem Commit-Bundle)
  • Inventar-Skript: general-purpose-Subagent für Step 1
  • High-PII Migrations: EIN general-purpose-Subagent für die 5 kritischen Files (Steps 2-6), weil Sicherheits-Audit pro File konzentriert werden muss
  • Bulk Migrations: mehrere parallele general-purpose-Subagents für Steps 7-13 (je einer pro App-Bereich) — aber jeder mit klarem Brief und max 15 Files
  • ESLint-Rule + Lefthook: general-purpose-Subagent für Steps 14-16
  • Doku: kleiner general-purpose-Subagent für Step 17
  • Verification: general-purpose-Subagent für Acceptance-Criteria-Scan (grep-Verifikation auf 0 Hits, Lint-Audit)
  • Code-Review: gsd-code-reviewer nach Task-Completion mit besonderem Fokus auf PII-Redaction in den 5 kritischen Files

Staging-Deployment-Steps

Eher kein Staging-Deployment nötig — das ist eine Code-Hygiene-Aufgabe. Aber:

  1. Branch agent/phase-02-logging pushen → Coolify-Auto-Deploy auf Staging
  2. Container-Log-Output beobachten via Coolify-UI:
    # SSH zum Hetzner-Server
    docker logs <coolify-web-container-id> --tail 100 -f
    # Erwartung: Strukturierte JSON-Zeilen wie:
    # {"level":30,"time":1700000000,"service":"web","msg":"User signed in","userId":"...","tenantId":"..."}
    
  3. Ein paar User-Flows triggern (Login, Email senden, AI-Call):
    • Keine console.*-Output mehr
    • Keine Klartext-Passwords/-Tokens
    • AI-Prompts nur als Hash + Length
  4. Sentry-Breadcrumbs-Check: Ein Test-Error triggern, Issue öffnen, Breadcrumb-Anzahl prüfen — sollte deutlich weniger sein als vor der Migration (vorher 30-50, nachher 5-10)
  5. Wenn alles grün: PR → main

Deviations from Plan

(Wird vom ausführenden Agent gefüllt — typischerweise: einige Bulk-Migrations brauchen mehr Commits weil Code mehr verschachtelt ist als angenommen.)

Progress Log

  • 2026-05-17 — 0b5221b4 — Step 1: scan-console.ts + docs/observability/console-migration-inventory.md baseline (29 hits / 6 files).
  • 2026-05-17 — 51c9861f — Step 2: AuthService migrated to webLogger; AuthService.pii.test.ts added (3 tests).
  • 2026-05-17 — d0b637ed — Step 3: EmailService migrated; EmailService.pii.test.ts added (3 tests); AuthService PII test hardened.
  • 2026-05-17 — b261b369 — Step 4: CrmService migrated (newsletter + ConvertKit paths); CrmService.pii.test.ts added (3 tests).
  • 2026-05-17 — a82cac05 — Step 5: PromptBuilderService migrated; hashPrompt SHA-256 helper added; PromptBuilderService.pii.test.ts added (4 tests).
  • 2026-05-17 — 8ec57d9b — Step 6: SeedService migrated; emailFingerprint helper; SeedService.pii.test.ts added (5 tests).
  • 2026-05-17 — c27bb350 — Step 7 (chunk 1/3): EventsService migrated.
  • 2026-05-17 — 8c61b8de — Step 7 (chunk 2/3): pricingService migrated.
  • 2026-05-17 — 01e77f72 — Step 13: @toolctrl/db permissions-seed migrated; @toolctrl/logger dep added.
  • 2026-05-17 — 165fed0c — Step 14: ESLint no-console: error enforced for server paths; test/scripts/client overrides added.
  • 2026-05-17 — (pending commit) — Step 15-17: pre-commit blocking verified (apps/api console.log → lefthook lint-staged exit 1); docs/observability/logging.md written; final inventory refresh (0 hits / 0 files).

Steps 8-12 were no-ops because the post-Step 7 inventory was already empty: the bulk of the historical ~200 hits had been removed by earlier Phase-02 work (the 51 console.* entries in apps/web/app/components/**/*.tsx, apps/web/app/routes/**/*.tsx, and various non-server helpers are in client-bundle files and out of the no-console-server scope per Step 14). A full re-scan after Step 13 returned 0 hits in server paths.

Lessons Learned

(Optional — typische Stolperfallen: Pino-Argument-Order führt zu Tests die "msg"-String an falscher Position erwarten; manche Files haben console.log innerhalb Template-Strings; console.dir muss separat gemoddet werden.)