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
4 changes: 2 additions & 2 deletions pkg/cli/context_cancellation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestDownloadWorkflowLogsWithCancellation(t *testing.T) {
cancel()

// Try to download logs with a cancelled context
err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "", "")
err := DownloadWorkflowLogs(ctx, "", 10, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "", "", false)

// Should return context.Canceled error
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestDownloadWorkflowLogsTimeoutRespected(t *testing.T) {

start := time.Now()
// Use a workflow name that doesn't exist to avoid actual network calls
_ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, "", "")
_ = DownloadWorkflowLogs(ctx, "nonexistent-workflow-12345", 100, "", "", "/tmp/test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 1, "", "", false)
elapsed := time.Since(start)

// Should complete within reasonable time (give 5 seconds buffer for test overhead)
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/logs_ci_scenario_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestLogsJSONOutputWithNoRuns(t *testing.T) {
10, // timeout
"summary.json", // summaryFile
"", // safeOutputType
false, // filteredIntegrity
)

// Restore stdout and read output
Expand Down
5 changes: 4 additions & 1 deletion pkg/cli/logs_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Examples:
` + string(constants.CLIExtensionPrefix) + ` logs --safe-output create-issue # Filter logs with create_issue messages
` + string(constants.CLIExtensionPrefix) + ` logs --ref main # Filter logs by branch or tag
` + string(constants.CLIExtensionPrefix) + ` logs --ref feature-xyz # Filter logs by feature branch
` + string(constants.CLIExtensionPrefix) + ` logs --filtered-integrity # Filter logs with DIFC integrity-filtered items in gateway logs

# Run ID range filtering
` + string(constants.CLIExtensionPrefix) + ` logs --after-run-id 1000 # Filter runs after run ID 1000
Expand Down Expand Up @@ -138,6 +139,7 @@ Examples:
repoOverride, _ := cmd.Flags().GetString("repo")
summaryFile, _ := cmd.Flags().GetString("summary-file")
safeOutputType, _ := cmd.Flags().GetString("safe-output")
filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity")

// Resolve relative dates to absolute dates for GitHub CLI
now := time.Now()
Expand Down Expand Up @@ -172,7 +174,7 @@ Examples:

logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s", workflowName, count, engine)

return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, summaryFile, safeOutputType)
return DownloadWorkflowLogs(cmd.Context(), workflowName, count, startDate, endDate, outputDir, engine, ref, beforeRunID, afterRunID, repoOverride, verbose, toolGraph, noStaged, firewallOnly, noFirewall, parse, jsonOutput, timeout, summaryFile, safeOutputType, filteredIntegrity)
},
}

Expand All @@ -191,6 +193,7 @@ Examples:
logsCmd.Flags().Bool("firewall", false, "Filter to only runs with firewall enabled")
logsCmd.Flags().Bool("no-firewall", false, "Filter to only runs without firewall enabled")
logsCmd.Flags().String("safe-output", "", "Filter to runs containing a specific safe output type (e.g., create-issue, missing-tool, missing-data)")
logsCmd.Flags().Bool("filtered-integrity", false, "Filter to runs with DIFC integrity-filtered items in the gateway logs")
logsCmd.Flags().Bool("parse", false, "Run JavaScript parsers on agent logs and firewall logs, writing Markdown to log.md and firewall.md")
addJSONFlag(logsCmd)
logsCmd.Flags().Int("timeout", 0, "Download timeout in seconds (0 = no timeout)")
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/logs_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestDownloadWorkflowLogs(t *testing.T) {
// Test the DownloadWorkflowLogs function
// This should either fail with auth error (if not authenticated)
// or succeed with no results (if authenticated but no workflows match)
err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "")
err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", "", "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "", false)

// If GitHub CLI is authenticated, the function may succeed but find no results
// If not authenticated, it should return an auth error
Expand Down Expand Up @@ -257,7 +257,7 @@ func TestDownloadWorkflowLogsWithEngineFilter(t *testing.T) {
if !tt.expectError {
// For valid engines, test that the function can be called without panic
// It may still fail with auth errors, which is expected
err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "")
err := DownloadWorkflowLogs(context.Background(), "", 1, "", "", "./test-logs", tt.engine, "", 0, 0, "", false, false, false, false, false, false, false, 0, "summary.json", "", false)

// Clean up any created directories
os.RemoveAll("./test-logs")
Expand Down
82 changes: 81 additions & 1 deletion pkg/cli/logs_filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestLogsCommandFlags(t *testing.T) {
cmd := NewLogsCommand()

// Check that all expected flags are present
expectedFlags := []string{"count", "start-date", "end-date", "output", "engine", "ref", "before-run-id", "after-run-id"}
expectedFlags := []string{"count", "start-date", "end-date", "output", "engine", "ref", "before-run-id", "after-run-id", "filtered-integrity"}

for _, flagName := range expectedFlags {
flag := cmd.Flags().Lookup(flagName)
Expand Down Expand Up @@ -444,3 +444,83 @@ func TestFindAgentLogFile(t *testing.T) {
}
})
}

