Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune trace au démarrage à part un console.log ad hoc, et surtout aucune trace des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en production n'aurait laissé aucune trace exploitable. - LoggerService (apps/api/src/lib/logger.service.ts) — classe (public debug/info/warn/error, private emit), même convention que ErrorHandlerService (packages/error-tools) : instance unique partagée exportée (`export const logger = new LoggerService()`). Émet une ligne JSON structurée par appel (timestamp/level/message + meta), filtrée par seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests pour ne pas alourdir la sortie de Mocha, info+ en production). Seul endroit du code autorisé à toucher `console` directement (biome-ignore justifié), toujours via une méthode nommée — jamais un console.log nu. - requestLogger (middlewares/request-logger.ts) — une ligne par requête terminée (méthode/chemin/statut/durée), montée en tout premier dans app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le pipeline. Niveau déduit du statut (info/warn/error). - errorLogger (middlewares/error-logger.ts) — monté juste avant createErrorMiddleware : réutilise errorHandlerService.handle() (pur/ sans effet de bord) pour classifier l'erreur avant que la vraie réponse ne soit construite, log en warn les 4xx routiniers (validation, 404, 401...) et en error les 5xx/exceptions non prévues (avec la stack). - error-handler.service.ts : retire le `console.error(error)` ad hoc de fromUnknownError — errorLogger voit désormais chaque erreur avant que ce service ne la mappe, donc ce console.error faisait doublon (et loggait en texte brut, pas en JSON structuré). - server.ts : le console.log de démarrage passe par logger.info. Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api + curl) — logs JSON corrects pour un 200, un 404, un 401. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import { expect } from "chai";
|
|
import { LoggerService, logger, minLevelFor } from "../src/lib/logger.service.js";
|
|
|
|
/**
|
|
* Stubs one `console` method for one test, capturing every call instead of
|
|
* actually writing to stdout/stderr — same "stub the one thing that
|
|
* touches the outside world" approach `the-meal-db.test.ts` uses for
|
|
* `fetch`. Restored by the caller (`afterEach` below) regardless of which
|
|
* test used it.
|
|
*/
|
|
function stubConsoleMethod(method: "debug" | "info" | "warn" | "error") {
|
|
const calls: unknown[][] = [];
|
|
// biome-ignore lint/suspicious/noConsole: this *is* the console-stubbing helper — it has to read the real method to be able to restore it. Never calls it for real.
|
|
const original = console[method];
|
|
console[method] = (...args: unknown[]) => {
|
|
calls.push(args);
|
|
};
|
|
return {
|
|
calls,
|
|
restore: () => (console[method] = original),
|
|
};
|
|
}
|
|
|
|
describe("logger.service", () => {
|
|
describe("minLevelFor", () => {
|
|
it("only lets warn/error through in test — Mocha's own output shouldn't get any noisier", () => {
|
|
expect(minLevelFor("test")).to.equal("warn");
|
|
});
|
|
|
|
it("only lets info/warn/error through in production — debug is too verbose to ship", () => {
|
|
expect(minLevelFor("production")).to.equal("info");
|
|
});
|
|
|
|
it("lets everything through, including debug, in development", () => {
|
|
expect(minLevelFor("development")).to.equal("debug");
|
|
});
|
|
});
|
|
|
|
describe("logger", () => {
|
|
let stub: ReturnType<typeof stubConsoleMethod>;
|
|
|
|
afterEach(() => {
|
|
stub?.restore();
|
|
});
|
|
|
|
it("emits a warn line as a single JSON object via console.warn, with a timestamp/level/message and any extra meta merged in", () => {
|
|
stub = stubConsoleMethod("warn");
|
|
|
|
logger.warn("Something routine failed", { status: 404, code: 4049 });
|
|
|
|
expect(stub.calls).to.have.length(1);
|
|
const [line] = stub.calls[0];
|
|
const parsed = JSON.parse(line as string);
|
|
expect(parsed.level).to.equal("warn");
|
|
expect(parsed.message).to.equal("Something routine failed");
|
|
expect(parsed.status).to.equal(404);
|
|
expect(parsed.code).to.equal(4049);
|
|
expect(new Date(parsed.timestamp).toString()).to.not.equal("Invalid Date");
|
|
});
|
|
|
|
it("emits an error line via console.error", () => {
|
|
stub = stubConsoleMethod("error");
|
|
|
|
logger.error("Something broke");
|
|
|
|
expect(stub.calls).to.have.length(1);
|
|
const parsed = JSON.parse(stub.calls[0][0] as string);
|
|
expect(parsed.level).to.equal("error");
|
|
});
|
|
|
|
it('drops debug/info under the test suite\'s own NODE_ENV=test (minLevelFor("test") === "warn")', () => {
|
|
const debugStub = stubConsoleMethod("debug");
|
|
const infoStub = stubConsoleMethod("info");
|
|
|
|
logger.debug("Should not appear");
|
|
logger.info("Should not appear either");
|
|
|
|
expect(debugStub.calls).to.have.length(0);
|
|
expect(infoStub.calls).to.have.length(0);
|
|
debugStub.restore();
|
|
infoStub.restore();
|
|
});
|
|
|
|
it("never lets meta override the line's own timestamp/level/message keys", () => {
|
|
stub = stubConsoleMethod("warn");
|
|
|
|
logger.warn("Real message", { message: "spoofed", level: "spoofed", timestamp: "spoofed" });
|
|
|
|
const parsed = JSON.parse(stub.calls[0][0] as string);
|
|
expect(parsed.message).to.equal("Real message");
|
|
expect(parsed.level).to.equal("warn");
|
|
expect(new Date(parsed.timestamp).toString()).to.not.equal("Invalid Date");
|
|
});
|
|
|
|
it('a separate instance constructed with nodeEnv="development" lets debug through, independently of the shared logger\'s own NODE_ENV=test threshold', () => {
|
|
const debugStub = stubConsoleMethod("debug");
|
|
const devLogger = new LoggerService("development");
|
|
|
|
devLogger.debug("Visible in dev");
|
|
|
|
expect(debugStub.calls).to.have.length(1);
|
|
debugStub.restore();
|
|
});
|
|
});
|
|
});
|