OpenTelemetry
Synapse speaks OTLP/HTTP for two signals: security event logs, and metrics. Point it at any OpenTelemetry collector and both arrive on the standard paths — there is no Gen0Sec-specific protocol in the way.
The backend-specific pages (Axiom, Honeycomb) are worked examples of what this page describes generically.
Turning it on
# /etc/synapse/config.yaml
telemetry:
enabled: true
otlp:
endpoint: "https://collector.example.com:4318"
headers:
authorization: "Bearer <token>"
# Sample the log stream. Errors bypass the sampler.
sample_rate: 1.0
always_keep_errors: true
# HTTP access logs are the highest-volume stream, so they sample separately.
access_log:
sample_rate: 0.1
always_keep_errors: true
endpoint is a base URL. Synapse appends the OTel-standard paths itself:
| Signal | Path |
|---|---|
| Logs | {endpoint}/v1/logs |
| Metrics | {endpoint}/v1/metrics |
Every request carries User-Agent: Synapse/<version>, so one allowlist entry covers
config-poll and telemetry push alike, and the two are distinguishable in a collector's own
access log.
Where configuration comes from
Four sources, highest precedence first. This ordering matters because a stray environment variable silently outranks your config file:
OTEL_EXPORTER_OTLP_*environment variables — the standard OTel knobs, and top precedence, the same as every other OTel SDK.- The
telemetry.otlpblock above. - Legacy platform fallback — if neither of the above sets an endpoint but
platform.api_keyandplatform.base_urlare set, Synapse pushes to{base_url}/v1/telemetry, where logs and metrics share one path. - File sink only —
/var/log/synapse/telemetry.log.
telemetry.enabled: false is a master switch: no OTLP push happens regardless of
endpoint or environment. The local file sink keeps working.
enabled defaults to true, so a config with no telemetry: section at all still emits —
to the platform if platform.api_key is set, otherwise to the file sink. If you want
nothing leaving the host, set enabled: false explicitly.
Metrics
| Metric | Type | Unit | |
|---|---|---|---|
synapse.kernel.drops | Counter | {packet} | Packets dropped by the in-kernel BPF firewall, partitioned by layer |
synapse.block.events.access_rules | Counter | {event} | Static IP / CIDR / country / ASN blocklists |
synapse.block.events.smart_firewall | Counter | {event} | JA4-family fingerprint matches, kernel and userland |
synapse.block.events.ids | Counter | {event} | Thalamus Suricata-style signatures |
synapse.block.events.waf | Counter | {event} | HTTP-layer wirefilter rules |
synapse.block.events.threat_intel | Counter | {event} | IP-reputation hits |
synapse.block.events.captcha | Counter | {event} | Challenge issued or failed |
synapse.block.events.rate_limit | Counter | {event} | Per-route or per-fingerprint rate hits |
synapse.ban.active | Gauge | {ban} | Bans currently held, observed on the collection interval |
synapse.ban.records_dropped | Counter | {record} | Ban-ledger records dropped |
synapse.telemetry.emit_latency | Histogram | ms | Cost of the emit path itself: file write, OTLP enqueue and metrics record |
Attributes
Two attribute keys carry the dimensions, and they are attached to different metrics:
| Attribute | On | Values |
|---|---|---|
synapse.action | every block.events.* counter | block, drop, allow, log, notice, ratelimit, captcha |
synapse.layer | synapse.kernel.drops, synapse.ban.active | the enforcing layer |
So the layer is encoded two different ways depending on the metric, and this is the thing
that trips up a first query: block.events.* is one counter per layer, with the
action as its attribute. A total across all enforcement is therefore a sum over the
metric family, not a single series filtered by a layer label:
sum(rate({__name__=~"synapse_block_events_.*"}[5m]))
Whereas synapse.kernel.drops and synapse.ban.active are single series that you do
split by synapse.layer:
sum by (synapse_layer) (rate(synapse_kernel_drops[5m]))
synapse.telemetry.emit_latency carries explicit bucket boundaries from 0.01 ms to 100 ms.
It measures Synapse's own observability overhead, which is the number to look at before
blaming telemetry for a latency regression.
Metrics export on a periodic reader. Override the interval with the standard
OTEL_METRIC_EXPORT_INTERVAL environment variable; it applies to every configured
destination.
Identifying an agent
Every record carries service.instance.id, set to Synapse's derived agent ID — a stable
hash of the agent name that survives restarts, which is what you want for a long-running
dashboard.
The fallback is the process PID, which changes on every restart. That is useful for spotting restarts and useless for a dashboard grouping by instance — a restart looks like one agent disappearing and a different one appearing.
Sampling
Two independent sample rates, because the streams differ in volume by orders of magnitude:
telemetry.otlp.sample_rate— the general log stream.telemetry.access_log.sample_rate— one record per proxied HTTP request, which on a busy proxy dominates everything else.
always_keep_errors makes 4xx and 5xx responses bypass the sampler, so a low access-log
sample rate still preserves every failure. Leave it on; sampling away errors is how a
sampled stream becomes misleading rather than merely incomplete.
Metrics are not sampled — they are aggregated at the source, so the export interval, not a sample rate, is what controls their volume.
Limits worth knowing
- The queue is bounded. If a collector is unreachable, records buffer and then drop at enqueue once the queue fills, with a warning logged. Telemetry backpressure never becomes request backpressure, which is the right trade — but it does mean a long collector outage is a gap, not a backlog.
- Batching adds delay. Records flush on a size threshold or a time cap, whichever comes first, so there is a small lag between a decision and its appearance downstream.
- No traces. Synapse exports logs and metrics. It is not instrumented for distributed tracing, and a collector expecting spans from it will see none.
See also
- Security Event Export — the event model behind the log stream, and the non-OTLP sinks
- Axiom and Honeycomb — backend-specific walkthroughs
- Configuration — the full
telemetryblock