003 · Observability
Audit log, diagnostic log, telemetry and secret redaction (spec_observability_003)
| Status | Implemented. Reverse-engineered from 53f8c53 (2026-09-24) and kept with the code since; its paths and names checked 2026-09-27 (//tools/specs). |
| Source | pkg/engine/audit/audit.go, pkg/redact/redact.go, pkg/observability/{logfile,telemetry,span}.go; wiring in apps/cli/setup.go, pkg/engine/workspace.go |
| Tests | pkg/engine/audit/audit_test.go, pkg/redact/redact_test.go, pkg/observability/observability_test.go, pkg/engine/runtime/telemetry_test.go |
| Depends on | spec_config_002 |
Three records, each with secrets masked and none able to break a session:
- the audit log — what the agent and the user did (for accountability);
- the diagnostic log — warnings and errors (for debugging);
- OpenTelemetry traces and logs — opt-in, content-free by default.
- RED-01 Built-in patterns are masked as
[REDACTED]: PEM private keys; AWS access key IDs (AKIA…); OpenAI/Anthropic keys (sk-,sk-ant-,sk-proj-); Google API keys (AIza…); GitHub tokens (ghp_ gho_ ghu_ ghs_ ghr_,github_pat_); Slack tokens (xox[abprs]-); JWTs. - RED-02 Assignment-style secrets (
password=,api_key:,token=,secret,access_key…, case-insensitive, value ≥ 6 chars): only the value is masked. - RED-03 Exact values are masked too: configured API keys (Gemini, OpenAI, Anthropic, web search), every MCP
envvalue, and the values of environment variables matchingsandbox.scrub_envglobs. Values shorter than 8 chars are ignored (false positives); longest first. - RED-04
Valuemasks recursively inside maps and slices (tool arguments) and returns a copy. A nil redactor still applies the built-in patterns.
- AUD-01
~/.blitz/audit/audit-YYYY-MM-DD.jsonl(UTC day rotation), directory 0700, files 0600, append-only. Disabled with[audit] enabled = false(then a nil logger no-ops everywhere). - AUD-02 Entry:
time(UTC),session,kind,tool,args,detail,decision,error,workspace. Session and workspace default to the context set when the session opened. - AUD-03 Kinds:
session_start,prompt,tool_call(name, args),tool_result(ok/error),approval(decision:once|session|always|auto-policy|session-rule|saved-rule|user-selected|unattended-permitted),denial(deny|no-approver|error|unattended-refused),hook(blockor error),undo(files),attachment(path and hash only),user_shell(!cmd,exit N, start error),user_search(/search webprovider and query). - AUD-04 Detail, error and args are redacted before writing. Write failures are reported once to stderr and otherwise ignored.
- LOG-01
~/.blitz/logs/blitz-YYYY-MM-DD.jsonl(log.dir), owner-only, slog JSON records. Levellog.levelorBLITZ_LOG_LEVEL:debug|info|warn|error|off. Files older thanlog.retain_days(14) are deleted (0 = keep). - LOG-02 Writes never block callers: records go to a 1024-entry queue drained by one writer goroutine with group commit; when full, lines are dropped and counted, and the count is written once there is room.
- LOG-03 String and error attributes are redacted before encoding. With telemetry on, lines carry trace and span IDs.
- LOG-04 With level
off, only extra handlers (the OTel log bridge) receive records, at info level. A failure to open the log disables it with a warning. - LOG-05 The CLI logs
start(version, provider, model, telemetry), failed turns, andexitwith code and error when non-zero. The service logs workspace opens and model problems per workspace. - LOG-06 Reading:
LogDays(dir)lists the days with a log (YYYY-MM-DD, newest first; other files ignored).ReadLog(dir, query)reads one day ("": the newest; anything but a date is refused), keeping records atmin_leveland above and containing every word oftextin any field (ignoring case), newest first, at mostlimit(500), and counts every match. Fields keep their order; values are text (strings bare, the rest JSON). Lines that aren’t records (a torn last line) are skipped.DeleteLogDay(dir, day, now)deletes a day’s log: one beforenow’s date (today’s and later are refused,ErrLogInUse: the sink is writing it),ErrNoLogDayfor a day with no log,ErrBadLogDayfor anything but a date. The service serves them (SVC-06a) to the desktop’s log viewer (DSK-60).
- OTEL-01 Off by default; nothing is sent unless
[telemetry] enabled = trueorBLITZ_TELEMETRY=1. Exports traces and logs over OTLP/HTTP totelemetry.endpoint, elseOTEL_EXPORTER_OTLP_ENDPOINT, elsehttp://localhost:4318. OtherOTEL_EXPORTER_OTLP_*settings apply. Service nameblitz. - OTEL-02 Content is never exported by default: prompts, replies, tool arguments and tool results are removed from spans (the ADK sets tool args/results on every
execute_toolspan regardless, so Blitz filters them in its own exporter).capture_content = trueincludes them with secrets masked (and setsOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTfor the ADK). - OTEL-03 Providers are built by Blitz, not
adk/telemetry.New(which adds its own unfiltered exporter). One provider per process: the ADK binds its tracer to the first global provider. - OTEL-04 Spans:
turn(root per prompt; see spec_engine_016 ENG-90/91), the ADK’s agent/model/tool spans,approval(time waiting on the user),hook <event>,compact,btw. Every span carriesgen_ai.conversation.id. Span attributes must not carry content — sizes, names and outcomes only. Cancellation is not recorded as an error. - OTEL-05 Export runs on SDK batch goroutines; shutdown gives up after 3 s if the collector is unreachable.
- OTEL-06 Telemetry start failure warns and disables telemetry only.
- OTEL-07 Metrics (PAR-MOD-07), exported with traces and logs over OTLP/HTTP (
/v1/metricsat the same endpoint, orOTEL_EXPORTER_OTLP_METRICS_ENDPOINT) every 30 s and at exit: countersblitz.session.count(reason: startup, new, resume, load…),blitz.turn.count(agent,outcome: ok, error, blocked, limit, cancelled; every prompt throughWorkspace.Run),blitz.token.usage(model,type: input, output, cached_input; every model call, the reviewer’s and judges’ too),blitz.cost.usage(USD,model; priced calls only),blitz.tool.calls(tool,outcome: ok, error),blitz.approval.decisions(kind,decision: how it was decided, the audit decision’s first word:user,rule-allow,mode-bypass,hook-deny,auto-allow…). No content and no session IDs as attributes. Without telemetry the global meter records nothing.