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-217printen 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.logegal ob Info/Warn/Error → kein Severity-Filtering im Production - Sentry-Breadcrumbs-Flood: Sentry-SDK fängt
console.*als Breadcrumbs ab → bei 50console.logpro 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→webLoggerapps/api/src/logger.ts→apiLoggerapps/worker/src/logger.ts→workerLoggerpackages/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 vonconsole.*-Wildwuchs überrannt werden, sondern von strukturiertem Logger-Output02-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_idals 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.logist oft eigentlich Debug, manchmal Warn).
Reihenfolge nach PII-Risiko:
- High-PII first (priorisiert, einer pro Commit):
apps/web/app/utils/services/.server/authService.ts:99— Login/Logout-Flow, hat Email + Session-Tokenapps/web/app/utils/services/.server/EmailService.ts:29— Recipient-Email + Subjectapps/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-Contentapps/web/app/utils/services/.server/SeedService.ts:199-217— könnte Seed-User-Passwords printen
- Rest von
apps/web/app/**/*.server.ts(x)— gruppiert nach Service, ~10-15 Files pro Commit apps/api/src/**— vermutlich wenigconsole.*, aber prüfenapps/worker/src/**— gleichpackages/**/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
-
Inventar —
scripts/observability/scan-console.tsbaut eine Liste allerconsole.*-Calls in Server-Pfaden mit File:Line + Inhalts-Snippet. Output als Markdown-Tabelle indocs/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
- Files:
-
High-PII Migration: authService —
apps/web/app/utils/services/.server/authService.ts— alleconsole.*aufwebLogger.*, 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
- Files:
-
High-PII Migration: EmailService — analog.
- Commit:
02-observability/logging: migrate EmailService.ts console calls to webLogger
- Commit:
-
High-PII Migration: CrmService — analog.
- Commit:
02-observability/logging: migrate CrmService.ts console calls to webLogger
- Commit:
-
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
- Commit:
-
High-PII Migration: SeedService — analog; Seed-Passwords explizit verbieten (Tests dazu, dass
webLogger.infokeine Passwords printet).- Commit:
02-observability/logging: migrate SeedService.ts console calls to webLogger
- Commit:
-
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
- Commits:
-
Bulk Migration:
apps/web/app/utils/**/*.server.ts— restliche Utils.- Commits:
02-observability/logging: migrate web utils to webLogger
- Commits:
-
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
- Commits:
-
Bulk Migration:
apps/web/app/modules/**/*.server.ts(x)— Module-Side.- Commits:
02-observability/logging: migrate web modules to webLogger chunk N/M
- Commits:
-
API-Migration —
apps/api/src/**zuapiLogger.- Commits:
02-observability/logging: migrate api console calls to apiLogger
- Commits:
-
Worker-Migration —
apps/worker/src/**zuworkerLogger.- Commits:
02-observability/logging: migrate worker console calls to workerLogger
- Commits:
-
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)
- Prüfen ob Package schon eigenen Logger hat (z.B. via
Phase 2 — Enforce
-
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
- Files:
-
Lefthook-Hook bestätigen — bestehender
lefthook.yml-Pre-commit-Hook ruftpnpm lint:staged. Prüfen ob das die neue Rule miterfasst (sollte automatisch). Test mit absichtlich injectedemconsole.login 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
- Files: ggf.
-
CI-Job —
.github/workflows/ci.ymlund Gitea-Equivalent:pnpm lintals 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)
- Files: ggf.
-
Doku —
docs/observability/logging.md:- Welcher Logger wann (
webLoggervsapiLoggervs paketlokal) - Pino-Convention (
errfür Error,objals erstes Arg,msgals 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
- Welcher Logger wann (
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 mitpassword/email/tokenals String-Key in Args). Test-File neben Service. - ESLint-Rule-Test:
tests/lint/no-console-server.test.ts— synthetic file mitconsole.login.server.ts→ ESLint-API liefert Error-Diagnostic. Synthetic file mitconsole.login Client-File → kein Error.
- Pro migrierten kritischen Service (auth, Email, Crm, PromptBuilder, Seed) ein Test, dass
- Integration-Test:
- Smoke: Pre-commit-Hook mit absichtlichem
console.logtriggern → Commit blockiert
- Smoke: Pre-commit-Hook mit absichtlichem
- 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/*/srcliefert 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: errorfü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=0grün auf dem gesamten Repopnpm typecheckgrünpnpm testgründocs/observability/logging.mdangelegt- Inventar-Script in
docs/observability/console-migration-inventory.mdfinal 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-consolean einer Einzelstelle (NICHT global) — als Notlösung wenn ein Hotfix dringendconsole.errorbraucht (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 neuenwebLogger-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. errals Pino-Convention für Error-Objekte — Pino'serr-Serializer redactet automatischerror.config.headers.authorizationetc. (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 derapps/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-reviewernach 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:
- Branch
agent/phase-02-loggingpushen → Coolify-Auto-Deploy auf Staging - 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":"..."} - Ein paar User-Flows triggern (Login, Email senden, AI-Call):
- Keine
console.*-Output mehr - Keine Klartext-Passwords/-Tokens
- AI-Prompts nur als Hash + Length
- Keine
- 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)
- 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: ESLintno-console: errorenforced 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.)