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
5 changes: 5 additions & 0 deletions .changeset/patch-add-continuation-field-logs.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,28 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou
processedRuns[i].Run.MissingToolCount = len(processedRuns[i].MissingTools)
}

// Build continuation data if timeout was reached and there are processed runs
var continuation *ContinuationData
if timeoutReached && len(processedRuns) > 0 {
// Get the oldest run ID from processed runs to use as before_run_id for continuation
oldestRunID := processedRuns[len(processedRuns)-1].Run.DatabaseID

continuation = &ContinuationData{
Message: "Timeout reached. Use these parameters to continue fetching more logs.",
WorkflowName: workflowName,
Count: count,
StartDate: startDate,
EndDate: endDate,
Engine: engine,
Branch: branch,
AfterRunID: afterRunID,
BeforeRunID: oldestRunID, // Continue from where we left off
Timeout: timeout,
}
}

// Build structured logs data
logsData := buildLogsData(processedRuns, outputDir)
logsData := buildLogsData(processedRuns, outputDir, continuation)

// Render output based on format preference
if jsonOutput {
Expand Down
132 changes: 131 additions & 1 deletion pkg/cli/logs_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestBuildLogsData(t *testing.T) {
}

// Build logs data
logsData := buildLogsData(processedRuns, tmpDir)
logsData := buildLogsData(processedRuns, tmpDir, nil)

// Verify summary
if logsData.Summary.TotalRuns != 2 {
Expand Down Expand Up @@ -261,6 +261,136 @@ func TestBuildMissingToolsSummary(t *testing.T) {
}
}

// TestBuildLogsDataWithContinuation tests continuation field in logs data
func TestBuildLogsDataWithContinuation(t *testing.T) {
tmpDir := t.TempDir()

// Create sample processed runs
processedRuns := []ProcessedRun{
{
Run: WorkflowRun{
DatabaseID: 12345,
Number: 1,
WorkflowName: "Test Workflow",
Status: "completed",
Conclusion: "success",
CreatedAt: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
URL: "https://github.com/test/repo/actions/runs/12345",
LogsPath: filepath.Join(tmpDir, "run-12345"),
},
},
{
Run: WorkflowRun{
DatabaseID: 12344,
Number: 2,
WorkflowName: "Test Workflow",
Status: "completed",
Conclusion: "success",
CreatedAt: time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC),
URL: "https://github.com/test/repo/actions/runs/12344",
LogsPath: filepath.Join(tmpDir, "run-12344"),
},
},
}

// Create continuation data (simulating timeout scenario)
continuation := &ContinuationData{
Message: "Timeout reached. Use these parameters to continue fetching more logs.",
WorkflowName: "Test Workflow",
Count: 100,
StartDate: "2024-01-01",
EndDate: "2024-12-31",
Engine: "copilot",
Branch: "main",
AfterRunID: 0,
BeforeRunID: 12344, // Continue from the oldest run
Timeout: 50,
}

// Build logs data with continuation
logsData := buildLogsData(processedRuns, tmpDir, continuation)

// Verify continuation field is present
if logsData.Continuation == nil {
t.Fatal("Expected continuation field to be present, got nil")
}

// Verify continuation data
if logsData.Continuation.Message != "Timeout reached. Use these parameters to continue fetching more logs." {
t.Errorf("Expected continuation message, got '%s'", logsData.Continuation.Message)
}
if logsData.Continuation.WorkflowName != "Test Workflow" {
t.Errorf("Expected WorkflowName 'Test Workflow', got '%s'", logsData.Continuation.WorkflowName)
}
if logsData.Continuation.BeforeRunID != 12344 {
t.Errorf("Expected BeforeRunID 12344, got %d", logsData.Continuation.BeforeRunID)
}
if logsData.Continuation.Count != 100 {
t.Errorf("Expected Count 100, got %d", logsData.Continuation.Count)
}
if logsData.Continuation.Engine != "copilot" {
t.Errorf("Expected Engine 'copilot', got '%s'", logsData.Continuation.Engine)
}

// Test JSON serialization of continuation
jsonOutput, err := json.MarshalIndent(logsData, "", " ")
if err != nil {
t.Fatalf("Failed to marshal logs data to JSON: %v", err)
}

// Verify continuation is in JSON
var parsedData LogsData
if err := json.Unmarshal(jsonOutput, &parsedData); err != nil {
t.Fatalf("Failed to unmarshal logs data from JSON: %v", err)
}

if parsedData.Continuation == nil {
t.Fatal("Expected continuation field in unmarshaled JSON, got nil")
}
if parsedData.Continuation.BeforeRunID != 12344 {
t.Errorf("Expected BeforeRunID 12344 in unmarshaled JSON, got %d", parsedData.Continuation.BeforeRunID)
}
}

