Structured Logging
Replace console.* with structured, maskable, JSON-in-production logging
Gap severity: HIGH — still open
19 console.* calls remain in backend production source (plan phase 6 / Backend quality §5 tracks the replacement). The convention already forbids them — this page keeps the target pattern.
The cal.diy pattern
packages/lib/logger.ts exports a tslog-based logger:
const logger = new Logger({
minLevel: parseInt(process.env.NEXT_PUBLIC_LOGGER_LEVEL || "4"),
maskValuesOfKeys: ["password", "passwordConfirmation", "credentials", "credential"],
type: IS_PRODUCTION ? "json" : "pretty", // JSON in prod for log aggregators
stylePrettyLogs: !IS_PRODUCTION,
});
export default logger;- JSON output in production → ingested by Datadog/Axiom/Loki with structured fields
- Automatically masks sensitive keys (password, credentials)
- Imported by 82 files across their monorepo
What to implement
Backend (apps/back)
Each service gets its own Nest Logger instance:
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
async login(...) {
this.logger.log(`Login attempt (userId=${user.id})`);
// instead of: console.log("login", email)
}
}Conventions: log ids, not emails (PII in prod logs); logger.error(message, error.stack) for failures; no emoji. For JSON output in production, configure Nest's built-in logger or add @nestjs/winston:
app.useLogger(
IS_PRODUCTION
? new WinstonLogger({ format: winston.format.json() })
: new Logger()
);Frontend (apps/web)
Client-side error paths should surface via toast (the app-wide pattern), not console.error. If a client logger is ever needed, mirror the tslog pattern with maskValuesOfKeys: ["password", "token", "secret"].
CI gate
- name: No console.log in source
run: |
! grep -rn "console\.\(log\|error\|warn\)" \
apps/back/src apps/web/app apps/web/features \
--include="*.ts" --include="*.tsx" \
| grep -v "seed\|\.spec\.\|\.test\."ESLint rule
"no-console": ["error", { allow: ["warn"] }]Add both gates after the phase-6 sweep so they enforce, not block.