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
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:
@@ -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] ---")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user