This document describes how POST /collect/now accepts an optional
JSON body to narrow the cycle to a subset of configured targets, and
how an optional Elevarq control plane can drive that narrowing
authenticated as a distinct actor. Specs: SIGNALS-R082 (Phase 1
- Phase 2) and R083.
This is operator-facing reference — for the wire contract, see the
spec text in features/signals/specification.md.
Elevarq Signals runs in one of two modes; the mode is set by
signals.mode in the config (default standalone).
| Mode | When to use | Auth surface |
|---|---|---|
standalone (default) |
Open-source / local-only / community deployments. Operator drives everything via the local API token. | One bearer token (api.token). |
managed |
Deployments where the commercial Elevarq control plane drives collection. | Two distinct bearer tokens. The local api.token for operator commands; a separate control_plane_token for the control plane. |
The configured target list in signals.yaml is the authoritative
ceiling in both modes. The control plane (or any caller) can only
narrow this set, never expand it.
The endpoint accepts an optional JSON body. All fields are optional. An empty body (or a missing body) keeps the historical "collect every enabled target" behaviour.
{
"targets": ["prod-main", "prod-reporting"],
"request_id": "abc_123",
"reason": "scheduled_cycle",
"force": false
}| Field | Validation | Meaning |
|---|---|---|
targets |
optional string[]; non-empty when present |
Subset of configured target names. Absent → collect all enabled targets. Empty array [] → 400 (treated as a client bug, never silently coerced). |
request_id |
optional ^[A-Za-z0-9_-]{1,32}$ |
Correlation identifier propagated through to per-target audit events. When absent, Elevarq Signals generates a ULID. |
reason |
optional ^[A-Za-z0-9_-]{1,64}$ |
Short tag-style label surfaced in audit events. Not free-form prose. |
force |
optional boolean, default false |
Bypasses R091's min_snapshot_interval for this cycle only. Does not bypass a paused circuit (R097) — resume the target first. |
Validation rejections return 400 Bad Request with an error
field plus accepted_targets / rejected_targets where applicable.
The cycle is not triggered when any target was rejected.
The examples below assume SIGNALS_API_TOKEN=dev-local-only-replace-in-prod-32chars and the
daemon listening on 127.0.0.1:8081 (the default).
curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}"Response:
{
"status": "collection triggered",
"request_id": "01J5K6T3HW2A4DGCXV5Z6P0M3R",
"accepted_targets": ["prod-main", "prod-reporting", "prod-analytics"]
}request_id is daemon-generated when the caller omits one.
curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"targets":["prod-main"]}'Response:
{
"status": "collection triggered",
"request_id": "01J5K6T3HW2A4DGCXV5Z6P0M3R",
"accepted_targets": ["prod-main"]
}curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"targets": ["prod-main", "prod-reporting"],
"request_id": "scheduled_run_2026_04_25",
"reason": "automated_cycle"
}'Response:
{
"status": "collection triggered",
"request_id": "scheduled_run_2026_04_25",
"accepted_targets": ["prod-main", "prod-reporting"]
}force: true tells the daemon to ignore the per-target
min_snapshot_interval guard for this cycle only. Useful when an
operator is iterating on configuration or recovering from an
incident and needs back-to-back snapshots inside the interval.
curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"targets":["prod-main"], "force": true}'force does not bypass a paused circuit (R097). A target
paused via /collect/pause stays paused even under force: true;
the operator must resume it explicitly. signalsctl collect now --force is the CLI surface for this field.
curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"targets":["prod-main","does-not-exist"]}'Response (HTTP 400):
{
"error": "one or more targets cannot be collected",
"accepted_targets": ["prod-main"],
"rejected_targets": [
{"name": "does-not-exist", "reason": "unknown_target"}
]
}The cycle does not run. The audit log contains a
collect_now_rejected event correlated by request_id (generated
or supplied).
curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"request_id":"abc 123"}'Response (HTTP 400):
{ "error": "request_id must match ^[A-Za-z0-9_-]{1,32}$" }curl -s -X POST http://127.0.0.1:8081/collect/now \
-H "Authorization: Bearer ${SIGNALS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"targets":[]}'Response (HTTP 400):
{
"error": "targets must be a non-empty array; omit the field to collect all enabled targets"
}Every accepted request produces a deterministic sequence of audit
events. The end-to-end correlation key is request_id.
The examples below use the slog text handler output (the
default). With signals.log_json: true the daemon emits the same
events with the same field names, JSON-encoded — see
audit-model.md for an example JSON record.
audit_event=collect_now_requested actor=local_operator request_id=01J5K… requested_targets=[prod-main] accepted_targets=[prod-main]
audit_event=collection_started target=prod-main 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 …
audit_event=collect_now_rejected actor=local_operator request_id=01J5K… requested_targets=[prod-main,does-not-exist] accepted_targets=[prod-main] rejected_targets=[{name=does-not-exist,reason=unknown_target}]
(no collection_started / collection_completed)
audit_event=collect_now_requested actor=local_operator request_id=01J5K… …
audit_event=collect_now_dropped actor=local_operator request_id=01J5K… reason_category=previous_request_pending
(no collection_started / collection_completed)
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 two dropped reasons differ by detection point — channel-full
at the API handler vs. overlap detected inside the collector — but
both mean the cycle for THIS request_id will not run. Auditors
can grep by request_id to see the drop.
Every accepted /collect/now request reaches a terminal outcome
for its request_id along exactly one of three branches:
collect_now_rejected— validation failure; cycle not queued.collect_now_dropped— queued but cycle never ran (channel full, or cycle overlap).- per-target
collection_started+collection_completed— the cycle ran. A multi-target request emits one started / completed pair per target, all sharing the samerequest_id. A request that omitstargetscollects every enabled target and emits one pair per target. There is no aggregate "cycle complete" record — the per-target pairs are the cycle's terminal records.
There is no silent loss path. See
audit-model.md for the full event catalogue
and the per-branch terminal-record rules.
The actor field on every audit event is sourced from the matched
bearer token (R083), never inferred from request shape:
| Bearer matched | actor |
Conditions |
|---|---|---|
api.token |
local_operator |
Any mode. Default for every operator-driven call. |
control_plane_token |
control_plane |
Only when signals.mode: managed. |
| Neither | (request never reaches the handler) | 401, R024 rate limiter records the failure. |
See authentication.md for the full Mode B
auth model, token rotation, and validation rules.
- Channel buffer = 1. Concurrent on-demand requests are
serialised. The first request is queued; later requests get
collect_now_dropped(channel-full) until the queued one is picked up by the collector goroutine. - No new rate limiting on accepted requests. R024's per-IP rate limiter on invalid auth attempts continues to apply.
- Phase 2 actor invariant.
actorvalue derives from the matched token. R082 Phase 2 always emittedlocal_operator; R083 lifted that to allowcontrol_plane. control_planeis reserved formode=managed. Inmode=standaloneeven a control-plane-token-shaped header value is treated as unknown — 401.
Operator override for the per-target circuit breaker (R097). State is in-memory; daemon restart resumes all targets. Audit log preserves the pause/resume trail across restart.
POST /collect/pause
Content-Type: application/json
Authorization: Bearer $SIGNALS_API_TOKEN
{"target": "prod-db", "reason": "investigating incident #4321"}target(optional) — name from config. Empty / absent ⇒ apply to every enabled target.reason(optional, ≤ 256 chars) — operator memo carried on thecircuit_pausedaudit event.
POST /collect/resume
Content-Type: application/json
Authorization: Bearer $SIGNALS_API_TOKEN
{"target": "prod-db"}target empty ⇒ resume all enabled targets.
| Path | Status | Body |
|---|---|---|
| Pause known target | 200 | {"paused":["prod-db"]} |
| Pause unknown target | 200 + circuit_pause_noop audit event |
{"paused":["new-target-not-yet-in-config"]} — pause is permissive (operators may pause a target they're about to add) |
Pause with reason > 256 chars |
400 | {"error":"reason exceeds 256 chars"} |
| Resume known target | 200 | {"resumed":["prod-db"]} |
| Resume unknown target | 400 + available-target list | {"error":"unknown target ...","available":[...]} (FC-CIRC-02) |
A target that fails fail_threshold consecutive cycles (default 3)
auto-transitions to open and auto-recovers after open_cooldown
(default 5 min). Manual paused takes priority — a paused target
stays paused until resume. Pause/resume operations carry the
operator actor and reason directly on the canonical
circuit_paused / circuit_resumed audit events (one event per
state change, no supplemental _request events).
signalsctl collect now --force bypasses R091's min-snapshot-interval
only; it does NOT bypass a paused circuit. To override a paused
target, resume it first.
R100: re-read the config file, validate, swap the runtime-mutable subset (target list) in place. Pools for removed and connection- modified targets are closed so the next cycle re-dials with new parameters.
# Signal
kill -HUP $(pidof signals)
# HTTP
curl -X POST http://localhost:8081/reload \
-H "Authorization: Bearer $SIGNALS_API_TOKEN"- Add / remove targets.
- Modify a target's connection params or
collectors.profile(R098 sensitivity profile).
poll_interval,target_timeout,query_timeout,min_snapshot_interval.signals.retention.*(R099) thresholds.signals.circuit.*(R097) thresholds.
| Path | Status | Audit |
|---|---|---|
| Reload, validation passes | 200 + {"reloaded":true,"target_count":N} |
config_reload_requested → config_reload_applied + per-target config_reload_target_added / _removed / _modified |
| Reload, file unreadable | 400 + redacted load error | config_reload_rejected with reason=load_failed |
Reload, ValidateStrict fails |
400 + redacted validation error | config_reload_rejected with reason=validate_failed |
A reload that fails to load or validate leaves the running state
untouched. Error messages on both paths are passed through
collector.RedactDSN and capped at 512 chars before they reach
the audit stream or the HTTP body (issue #87 hardening).