Skip to content

feat(cli): add --log-file for JSONL run logs#534

Open
davidslater wants to merge 2 commits into
mainfrom
ace/01KXGX2RY42XAFB85GPAPEWASW
Open

feat(cli): add --log-file for JSONL run logs#534
davidslater wants to merge 2 commits into
mainfrom
ace/01KXGX2RY42XAFB85GPAPEWASW

Conversation

@davidslater

Copy link
Copy Markdown
Collaborator

Created by GitHub Ace · View Session

Summary

Implements #380 ("Jsonl logs — Add flag to write jsonl logs to file").

Adds a --log-file <path> flag (also configurable via the THREAT_DETECTION_LOG_FILE environment variable) that writes a structured JSON Lines trace of a detection run — one JSON object per line — to a file. This is an additive observability sink for debugging/auditing detection runs in CI; it does not change the result JSON contract or exit codes.

What's included

  • New pkg/runlog package — a small, nil-safe JSONL logger.
    • Records always lead with time (RFC 3339), level (info/error), and event, followed by event-specific fields in deterministic sorted order (stable, diff-friendly).
    • Open() creates a fresh 0600 file (truncating any existing one) that the logger owns and closes.
    • A nil *Logger is a valid no-op, so call sites stay clean when logging is disabled.
  • Wiring in cmd/threat-detect/main.go — emits run_start, artifacts_loaded, prompt_built, per-attempt events (attempt_start / attempt_recorded / attempt_no_verdict), verdict, detection_failed, and a terminal status record carrying the same reason and exit code as the existing stderr status line.
  • Failure to open the log file is treated as a config error (the caller explicitly asked for logs).
  • Docs: README CLI flag + a JSONL section with an example, spec clause TD-20a, and CLAUDE.md flags/layout updates.
  • Tests for record shape, leading-key ordering, reserved-key shadowing, nil-logger no-op, and file permissions/truncation.

Example output

{"time":"2026-07-14T18:00:00Z","level":"info","event":"run_start","engine":"copilot","model":"","retries":1,"version":"1.2.3"}
{"time":"2026-07-14T18:00:03Z","level":"info","event":"verdict","has_threats":false,"malicious_patch":false,"prompt_injection":false,"reasons":[],"secret_leak":false}
{"time":"2026-07-14T18:00:03Z","level":"info","event":"status","exit":0,"reason":"result_recorded"}

Testing

⚠️ The Go toolchain is not available in the environment where this change was authored, so make fmt lint build test could not be run locally. Changes were reviewed manually for compile-correctness and gofmt (tab) compliance. Please run make agent-finish in CI / locally to validate before merge.

Closes #380.

Implements issue #380. Adds a --log-file flag (and THREAT_DETECTION_LOG_FILE
env var) that writes a structured JSON Lines trace of a detection run.

- New pkg/runlog package: a nil-safe JSONL logger whose records lead with
  time/level/event and emit remaining fields in deterministic sorted order;
  Open creates a 0600, truncated file the logger owns.
- Wire logging into cmd/threat-detect run(): run_start, artifacts_loaded,
  prompt_built, per-attempt events, verdict, detection_failed, and a terminal
  status record mirroring the stderr status line (reason + exit code).
- Log file is additive observability only: result JSON contract and exit codes
  are unchanged; failure to open the log file is a config error.
- Docs: README CLI flag + JSONL section, spec TD-20a, CLAUDE.md flags/layout.
- Tests for record shape, key ordering, reserved-key shadowing, nil logger,
  and file permissions/truncation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: David Slater <12449447+davidslater@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional JSONL run logging for detection observability without changing verdicts or exit codes.

Changes:

  • Adds a nil-safe JSONL logger and tests.
  • Wires --log-file and its environment variable into detection runs.
  • Documents the feature and normative contract.
Show a summary per file
File Description
cmd/threat-detect/main.go Adds CLI configuration and run-event logging.
pkg/runlog/runlog.go Implements the JSONL writer.
pkg/runlog/runlog_test.go Tests formatting and file behavior.
specs/threat-detection-spec.md Defines TD-20a.
README.md Documents usage and output.
CLAUDE.md Updates repository guidance.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 6
  • Review effort level: Medium

Comment thread cmd/threat-detect/main.go
Comment on lines +147 to +152
logger.Info("run_start", map[string]any{
"version": detector.Version,
"engine": engineID,
"model": model,
"retries": retries,
})
Comment thread cmd/threat-detect/main.go
// Open the JSONL run log if requested. A failure here is a config error:
// the caller explicitly asked for logs and should learn they were not written.
if logFile != "" {
l, err := runlog.Open(logFile)
Comment thread pkg/runlog/runlog.go
Comment on lines +50 to +54
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return nil, fmt.Errorf("opening JSONL log file: %w", err)
}
return &Logger{w: f, closer: f, now: time.Now}, nil
Comment thread pkg/runlog/runlog.go Outdated
}
l.mu.Lock()
defer l.mu.Unlock()
_, _ = l.w.Write(line)
Comment thread specs/threat-detection-spec.md Outdated
`THREAT_DETECTION_LOG_FILE` environment variable). When enabled, the detector MUST
write one JSON object per line, each containing at least the `time`, `level`, and
`event` keys, and MUST record a terminal `status` event carrying the same `reason`
and exit `code` as the stderr status line. The run log is an additive observability
Comment thread cmd/threat-detect/main.go
flag.StringVar(&model, "model", "", "Model to use for detection")
flag.StringVar(&promptFile, "prompt-template", "", "Path to custom prompt template (defaults to built-in)")
flag.StringVar(&outputJSON, "output", "", "Path to write JSON result (defaults to stdout)")
flag.StringVar(&logFile, "log-file", os.Getenv("THREAT_DETECTION_LOG_FILE"), "Path to write JSONL run logs (env: THREAT_DETECTION_LOG_FILE)")
- Normalize engine ID before logging run_start so the audit record shows
  the engine that actually runs (copilot) instead of "". Adds exported
  engine.Canonical/DefaultEngineID and reuses them in engine.New.
- Reject --log-file colliding with --output (cleaned/symlink-equivalent
  paths) as a config error to avoid corrupting both sinks.
- runlog.Open now chmods the descriptor to 0600 so truncating a pre-existing
  0644 file cannot leave audit data world-readable.
- runlog now records the first encode/write error and surfaces it from Close
  so disk-full/short-write failures are not silently lost.
- Spec TD-20a: rename the status field from `code` to `exit` to match the
  implementation and README.
- Add integration tests for run(): JSONL log contents/lifecycle/status,
  env-var resolution, path-collision rejection, and log-open failure; plus
  runlog chmod/write-error tests and engine.Canonical tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: David Slater <12449447+davidslater@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jsonl logs

2 participants