Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 91 additions & 6 deletions pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
TokenUsage int
EstimatedCost float64
Turns int
ErrorCount int
WarningCount int
LogsPath string
}

Expand Down Expand Up @@ -86,6 +88,52 @@
// ErrNoArtifacts indicates that a workflow run has no artifacts
var ErrNoArtifacts = errors.New("no artifacts found for this run")

// fetchJobStatuses gets job information for a workflow run and counts failed jobs
func fetchJobStatuses(runID int64, verbose bool) (int, error) {
args := []string{"api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion}"}

Check failure on line 94 in pkg/cli/logs.go

View workflow job for this annotation

GitHub Actions / Lint Code

File is not properly formatted (gofmt)

Check failure on line 94 in pkg/cli/logs.go

View workflow job for this annotation

GitHub Actions / Lint Code

File is not properly formatted (gofmt)
if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Fetching job statuses for run %d", runID)))
}

cmd := exec.Command("gh", args...)
output, err := cmd.CombinedOutput()
if err != nil {
if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Failed to fetch job statuses for run %d: %v", runID, err)))
}
// Don't fail the entire operation if we can't get job info
return 0, nil
}

// Parse each line as a separate JSON object
failedJobs := 0
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}

var job JobInfo
if err := json.Unmarshal([]byte(line), &job); err != nil {
if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Failed to parse job info: %s", line)))
}
continue
}

// Count jobs with failure conclusions as errors
if job.Conclusion == "failure" || job.Conclusion == "cancelled" || job.Conclusion == "timed_out" {
failedJobs++
if verbose {
fmt.Println(console.FormatVerboseMessage(fmt.Sprintf("Found failed job '%s' with conclusion '%s'", job.Name, job.Conclusion)))
}
}
}

return failedJobs, nil
}

// DownloadResult represents the result of downloading artifacts for a single run
type DownloadResult struct {
Run WorkflowRun
Expand All @@ -98,6 +146,13 @@
LogsPath string
}

// JobInfo represents basic information about a workflow job
type JobInfo struct {
Name string `json:"name"`
Status string `json:"status"`
Conclusion string `json:"conclusion"`
}

// AwInfo represents the structure of aw_info.json files
type AwInfo struct {
EngineID string `json:"engine_id"`
Expand Down Expand Up @@ -409,8 +464,18 @@
run.TokenUsage = result.Metrics.TokenUsage
run.EstimatedCost = result.Metrics.EstimatedCost
run.Turns = result.Metrics.Turns
run.ErrorCount = result.Metrics.ErrorCount
run.WarningCount = result.Metrics.WarningCount
run.LogsPath = result.LogsPath

// Add failed jobs to error count
if failedJobCount, err := fetchJobStatuses(run.DatabaseID, verbose); err == nil {
run.ErrorCount += failedJobCount
if verbose && failedJobCount > 0 {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Added %d failed jobs to error count for run %d", failedJobCount, run.DatabaseID)))
}
}

// Store access analysis for later display (we'll access it via the result)
// No need to modify the WorkflowRun struct for this

Expand Down Expand Up @@ -972,43 +1037,53 @@
}

// Prepare table data
headers := []string{"Run ID", "Workflow", "Status", "Duration", "Tokens", "Cost ($)", "Turns", "Created", "Logs Path"}
headers := []string{"Run ID", "Workflow", "Status", "Duration", "Tokens", "Cost ($)", "Turns", "Errors", "Warnings", "Created", "Logs Path"}
var rows [][]string

var totalTokens int
var totalCost float64
var totalDuration time.Duration
var totalTurns int
var totalErrors int
var totalWarnings int

