Skip to main content

Reference

Audit model

The structured audit events Elevarq Signals emits for every operationally significant moment — startup, collection, on-demand requests, exports — with their fields and secret-handling guarantees.

Elevarq Signals emits structured slog records keyed by audit_event=<name> for every operationally significant lifecycle moment — startup, collection cycles, on-demand requests, exports. This document catalogues the events, defines the correlation rules between them, and documents the audit-completeness invariant.

Event catalogue

Startup events (emitted once at process start)

EventCarries
config_validatedstatus (ok / error), warnings count, hard_errors count
high_sensitivity_collectorsenabled (boolean — high-sensitivity gate state)
targets_loadedenabled count, disabled count
mode_configuredmode (standalone / managed), control_plane_token_configured (boolean)

The mode_configured event records the audit posture for managed mode. The token VALUE is never logged — only the configured/not-configured boolean. The audit-attribute denylist would normally filter the key (token substring match), but a small hand-curated allow-list permits this specific metadata key.

Per-target collection events (emitted per cycle)

EventCarries
collection_startedtarget, plus request_id and actor when correlated to an on-demand request
collection_completedtarget, snapshot_id, status (success / partial / failed), duration_ms, collectors_total / _success / _failed / _skipped, plus request_id and actor when correlated

Interval-driven cycles (the periodic ticker) carry no request_id and no actor — those attributes are omitted entirely rather than emitted as empty strings.

/collect/now events

EventWhen emittedCarries
collect_now_requestedrequest was accepted; cycle queuedactor, request_id, requested_targets (array or "all_enabled"), accepted_targets, optional reason
collect_now_rejectedrequest failed validationactor, error (one of invalid_json, invalid_request_id, invalid_reason, empty_targets_array, targets_not_collectible), plus the parsed input fields if available before the rejection
collect_now_droppedaccepted by validation but cycle won't runactor, request_id, reason_category

collect_now_dropped has two reason_category values:

  • previous_request_pending — the channel buffer was already full at the API handler. The new request never reached the collector goroutine.
  • cycle_overlap_skipped — the collector goroutine picked up the request but the run lock could not be acquired because a previous cycle (manual or interval-driven) was still in flight.

Either way, the cycle for that request_id does not run. Both sub-cases emit the same event name so SIEM filters can grep collect_now_dropped once.

Export events

EventWhen emittedCarries
export_requestedclient called GET /exportactor, source_ip, target_id, since, until
export_completedexport finished (success or failure)actor, status, duration_ms, size_bytes, optional error_category

Failure paths set status=failed and error_category to one of invalid_target_id, invalid_time_format, invalid_time_range, conflicting_selectors, snapshot_not_found, builder_error, or write_error.

Circuit-breaker events

Operator-safety surfaces. State is in-memory; the audit trail is the only durable record across restart.

EventWhen emittedCarries
circuit_openedAuto: fail_threshold consecutive cycle errors on this targettarget, from
circuit_closedAuto: open_cooldown elapsed after circuit_openedtarget
circuit_pausedManual: POST /collect/pause or signalsctl collect pausetarget, from, optional actor, optional reason (≤ 256 chars)
circuit_resumedManual: POST /collect/resume or signalsctl collect resumetarget, optional actor
circuit_pause_noopOperator paused an unknown target (permissive no-op)target, actor, reason_category=unknown_target
collection_skippedA non-closed circuit prevents a cycle from runningtarget, reason_category ∈ {circuit_open, circuit_paused, min_interval_not_elapsed}

collection_skipped is shared with the min-snapshot-interval guard. The reason_category field disambiguates.

Configuration-reload events

EventWhen emittedCarries
config_reload_requestedSIGHUP delivered, or POST /reload receivedactor, trigger ∈ {SIGHUP, http}
config_reload_appliedReload validated and target list swappedactor, target_count
config_reload_rejectedLoad or strict validation failedactor, reason ∈ {load_failed, validate_failed}, redacted error (capped at 512 chars)
config_reload_target_addedReload introduced a target not in the previous listtarget
config_reload_target_removedReload removed a target from the previous listtarget
config_reload_target_modifiedA target's connection params changed (pool torn down)target

config_reload_rejected error is passed through DSN redaction and length-capped before logging so a YAML-parse error that quotes config-file content does not leak credential material into the audit stream.

Correlation: request_id

request_id is the end-to-end correlation key for everything that happens because of one /collect/now call.

  • The caller may supply it (regex ^[A-Za-z0-9_-]{1,32}$).
  • If absent, Elevarq Signals generates a ULID (which satisfies the regex by construction).
  • The 202 response echoes whichever value was used.
  • The same value appears on collect_now_requested, collect_now_dropped (when the cycle is dropped), and on every per-target collection_started / collection_completed for the cycle when it runs.

A single request thus produces a deterministic sequence of audit records, all greppable by request_id=<value>.

Example: successful narrowing run, two targets