// TestBuildLogsDataWithoutContinuation tests that continuation is omitted when nil
func TestBuildLogsDataWithoutContinuation(t *testing.T) {
tmpDir := t.TempDir()

processedRuns := []ProcessedRun{
{
Run: WorkflowRun{
DatabaseID: 12345,
WorkflowName: "Test Workflow",
LogsPath: filepath.Join(tmpDir, "run-12345"),
},
},
}

// Build logs data without continuation
logsData := buildLogsData(processedRuns, tmpDir, nil)

// Verify continuation field is nil
if logsData.Continuation != nil {
t.Errorf("Expected continuation field to be nil, got %+v", logsData.Continuation)
}

// Test JSON serialization
jsonOutput, err := json.Marshal(logsData)
if err != nil {
t.Fatalf("Failed to marshal logs data to JSON: %v", err)
}

// Verify continuation is omitted from JSON (due to omitempty tag)
var parsedMap map[string]any
if err := json.Unmarshal(jsonOutput, &parsedMap); err != nil {
t.Fatalf("Failed to unmarshal logs data to map: %v", err)
}

if _, exists := parsedMap["continuation"]; exists {
t.Error("Expected continuation field to be omitted from JSON when nil")
}
}

// TestBuildMCPFailuresSummary tests MCP failures aggregation
func TestBuildMCPFailuresSummary(t *testing.T) {
processedRuns := []ProcessedRun{
Expand Down
18 changes: 17 additions & 1 deletion pkg/cli/logs_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,24 @@ type LogsData struct {
MissingTools []MissingToolSummary `json:"missing_tools,omitempty" console:"title:🛠️ Missing Tools Summary,omitempty"`
MCPFailures []MCPFailureSummary `json:"mcp_failures,omitempty" console:"title:⚠️ MCP Server Failures,omitempty"`
AccessLog *AccessLogSummary `json:"access_log,omitempty" console:"title:Access Log Analysis,omitempty"`
Continuation *ContinuationData `json:"continuation,omitempty" console:"-"`
LogsLocation string `json:"logs_location" console:"-"`
}

// ContinuationData provides parameters to continue querying when timeout is reached
type ContinuationData struct {
Message string `json:"message"`
WorkflowName string `json:"workflow_name,omitempty"`
Count int `json:"count,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
Engine string `json:"engine,omitempty"`
Branch string `json:"branch,omitempty"`
AfterRunID int64 `json:"after_run_id,omitempty"`
BeforeRunID int64 `json:"before_run_id,omitempty"`
Timeout int `json:"timeout,omitempty"`
}

// LogsSummary contains aggregate metrics across all runs
type LogsSummary struct {
TotalRuns int `json:"total_runs" console:"header:Total Runs"`
Expand Down Expand Up @@ -78,7 +93,7 @@ type AccessLogSummary struct {
}

// buildLogsData creates structured logs data from processed runs
func buildLogsData(processedRuns []ProcessedRun, outputDir string) LogsData {
func buildLogsData(processedRuns []ProcessedRun, outputDir string, continuation *ContinuationData) LogsData {
// Build summary
var totalDuration time.Duration
var totalTokens int
Expand Down Expand Up @@ -167,6 +182,7 @@ func buildLogsData(processedRuns []ProcessedRun, outputDir string) LogsData {
MissingTools: missingTools,
MCPFailures: mcpFailures,
AccessLog: accessLog,
Continuation: continuation,
LogsLocation: absOutputDir,
}
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/cli/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,15 @@ Note: Output can be filtered using the jq parameter.`,
JqFilter string `json:"jq,omitempty" jsonschema:"Optional jq filter to apply to JSON output"`
}
mcp.AddTool(server, &mcp.Tool{
Name: "logs",
Description: "Download and analyze workflow logs",
Name: "logs",
Description: `Download and analyze workflow logs.

Returns JSON with workflow run data and metrics. If the command times out before fetching all available logs,
a "continuation" field will be present in the response with updated parameters to continue fetching more data.
Check for the presence of the continuation field to determine if there are more logs available.

The continuation field includes all necessary parameters (before_run_id, etc.) to resume fetching from where
the previous request stopped due to timeout.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, args logsArgs) (*mcp.CallToolResult, any, error) {
// Build command arguments
// Force output directory to /tmp/gh-aw/aw-mcp/logs for MCP server
Expand Down
Loading