fix(logs): respect log level explicitly declared by structured loggers

This commit is contained in:
fcsouza
2026-06-12 10:31:57 -03:00
parent 1f4f94042f
commit dc2fc40c38
2 changed files with 168 additions and 0 deletions
@@ -0,0 +1,108 @@
import { describe, expect, test } from "vitest";
import { getLogType } from "@/components/dashboard/docker/logs/utils";
describe("getLogType", () => {
describe("explicit level declared by structured loggers", () => {
test("JSON string levels (pino, winston, zap)", () => {
expect(getLogType('{"level":"trace","msg":"x"}').type).toBe("debug");
expect(getLogType('{"level":"debug","msg":"x"}').type).toBe("debug");
expect(getLogType('{"level":"info","msg":"x"}').type).toBe("info");
expect(getLogType('{"level":"warn","msg":"x"}').type).toBe("warning");
expect(getLogType('{"level":"warning","msg":"x"}').type).toBe("warning");
expect(getLogType('{"level":"error","msg":"x"}').type).toBe("error");
expect(getLogType('{"level":"fatal","msg":"x"}').type).toBe("error");
});
test("JSON numeric levels (pino, bunyan)", () => {
expect(getLogType('{"level":20,"msg":"x"}').type).toBe("debug");
expect(getLogType('{"level":30,"msg":"x"}').type).toBe("info");
expect(getLogType('{"level":40,"msg":"x"}').type).toBe("warning");
expect(getLogType('{"level":50,"msg":"x"}').type).toBe("error");
expect(getLogType('{"level":60,"msg":"x"}').type).toBe("error");
});
test("syslog/GELF numeric levels", () => {
expect(getLogType('{"level":3,"msg":"x"}').type).toBe("error");
expect(getLogType('{"level":4,"msg":"x"}').type).toBe("warning");
expect(getLogType('{"level":6,"msg":"x"}').type).toBe("info");
expect(getLogType('{"level":7,"msg":"x"}').type).toBe("debug");
});
test("GCP-style severity and ECS log.level", () => {
expect(getLogType('{"severity":"ERROR","message":"x"}').type).toBe(
"error",
);
expect(getLogType('{"severity":"WARNING","message":"x"}').type).toBe(
"warning",
);
expect(getLogType('{"log.level":"error","message":"x"}').type).toBe(
"error",
);
});
test("logfmt levels", () => {
expect(
getLogType('ts=2026-06-12T10:00:00Z level=error msg="boom"').type,
).toBe("error");
expect(
getLogType('ts=2026-06-12T10:00:00Z level=info msg="ok"').type,
).toBe("info");
expect(getLogType("level=warn msg=careful").type).toBe("warning");
});
test("declared level wins over keywords in the message (#4589, #1996)", () => {
// "version"/"GET" in this pino line would otherwise match the debug keywords
const pinoError =
'{"level":"error","version":"72b4450","method":"GET","path":"/api/campaigns","err":{"type":"ForbiddenError","stack":"ForbiddenError: at requireRole (/app/src/plugins/campaign.plugin.ts:166:15)"},"msg":"Forbidden"}';
expect(getLogType(pinoError).type).toBe("error");
// info line containing error-like keywords (#4589)
expect(
getLogType(
'{"level":"info","msg":"Failed to open mempool file. Continuing anyway."}',
).type,
).toBe("info");
// successful job summary with "failed: false, error: none" (#4538)
expect(
getLogType(
'level=info msg="Finished job, failed: false, skipped: false, error: none"',
).type,
).toBe("info");
});
test("declared level wins over statusCode", () => {
expect(getLogType('{"level":"info","statusCode":500}').type).toBe("info");
});
test("unknown level names fall back to keyword detection", () => {
expect(
getLogType('{"level":"verbose","msg":"connection failed"}').type,
).toBe("error");
});
test("env-var-like text is not treated as logfmt level", () => {
expect(getLogType("LOG_LEVEL=error NODE_ENV=production").type).not.toBe(
"error",
);
});
});
describe("fallback detection for unstructured logs (unchanged)", () => {
test("statusCode classification", () => {
expect(getLogType('{"statusCode":500,"msg":"x"}').type).toBe("error");
expect(getLogType('{"statusCode":404,"msg":"x"}').type).toBe("warning");
expect(getLogType('{"statusCode":200,"msg":"x"}').type).toBe("success");
});
test("keyword classification", () => {
expect(getLogType("error: something broke").type).toBe("error");
expect(getLogType("warning: disk almost full").type).toBe("warning");
expect(getLogType("Server listening on port 8080").type).toBe("success");
});
test("defaults to info", () => {
expect(getLogType("hello world").type).toBe("info");
});
});
});
@@ -72,8 +72,68 @@ export function parseLogs(logString: string): LogLine[] {
.filter((log) => log !== null);
}
const LEVEL_NAME_TO_TYPE: Record<string, LogType> = {
trace: "debug",
debug: "debug",
info: "info",
information: "info",
notice: "info",
warn: "warning",
warning: "warning",
error: "error",
err: "error",
fatal: "error",
critical: "error",
panic: "error",
alert: "error",
emergency: "error",
};
const numericLevelToType = (level: number): LogType => {
// pino/bunyan scale: 10=trace 20=debug 30=info 40=warn 50=error 60=fatal
if (level >= 50) return "error";
if (level >= 40) return "warning";
if (level >= 30) return "info";
if (level >= 10) return "debug";
// syslog/GELF scale: 0=emergency ... 7=debug
if (level <= 3) return "error";
if (level === 4) return "warning";
if (level <= 6) return "info";
return "debug";
};
// Extract the log level explicitly declared by structured loggers
// (pino, bunyan, winston, zap, slog, logfmt, GCP severity)
const getExplicitLevelType = (message: string): LogType | null => {
// JSON string levels: {"level":"error"} / {"severity":"ERROR"} / {"log.level":"warn"}
const jsonStringMatch = message.match(
/"(?:level|severity|log\.level|loglevel)"\s*:\s*"([a-z]+)"/i,
);
if (jsonStringMatch?.[1]) {
return LEVEL_NAME_TO_TYPE[jsonStringMatch[1].toLowerCase()] ?? null;
}
// JSON numeric levels: {"level":50}
const jsonNumericMatch = message.match(/"level"\s*:\s*(\d{1,2})\b/);
if (jsonNumericMatch?.[1]) {
return numericLevelToType(Number(jsonNumericMatch[1]));
}
// logfmt: level=error
const logfmtMatch = message.match(/(?:^|\s)(?:level|severity)=([a-z]+)\b/i);
if (logfmtMatch?.[1]) {
return LEVEL_NAME_TO_TYPE[logfmtMatch[1].toLowerCase()] ?? null;
}
return null;
};
// Detect log type based on message content
export const getLogType = (message: string): LogStyle => {
// A level explicitly declared by the logger wins over any inference
const explicitType = getExplicitLevelType(message);
if (explicitType) return LOG_STYLES[explicitType];
// Detect HTTP statusCode
const statusMatch = message.match(/"statusCode"\s*:\s*"?(\d{3})"?/);