Skip to content

Conversation

@Akinator31
Copy link

What is this PR about?

This PR improves the log type detection system in the Docker logs viewer to significantly reduce
false positives. The previous implementation used broad regex patterns that incorrectly flagged
informational messages as errors (e.g., "Failed=0" or "0 builds failed").

Technical Approach

The new implementation uses a confidence-based scoring system with three levels of pattern
matching:

Level 1 - Structured Patterns (Score: 100pts)

  • Highest confidence, no false positives
  • Examples: [ERROR], ERROR:, level=error, severity=warning
  • Emoji indicators: ⚠️ (warning), ❌ (error), ✅ (success)

Level 2 - Contextual Patterns (Score: 50-70pts)

  • Requires full phrases with context
  • Examples: "failed to connect", "uncaught exception", "Token exchange failed with status: 400"
  • HTTP error codes: 4xx and 5xx status codes
  • Stack trace detection: at Object.<anonymous> (/path/file.js:123:45)

Level 3 - Keyword Patterns (Score: 20-30pts)

  • Simple keywords with negative context detection
  • Only applied if keyword is NOT in phrases like: "did not fail", "without errors", "if failed"
    (conditional)
  • Examples: "exception", "crash", "deprecated" (when used standalone)

How Scoring Works

  1. Multiple patterns can match - scores accumulate for each log type
  2. Highest score wins - the log type with the most points is selected
  3. Defaults to info - if no patterns match, the log is marked as info

Example:

  • "⚠️ Token exchange failed with status: 400" scores:

    • Warning: 100pts (⚠️ emoji)
    • Error: 135pts (65pts "failed with" + 70pts "status: 400")
    • Result: Error wins
  • "Failed=0 Scanned=1" scores:

    • No patterns match "Failed=0" as it's not a contextual error phrase
    • Result: Info (default)

Key Improvements

  • Emoji support: Now detects ⚠️, ❌, ✅ indicators
  • HTTP error codes: Properly identifies 4xx/5xx status codes
  • Negative context detection: Prevents false positives from phrases like "did not fail"
  • Improved contextual matching: "X failed with Y" vs "Failed=0" are now distinguished
  • Comprehensive tests: 25 unit tests covering all detection scenarios

This is my proposed approach, but I'm very open to alternative implementations. Feel free to share any suggestions or concerns—I'm happy to adjust based on your feedback.

Checklist

Before submitting this PR, please make sure that:

Issues related (if applicable)

fixes #1996

Screenshots (if applicable)

Before: Logs like time="2025-11-20T08:19:13Z" level=info msg="Session done" Failed=0 were
incorrectly marked as errors

image image

After: These logs are now correctly displayed as info, while real errors like ⚠️ Token exchange failed with status: 400 Bad Request are properly detected

image image

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the log type detection system to use a confidence-based scoring mechanism, significantly reducing false positives when classifying Docker log messages. The previous implementation incorrectly flagged informational messages containing words like "failed" (e.g., "Failed=0") as errors.

