Single-Event API¶
POST /events emits one log or metric event synchronously. The request blocks until the sink ACKs delivery, then returns the latency it took. Use it when you want a single signal to land now and you want to know it landed before continuing.
The flat JSON body is the easy alternative to the v2 scenario shape — paste the encoder and sink inline, no defaults:, no version: 2, no scenario IDs to track.
When to use /events vs /scenarios¶
| Want… | Endpoint |
|---|---|
| One signal, one moment, blocks until delivered | POST /events |
| A stream of signals at a sustained rate | POST /scenarios |
Both share the same encoders, sinks, auth, and loopback-warning behavior. They differ only in lifecycle:
/eventsis synchronous. The handler encodes the event, pushes it through the sink, and returns{sent, signal_type, latency_ms}once the destination ACKs (typically 5–30 ms). There is no scenario ID — the call is fire-and-confirm./scenariosis asynchronous. The server returns a scenario ID immediately and the scenario runs in the background until itsdurationexpires or you callDELETE /scenarios/{id}.
Two real-world drivers
- Workshop CLI — fire teaching events without learning the v2 YAML shape.
- Live demos — a single
curlon stage produces a Loki log line that Grafana picks up as a panel annotation within ~5–15 ms.
Request body¶
The body is a JSON object tagged by signal_type. The discriminator selects which per-branch field is required (log for logs, metric for metrics).
curl -X POST http://localhost:8080/events \
-H "Content-Type: application/json" \
-d '{
"signal_type": "logs",
"labels": {"event": "deploy_start", "env": "prod"},
"log": {
"severity": "info",
"message": "Deploy started",
"fields": {"version": "1.2.2"}
},
"encoder": {"type": "json_lines"},
"sink": {"type": "loki", "url": "http://loki:3100"}
}'
curl -X POST http://localhost:8080/events \
-H "Content-Type: application/json" \
-d '{
"signal_type": "metrics",
"labels": {"event": "deploy_start", "job": "sonda"},
"metric": {"name": "deploy_events_total", "value": 1.0},
"encoder": {"type": "remote_write"},
"sink": {"type": "remote_write", "url": "http://prom:9090/api/v1/write"}
}'
Field reference¶
| Field | Type | Required | Notes |
|---|---|---|---|
signal_type |
string | yes | "logs" or "metrics". Anything else returns 400. |
labels |
object\<string,string> | yes | Forwarded to the sink. Loki uses them as stream labels. May be {}. |
log |
object | when signal_type=logs |
See Log payload below. |
metric |
object | when signal_type=metrics |
See Metric payload below. |
encoder |
object | yes | Same shape as /scenarios. See Encoders. |
sink |
object | yes | Same shape as /scenarios. See Sinks. |
Log payload (signal_type: "logs"):
| Field | Type | Required | Notes |
|---|---|---|---|
severity |
string | yes | trace / debug / info / warn / error / fatal (lowercase). |
message |
string | yes | Human-readable log message. |
fields |
object\<string,string> | no | Flat structured fields. Defaults to {}. |
Metric payload (signal_type: "metrics"):
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Metric name. Must match [a-zA-Z_:][a-zA-Z0-9_:]*. |
value |
number (f64) | yes | Sample value. |
The on-wire metric shape (counter, gauge, histogram lines, TimeSeries protobuf, …) is determined by the encoder you pick — there is no separate metric_type field.
Response¶
Success (200)¶
latency_ms is the wall-clock time the handler spent encoding the event and waiting for the sink to ACK.
When pre-flight checks find advisories (e.g. a sink URL pointing at a loopback host), the response includes a warnings array. The field is omitted entirely when no warnings fire, so older clients parse responses unchanged:
{
"sent": true,
"signal_type": "logs",
"latency_ms": 7,
"warnings": [
"scenario entry 'events.logs' sink `loki` targets `http://localhost:3100` — this host resolves to the sonda-server container's own loopback, not your host. Use a Docker Compose service name (e.g. `victoriametrics:8428`) or a Kubernetes Service DNS name instead. See docs/deployment/endpoints.md."
]
}
Warnings are informational — they never block delivery. The same message is also written to the server log via tracing::warn!.
Errors¶
All errors share the envelope {"error": "<short_code>", "detail": "<message>"}.
| Status | When | Example detail |
|---|---|---|
| 400 Bad Request | Malformed JSON; unknown signal_type; missing per-branch field; unknown encoder/sink type. |
unknown variant 'traces', expected 'logs' or 'metrics' |
| 401 Unauthorized | API key configured and Authorization: Bearer <key> missing or wrong. |
missing or malformed Authorization header |
| 422 Unprocessable Entity | Encoder/sink config validation failed (invalid metric name, tcp retry max_attempts: 0, etc.). |
invalid metric name "1bad": must match [a-zA-Z_:][a-zA-Z0-9_:]* |
| 502 Bad Gateway | Sink push or flush returned an error (Loki down, network unreachable, etc.). | sink error: TCP connect to 127.0.0.1:1: Connection refused |
| 500 Internal Server Error | Unexpected — encoder error or panic in the blocking task. | runtime error: <detail> |
Authentication¶
/events follows the same auth model as /scenarios. When the server starts with --api-key <key> (or SONDA_API_KEY=<key>), every request must include Authorization: Bearer <key>. When no key is configured, /events is publicly accessible — backwards compatible with existing deployments.
curl -X POST http://localhost:8080/events \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"signal_type": "logs",
"labels": {"event": "x"},
"log": {"severity": "info", "message": "hello"},
"encoder": {"type": "json_lines"},
"sink": {"type": "stdout"}
}'
See Authentication on the Server API page for the full configuration reference.
Demo: Grafana annotation from one curl¶
Fire a single log line and watch it appear as a Grafana panel annotation within seconds. This works on the default sonda-server binary — no feature flags required.
curl -s -X POST http://127.0.0.1:8080/events \
-H "Content-Type: application/json" \
-d '{
"signal_type":"logs",
"labels":{"event":"deploy_start","env":"prod"},
"log":{"severity":"info","message":"Deploy started","fields":{"version":"1.2.2"}},
"encoder":{"type":"json_lines"},
"sink":{"type":"loki","url":"http://loki:3100"}
}'
# {"sent":true,"signal_type":"logs","latency_ms":7}
curl -s 'http://localhost:3100/loki/api/v1/query_range' \
--data-urlencode 'query={event="deploy_start"}' \
--data-urlencode 'limit=1'
End-to-end latency observed against a real Loki instance: 5–15 ms from curl to ACK. Wire a Grafana annotation query against {event="deploy_start"} and the panel renders an overlay automatically.
Build-time feature flags¶
The default sonda-server binary supports loki, stdout, file, tcp, udp, http_push, json_lines, prometheus_text, and syslog. The Loki annotation demo above works out of the box.
A few sinks and encoders are gated behind cargo features to keep the default binary small:
| Need | Build with |
|---|---|
remote_write encoder, remote_write sink |
cargo build --release -p sonda-server -F remote-write |
otlp encoder, otlp_grpc sink |
cargo build --release -p sonda-server -F otlp |
kafka sink |
cargo build --release -p sonda-server -F kafka |
If a request references a type that isn't compiled in, the server returns 422 with a clear hint, e.g. encoder type 'remote_write' requires the 'remote-write' feature: cargo build -F remote-write.
Sink URL gotchas¶
When the server runs in a container, a sink.url of http://localhost:<port> resolves to the server's own loopback, not your host. The request still succeeds, but the warnings array calls out the misconfiguration. In Docker Compose use the service name (http://loki:3100); in Kubernetes use the in-cluster Service DNS. See Endpoints & networking for the full reference.
Not in this version¶
- Burst path — there is no
countordurationfield. For sustained emission, usePOST /scenarios. - Trace and flow signal types — only
logsandmetricsare supported. - CLI subcommand — there is no
sonda emityet. The endpoint is the only entry point.