audit_event=collect_now_requested actor=local_operator request_id=01J5K… requested_targets=[prod-main,prod-reporting] accepted_targets=[prod-main,prod-reporting] reason=automated
audit_event=collection_started     target=prod-main      request_id=01J5K… actor=local_operator
audit_event=collection_started     target=prod-reporting request_id=01J5K… actor=local_operator
audit_event=collection_completed   target=prod-main      request_id=01J5K… actor=local_operator status=success duration_ms=312 collectors_total=55 collectors_success=51 collectors_failed=0 collectors_skipped=4
audit_event=collection_completed   target=prod-reporting request_id=01J5K… actor=local_operator status=success duration_ms=298 collectors_total=55 collectors_success=51 collectors_failed=0 collectors_skipped=4

Example: rejected request

audit_event=collect_now_rejected actor=local_operator request_id=01J5K… error=targets_not_collectible requested_targets=[prod-main,does-not-exist] accepted_targets=[prod-main] rejected_targets=[{name=does-not-exist,reason=unknown_target}]

(no subsequent collection_started or collection_completed for that request_id)

Example: dropped because previous cycle still running

audit_event=collect_now_requested actor=local_operator request_id=01J5K…
audit_event=collect_now_dropped   actor=local_operator request_id=01J5K… reason_category=cycle_overlap_skipped

The actor field

actor identifies the principal that originated a request. The value is sourced from the bearer token the auth middleware matched — never inferred from request shape.

ValueOrigin
local_operatorThe local API token (api.token) matched.
control_planeThe Elevarq control-plane token matched (only valid in mode=managed).

Events that carry actor:

  • collect_now_requested / _rejected / _dropped
  • collection_started / collection_completed (when correlated by a request_id; omitted on interval-driven cycles)
  • export_requested / export_completed

The mode_configured startup event does NOT carry actor — there is no caller at startup. The other startup audit events (config_validated, high_sensitivity_collectors, targets_loaded) likewise do not carry actor.

See the Elevarq Signals control-plane documentation (signals-control-plane.md) for the token-to-actor mapping and the auth middleware behaviour.

Audit completeness invariant

Every accepted /collect/now request reaches a terminal outcome for its request_id along exactly one of three branches:

BranchTerminal recordsWhen
rejectedone collect_now_rejectedvalidation failed; cycle never queued
droppedone collect_now_dropped (after the matching collect_now_requested)queued but cycle never ran — channel full at the API handler, or cycle overlap detected by the collector
ranone collection_started per target + one collection_completed per targetcycle ran

The "ran" branch is per-target by design. A request that narrows to two targets emits two collection_started records and two collection_completed records, all sharing the same request_id. A request that omits the targets field collects every enabled target and emits one started / completed pair per target. There is no aggregate "cycle complete" record — keeping the per-target pairs as the cycle's terminal records means a partial multi-target run still produces complete per-target audit history for the targets that ran before any abort.

There is no silent loss path. If a request_id appears on collect_now_requested but the audit log shows no records on any of the three branches above, that is a defect.

A stabilization pass closed the last known hole: a request that survived validation, queued cleanly, but then got picked up when a previous cycle was mid-flight (the run-lock overlap path). The collector now emits collect_now_dropped reason_category=cycle_overlap_skipped for that case so the request_id always reaches a terminal record.

Secret-handling guarantees

Every audit attribute is filtered through a centralised denylist before being emitted. The filter rejects keys whose lowercased name contains any of:

password, secret, api_token, token, dsn, connection_string, payload, query_result

A small hand-curated allow-list overrides the substring match for specific keys that contain a denylist substring but only carry metadata about a configured value (booleans / counts / fingerprints), never the secret itself. The allow-list currently contains exactly one entry: control_plane_token_configured (the boolean on mode_configured).

This filter applies to every audit-log call. Individual call sites cannot bypass it. Automated tests pin this contract end to end, verifying that audit output contains no secret or token values.

Output formats

signals.log_json: true switches the slog handler from text to JSON. The same audit events are emitted; only the encoding changes. Example JSON shape for collect_now_requested:

{
  "time": "2026-04-25T20:14:33.512Z",
  "level": "INFO",
  "msg": "audit",
  "audit_event": "collect_now_requested",
  "actor": "local_operator",
  "request_id": "01J5K6T3HW2A4DGCXV5Z6P0M3R",
  "requested_targets": ["prod-main"],
  "accepted_targets": ["prod-main"]
}

What's intentionally not here

  • No auth_failed audit events on token mismatch. The existing 401 plus the per-IP rate limiter is the auth-failure surface. Adding explicit per-failure audit records would be noisy under bot scanning and is deferred to a future audit-completeness pass with its own rate limiting.
  • No token-rotation audit event. The file re-read on every request is silent by design; the audit log on the next request after rotation is sufficient observability.
  • No per-request_id outcome lookup endpoint. Auditors grep the audit log by request_id; an HTTP endpoint that resolves request_id → outcome is future work.