Key changes:

  • Introduced a three-tier pattern matching system (structured, contextual, keyword) with weighted scores
  • Added negative context detection to prevent false positives from phrases like "did not fail"
  • Enhanced pattern coverage including emojis, HTTP status codes, and stack traces

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
apps/dokploy/components/dashboard/docker/logs/utils.ts Replaced simple regex checks with a scoring-based classification system using structured, contextual, and keyword pattern arrays
apps/dokploy/test/logs/log-type.test.ts Added comprehensive test suite with 25 test cases covering all log type detection scenarios including false positive prevention

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 81 to 96
const isNegativeContext = (message: string, keyword: string): boolean => {
const lowerMessage = message.toLowerCase();
const lowerKeyword = keyword.toLowerCase();

const negativePatterns = [
`(?:not|no|without|won't|didn't|doesn't|never)\\s+(?:\\w+\\s+){0,3}${lowerKeyword}`,
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern allows up to 3 words between negation and keyword ({0,3}), but this threshold appears arbitrary. Consider documenting why 3 words was chosen, or extracting it as a named constant (e.g., MAX_WORD_DISTANCE = 3) to make the intent clearer and the value easier to adjust if needed.

Suggested change
const isNegativeContext = (message: string, keyword: string): boolean => {
const lowerMessage = message.toLowerCase();
const lowerKeyword = keyword.toLowerCase();
const negativePatterns = [
`(?:not|no|without|won't|didn't|doesn't|never)\\s+(?:\\w+\\s+){0,3}${lowerKeyword}`,
// Allow a small number of intermediate words between a negation term and the keyword
// (e.g. "not at all really error"); 3 was chosen as a balance between recall and noise.
const MAX_NEGATION_KEYWORD_WORD_DISTANCE = 3;
const isNegativeContext = (message: string, keyword: string): boolean => {
const lowerMessage = message.toLowerCase();
const lowerKeyword = keyword.toLowerCase();
const negativePatterns = [
`(?:not|no|without|won't|didn't|doesn't|never)\\s+(?:\\w+\\s+){0,${MAX_NEGATION_KEYWORD_WORD_DISTANCE}}${lowerKeyword}`,

Copilot uses AI. Check for mistakes.
score: 100,
type: "error",
},
{ pattern: /|🔴|🚨/i, score: 100, type: "error" },
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case-insensitive flag i is unnecessary for emoji patterns since emojis don't have case variants. Consider removing the flag for clarity and minor performance improvement.

Copilot uses AI. Check for mistakes.
{ pattern: /^(?:warn(?:ing)?):?\s/i, score: 100, type: "warning" },
{ pattern: /level=(?:warn(?:ing)?)/i, score: 100, type: "warning" },
{ pattern: /severity=(?:warn(?:ing)?)/i, score: 100, type: "warning" },
{ pattern: /||/i, score: 100, type: "warning" },
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case-insensitive flag i is unnecessary for emoji patterns since emojis don't have case variants. Consider removing the flag for clarity and minor performance improvement.

Copilot uses AI. Check for mistakes.
{ pattern: /level=(?:debug|trace)/i, score: 100, type: "debug" },

{ pattern: /\[(?:success|ok|done)\]/i, score: 100, type: "success" },
{ pattern: /|||\[ok\]/i, score: 100, type: "success" },
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case-insensitive flag i applies to the entire pattern, but is only meaningful for the \[ok\] portion. Consider splitting this into two patterns or documenting that the flag is intentionally applied to cover variations like [OK] and [Ok].

Suggested change
{ pattern: /|||\[ok\]/i, score: 100, type: "success" },
{ pattern: /||/, score: 100, type: "success" },
{ pattern: /\[ok\]/i, score: 100, type: "success" },

Copilot uses AI. Check for mistakes.
Comment on lines 300 to 303
for (const [type, score] of Object.entries(scores)) {
if (score > maxScore) {
maxScore = score;
detectedType = type as LogType;
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When multiple log types have the same score, the detected type depends on object enumeration order, which may be unpredictable. Consider adding explicit tie-breaking logic (e.g., prioritizing 'error' over 'warning') to ensure consistent behavior when scores are equal.

Suggested change
for (const [type, score] of Object.entries(scores)) {
if (score > maxScore) {
maxScore = score;
detectedType = type as LogType;
const LOG_TYPE_PRIORITY: Record<LogType, number> = {
error: 0,
warning: 1,
success: 2,
info: 3,
debug: 4,
};
for (const [type, score] of Object.entries(scores)) {
const currentType = type as LogType;
if (
score > maxScore ||
(score === maxScore &&
LOG_TYPE_PRIORITY[currentType] < LOG_TYPE_PRIORITY[detectedType])
) {
maxScore = score;
detectedType = currentType;

Copilot uses AI. Check for mistakes.
@Siumauricio
Copy link
Contributor

can you rsolve the conflicts? @Akinator31

@Akinator31 Akinator31 force-pushed the fix/log-type-detection branch from ee7ce57 to 669516d Compare December 21, 2025 10:10
@Akinator31
Copy link
Author

Rebased on canary and resolved conflicts @Siumauricio

Changes made:

  • Added explicit tie-breaking logic with LOG_TYPE_PRIORITY (error > warning > success > info > debug) to ensure deterministic behavior when scores are equal
  • Separated emoji patterns from text patterns (e.g., [ok]) for clarity
  • Fixed import order in test file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Log viewer incorrectly displaying error log level when error log level is not used

2 participants