for _, run := range runs {
// Format duration
durationStr := "N/A"
durationStr := ""
if run.Duration > 0 {
durationStr = formatDuration(run.Duration)
totalDuration += run.Duration
}

// Format cost
costStr := "N/A"
costStr := ""
if run.EstimatedCost > 0 {
costStr = fmt.Sprintf("%.3f", run.EstimatedCost)
totalCost += run.EstimatedCost
}

// Format tokens
tokensStr := "N/A"
tokensStr := ""
if run.TokenUsage > 0 {
tokensStr = formatNumber(run.TokenUsage)
totalTokens += run.TokenUsage
}

// Format turns
turnsStr := "N/A"
turnsStr := ""
if run.Turns > 0 {
turnsStr = fmt.Sprintf("%d", run.Turns)
totalTurns += run.Turns
}

// Format errors
errorsStr := fmt.Sprintf("%d", run.ErrorCount)
totalErrors += run.ErrorCount

// Format warnings
warningsStr := fmt.Sprintf("%d", run.WarningCount)
totalWarnings += run.WarningCount

// Truncate workflow name if too long
workflowName := run.WorkflowName
if len(workflowName) > 20 {
Expand All @@ -1018,14 +1093,22 @@
// Format relative path
relPath, _ := filepath.Rel(".", run.LogsPath)

// Format status - show conclusion directly for completed runs
statusStr := run.Status
if run.Status == "completed" && run.Conclusion != "" {
statusStr = run.Conclusion
}

row := []string{
fmt.Sprintf("%d", run.DatabaseID),
workflowName,
run.Status,
statusStr,
durationStr,
tokensStr,
costStr,
turnsStr,
errorsStr,
warningsStr,
run.CreatedAt.Format("2006-01-02"),
relPath,
}
Expand All @@ -1041,6 +1124,8 @@
formatNumber(totalTokens),
fmt.Sprintf("%.3f", totalCost),
fmt.Sprintf("%d", totalTurns),
fmt.Sprintf("%d", totalErrors),
fmt.Sprintf("%d", totalWarnings),
"",
"",
}
Expand Down
17 changes: 9 additions & 8 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,7 @@ func (e *CodexEngine) ParseLogMetrics(logContent string, verbose bool) LogMetric
totalTokenUsage += tokenUsage
}

// Count errors and warnings
lowerLine := strings.ToLower(line)
if strings.Contains(lowerLine, "error") {
metrics.ErrorCount++
}
if strings.Contains(lowerLine, "warning") {
metrics.WarningCount++
}
// Basic processing - error/warning counting moved to end of function
}

// Add final sequence if any
Expand All @@ -348,6 +341,14 @@ func (e *CodexEngine) ParseLogMetrics(logContent string, verbose bool) LogMetric
return metrics.ToolCalls[i].Name < metrics.ToolCalls[j].Name
})

// Count errors and warnings using pattern matching for better accuracy
errorPatterns := e.GetErrorPatterns()
if len(errorPatterns) > 0 {
counts := CountErrorsAndWarningsWithPatterns(logContent, errorPatterns)
metrics.ErrorCount = counts.ErrorCount
metrics.WarningCount = counts.WarningCount
}

return metrics
}

Expand Down
17 changes: 9 additions & 8 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,14 +393,7 @@ func (e *CopilotEngine) ParseLogMetrics(logContent string, verbose bool) LogMetr
}
}

// Count errors and warnings
lowerLine := strings.ToLower(line)
if strings.Contains(lowerLine, "error") {
metrics.ErrorCount++
}
if strings.Contains(lowerLine, "warning") {
metrics.WarningCount++
}
// Basic processing - error/warning counting moved to end of function
}

// Add final sequence if any
Expand All @@ -421,6 +414,14 @@ func (e *CopilotEngine) ParseLogMetrics(logContent string, verbose bool) LogMetr
return metrics.ToolCalls[i].Name < metrics.ToolCalls[j].Name
})

// Count errors and warnings using pattern matching for better accuracy
errorPatterns := e.GetErrorPatterns()
if len(errorPatterns) > 0 {
counts := CountErrorsAndWarningsWithPatterns(logContent, errorPatterns)
metrics.ErrorCount = counts.ErrorCount
metrics.WarningCount = counts.WarningCount
}

return metrics
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/custom_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,16 @@ func (e *CustomEngine) ParseLogMetrics(logContent string, verbose bool) LogMetri
continue
}

// Custom engine continues with basic processing
}

// Count errors and warnings - Custom engine doesn't have its own patterns,
// so use simple string matching as fallback
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}

// Count errors and warnings
lowerLine := strings.ToLower(line)
if strings.Contains(lowerLine, "error") {
Expand Down
Loading
Loading