2
0

feat(api): add log level filtering to job logs endpoint
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Failing after 19s
Build and Release / Lint (push) Failing after 48s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m22s

Introduce a new 'level' parameter (errors/warnings/all) to replace the deprecated 'errors_only' boolean. This provides more granular control over log filtering with three levels: errors only, errors+warnings, or full logs. Maintains backward compatibility with the old parameter while defaulting to 'errors' for failed jobs and 'all' for successful ones.
This commit is contained in:
2026-01-26 10:53:41 -05:00
parent 931bcfd7ee
commit a99a5ce168

View File

@@ -202,13 +202,18 @@ var mcpTools = []MCPTool{
"type": "integer",
"description": "The job ID",
},
"level": map[string]any{
"type": "string",
"enum": []string{"errors", "warnings", "all"},
"description": "Log filter level: 'errors' returns only error lines, 'warnings' returns errors and warnings, 'all' returns full logs (default: 'errors' for failed jobs, 'all' otherwise)",
},
"errors_only": map[string]any{
"type": "boolean",
"description": "If true, only return error-related log lines with context (default: true for failed jobs)",
"description": "Deprecated: use 'level' instead. If true, equivalent to level='errors'",
},
"context_lines": map[string]any{
"type": "integer",
"description": "Number of lines before/after each error to include (default: 5)",
"description": "Number of lines before/after each matched line to include (default: 5)",
},
},
"required": []string{"owner", "repo", "job_id"},
@@ -979,11 +984,30 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
}, nil
}
// Determine if we should extract errors only
// Default to errors_only=true for failed jobs
errorsOnly := job.Status.String() == "failure"
if val, ok := args["errors_only"].(bool); ok {
errorsOnly = val
// Determine log filter level
// Default to "errors" for failed jobs, "all" otherwise
logLevel := "all"
if job.Status.String() == "failure" {
logLevel = "errors"
}
// New "level" parameter takes priority
if val, ok := args["level"].(string); ok {
switch val {
case "errors", "warnings", "all":
logLevel = val
}
}
// Backward compat: errors_only overrides if level was not explicitly set
if _, hasLevel := args["level"].(string); !hasLevel {
if val, ok := args["errors_only"].(bool); ok {
if val {
logLevel = "errors"
} else {
logLevel = "all"
}
}
}
contextLines := 5
@@ -991,6 +1015,8 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
contextLines = int(val)
}
filtering := logLevel != "all"
// Get steps for this task
steps := actions.FullSteps(task)
@@ -1015,16 +1041,22 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
allLines = append(allLines, row.Content)
}
if errorsOnly {
// Extract only error-related lines with context
errorLines := extractErrorLines(allLines, contextLines)
if len(errorLines) > 0 {
stepInfo["lines"] = errorLines
stepInfo["line_count"] = len(errorLines)
if filtering {
// Build pattern list based on level
patterns := logErrorPatterns
if logLevel == "warnings" {
patterns = append(patterns, logWarningPatterns...)
}
// Extract matching lines with context
matchedLines := extractLogLines(allLines, patterns, contextLines)
if len(matchedLines) > 0 {
stepInfo["lines"] = matchedLines
stepInfo["line_count"] = len(matchedLines)
stepInfo["filtered"] = true
stepInfo["original_line_count"] = len(allLines)
} else if step.Status.String() == "failure" {
// For failed steps with no detected errors, include last N lines
// For failed steps with no detected matches, include last N lines
lastN := min(50, len(allLines))
stepInfo["lines"] = allLines[len(allLines)-lastN:]
stepInfo["line_count"] = lastN
@@ -1032,7 +1064,7 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
stepInfo["original_line_count"] = len(allLines)
stepInfo["note"] = "No specific errors detected, showing last 50 lines"
}
// Skip successful steps when filtering for errors
// Skip successful steps when filtering
} else {
stepInfo["lines"] = allLines
stepInfo["line_count"] = len(allLines)
@@ -1041,7 +1073,7 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
}
// Only include steps that have content when filtering
if !errorsOnly || stepInfo["lines"] != nil || step.Status.String() == "failure" {
if !filtering || stepInfo["lines"] != nil || step.Status.String() == "failure" {
stepLogs = append(stepLogs, stepInfo)
}
}
@@ -1054,48 +1086,61 @@ func toolGetJobLogs(ctx *context_service.APIContext, args map[string]any) (any,
"log_expired": task.LogExpired,
"steps": stepLogs,
"step_count": len(stepLogs),
"errors_only": errorsOnly,
"level": logLevel,
}, nil
}
// extractErrorLines finds error-related lines and includes context around them
func extractErrorLines(lines []string, contextLines int) []string {
// Patterns that indicate errors
errorPatterns := []string{
"error:", "Error:", "ERROR:", "error[",
"failed", "Failed", "FAILED",
"fatal:", "Fatal:", "FATAL:",
"panic:", "PANIC:",
"exception:", "Exception:",
"cannot ", "Cannot ",
"undefined:", "Undefined:",
"not found", "Not found", "NOT FOUND",
"permission denied", "Permission denied",
"exit code", "exit status",
"--- FAIL:",
"SIGILL", "SIGSEGV", "SIGABRT", "SIGKILL",
"Build FAILED",
"error MSB", "error CS", "error TS",
"npm ERR!",
"go: ", // go module errors
// logErrorPatterns matches lines that indicate errors (strict).
var logErrorPatterns = []string{
"error:", "error[",
"failed", "FAILED",
"fatal:", "FATAL:",
"panic:", "PANIC:",
"exception:",
"cannot ",
"undefined:",
"permission denied",
"exit code", "exit status",
"--- FAIL:",
"SIGILL", "SIGSEGV", "SIGABRT", "SIGKILL",
"Build FAILED",
"error MSB", "error CS", "error TS",
"npm ERR!",
}
// logWarningPatterns matches lines that indicate warnings (used with "warnings" level).
var logWarningPatterns = []string{
"warning:", "warn:",
"warning MSB", "warning CS", "warning TS",
"deprecated",
"not found",
"go: ", // go module messages (downloads, version info)
}
// extractLogLines finds lines matching the given patterns and includes context around them.
func extractLogLines(lines, patterns []string, contextLines int) []string {
// Pre-lowercase the patterns
lowerPatterns := make([]string, len(patterns))
for i, p := range patterns {
lowerPatterns[i] = strings.ToLower(p)
}
// Find indices of error lines
errorIndices := make(map[int]bool)
// Find indices of matching lines
matchIndices := make(map[int]bool)
for i, line := range lines {
lineLower := strings.ToLower(line)
for _, pattern := range errorPatterns {
if strings.Contains(lineLower, strings.ToLower(pattern)) {
for _, pattern := range lowerPatterns {
if strings.Contains(lineLower, pattern) {
// Mark this line and surrounding context
for j := max(0, i-contextLines); j <= min(len(lines)-1, i+contextLines); j++ {
errorIndices[j] = true
matchIndices[j] = true
}
break
}
}
}
if len(errorIndices) == 0 {
if len(matchIndices) == 0 {
return nil
}
@@ -1103,7 +1148,7 @@ func extractErrorLines(lines []string, contextLines int) []string {
result := make([]string, 0)
lastIdx := -1
for i, line := range lines {
if errorIndices[i] {
if matchIndices[i] {
if lastIdx >= 0 && i > lastIdx+1 {
result = append(result, "--- [skipped lines] ---")
}