// TestRunHasDifcFilteredItems verifies the DIFC filtered-integrity filter helper.
func TestRunHasDifcFilteredItems(t *testing.T) {
const gatewayWithDifc = `{"timestamp":"2025-01-01T00:00:00Z","type":"DIFC_FILTERED","server_id":"github","tool_name":"create_issue","reason":"integrity"}` + "\n"
const gatewayWithoutDifc = `{"timestamp":"2025-01-01T00:00:00Z","event":"tool_call","server_name":"github","tool_name":"list_issues","duration":10}` + "\n"

tests := []struct {
name string
fileContent string
filePath func(dir string) string
want bool
}{
{
name: "gateway.jsonl with DIFC_FILTERED event",
fileContent: gatewayWithDifc,
filePath: func(dir string) string { return filepath.Join(dir, "gateway.jsonl") },
want: true,
},
{
name: "gateway.jsonl without DIFC_FILTERED events",
fileContent: gatewayWithoutDifc,
filePath: func(dir string) string { return filepath.Join(dir, "gateway.jsonl") },
want: false,
},
{
name: "mcp-logs/gateway.jsonl with DIFC_FILTERED event",
fileContent: gatewayWithDifc,
filePath: func(dir string) string { return filepath.Join(dir, "mcp-logs", "gateway.jsonl") },
want: true,
},
Comment on lines +450 to +476
{
name: "no gateway logs present",
fileContent: "",
filePath: nil, // no file created
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := testutil.TempDir(t, "difc-filter-*")

if tt.filePath != nil {
path := tt.filePath(dir)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("failed to create directory: %v", err)
}
if err := os.WriteFile(path, []byte(tt.fileContent), 0644); err != nil {
t.Fatalf("failed to write gateway file: %v", err)
}
}

got, err := runHasDifcFilteredItems(dir, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("runHasDifcFilteredItems() = %v, want %v", got, tt.want)
}
})
}
}

// TestFilteredIntegrityFlag verifies the --filtered-integrity flag is registered correctly.
func TestFilteredIntegrityFlag(t *testing.T) {
cmd := NewLogsCommand()

flag := cmd.Flags().Lookup("filtered-integrity")
if flag == nil {
t.Fatal("Expected flag 'filtered-integrity' not found in logs command")
}

if flag.DefValue != "false" {
t.Errorf("Expected 'filtered-integrity' default to be 'false', got: %s", flag.DefValue)
}

if flag.Usage == "" {
t.Error("Expected 'filtered-integrity' flag to have usage text")
}
}
4 changes: 3 additions & 1 deletion pkg/cli/logs_json_stderr_order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestLogsJSONOutputBeforeStderr(t *testing.T) {
10, // timeout
"summary.json", // summaryFile
"", // safeOutputType
false, // filteredIntegrity
)

// Close writers first
Expand Down Expand Up @@ -177,7 +178,8 @@ func TestLogsJSONAndStderrRedirected(t *testing.T) {
true, // jsonOutput
10,
"summary.json",
"", // safeOutputType
"", // safeOutputType
false, // filteredIntegrity
)

// Close the writer
Expand Down
40 changes: 38 additions & 2 deletions pkg/cli/logs_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ func getMaxConcurrentDownloads() int {
}

// DownloadWorkflowLogs downloads and analyzes workflow logs with metrics
func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, summaryFile string, safeOutputType string) error {
logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType)
func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, startDate, endDate, outputDir, engine, ref string, beforeRunID, afterRunID int64, repoOverride string, verbose bool, toolGraph bool, noStaged bool, firewallOnly bool, noFirewall bool, parse bool, jsonOutput bool, timeout int, summaryFile string, safeOutputType string, filteredIntegrity bool) error {
logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType, filteredIntegrity)

// Ensure .github/aw/logs/.gitignore exists on every invocation
if err := ensureLogsGitignore(); err != nil {
Expand Down Expand Up @@ -309,6 +309,23 @@ func DownloadWorkflowLogs(ctx context.Context, workflowName string, count int, s
}
}

// Apply filtered-integrity filtering if --filtered-integrity flag is specified
if filteredIntegrity {
hasFiltered, checkErr := runHasDifcFilteredItems(result.LogsPath, verbose)
if checkErr != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check DIFC filtered items for run %d: %v", result.Run.DatabaseID, checkErr)))
continue
}

if !hasFiltered {
logsOrchestratorLog.Printf("Skipping run %d: no DIFC filtered items found", result.Run.DatabaseID)
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skipping run %d: no DIFC integrity-filtered items found in gateway logs", result.Run.DatabaseID)))
}
continue
}
}

// Update run with metrics and path
run := result.Run
run.TokenUsage = result.Metrics.TokenUsage
Expand Down Expand Up @@ -869,3 +886,22 @@ func runContainsSafeOutputType(runDir string, safeOutputType string, verbose boo

return false, nil
}

// runHasDifcFilteredItems checks if a run's gateway logs contain any DIFC_FILTERED events.
// It parses the gateway logs (falling back to rpc-messages.jsonl when gateway.jsonl is absent)
// and returns true when at least one DIFC integrity- or secrecy-filtered event is present.
func runHasDifcFilteredItems(runDir string, verbose bool) (bool, error) {
logsOrchestratorLog.Printf("Checking run for DIFC filtered items: dir=%s", runDir)

gatewayMetrics, err := parseGatewayLogs(runDir, verbose)
if err != nil {
// No gateway log file present — not an error for workflows without MCP
return false, nil
Comment on lines +898 to +899
}

if gatewayMetrics == nil {
return false, nil
}

return gatewayMetrics.TotalFiltered > 0, nil
}
Loading