Merge pull request #4824 from Dokploy/fix/log-viewer-false-error-classification

fix(logs): don't classify explicit non-error key/values as error
This commit is contained in:
Mauricio Siu
2026-08-06 13:55:19 -06:00
committed by GitHub
2 changed files with 62 additions and 10 deletions
@@ -0,0 +1,42 @@
import { getLogType } from "@/components/dashboard/docker/logs/utils";
import { expect, test } from "vitest";
test("classifies real failures as error", () => {
expect(getLogType("Error: connection refused at db:5432").type).toBe("error");
expect(getLogType("[ERROR] something went wrong").type).toBe("error");
expect(getLogType("Deployment failed").type).toBe("error");
expect(
getLogType(
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326ms", failed: true, skipped: false, error: exit code 1',
).type,
).toBe("error");
});
test("does not classify explicit non-error key/values as error (#4538)", () => {
// ofelia job-completion summary for a successful run
expect(
getLogType(
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326.16795ms", failed: false, skipped: false, error: none',
).type,
).not.toBe("error");
expect(getLogType("request done, error: null").type).not.toBe("error");
expect(getLogType("checks passed, failures=0").type).not.toBe("error");
expect(getLogType('shutdown clean, error=""').type).not.toBe("error");
expect(getLogType('job done failed=false error=""').type).not.toBe("error");
});
test("keeps errors whose value merely starts with a no-error word", () => {
expect(getLogType("connect failed: no route to host").type).toBe("error");
expect(getLogType("connection failed: no such host").type).toBe("error");
expect(
getLogType("error: none of the configured nodes are available").type,
).toBe("error");
expect(getLogType("error: nil pointer dereference").type).toBe("error");
expect(getLogType("request failed: 0 bytes received").type).toBe("error");
});
test("keeps statusCode-based classification", () => {
expect(getLogType('{"statusCode": "500"}').type).toBe("error");
expect(getLogType('{"statusCode": "204"}').type).toBe("success");
});
@@ -97,17 +97,27 @@ export const getLogType = (message: string): LogStyle => {
return LOG_STYLES.info;
}
// Key/value pairs that explicitly report a non-error (e.g. "error: none",
// "failed: false") must not trigger the error keyword patterns below
const nonErrorColonPairs =
/\b(?:error|err|errors|failed|failure|failures)\s*:\s*(?:none|null|nil|false|0|no|-|""|'')(?=[,;.)\]]|$)/gi;
const nonErrorLogfmtPairs =
/\b(?:error|err|errors|failed|failure|failures)\s*=\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi;
const errorScope = lowerMessage
.replace(nonErrorColonPairs, "")
.replace(nonErrorLogfmtPairs, "");
if (
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
/(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
/\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
/(?:^|\s)(?:error|err):?\s/i.test(errorScope) ||
/\b(?:exception|failed|failure)\b/i.test(errorScope) ||
/(?:stack\s?trace):\s*$/i.test(errorScope) ||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(errorScope) ||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(errorScope) ||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(errorScope) ||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(errorScope) ||
/\[(?:error|err|fatal)\]/i.test(errorScope) ||
/\b(?:crash|critical|fatal)\b/i.test(errorScope) ||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(errorScope)
) {
return LOG_STYLES.error;
}