OpenTelemetry integration
Wiring fastapi-guard's spans and metrics into your existing OTel pipeline - Grafana, Tempo, Honeycomb, Datadog, whatever you already run.
If your app already exports spans and metrics to an OTel collector, fastapi-guard's signals should land there too - same backend, same correlation IDs, same dashboards.
Don't run two telemetry systems
Without OTel: application telemetry goes to your existing collector, security events go to the Guard Core dashboard. Correlation across the two is manual. Alerting and SLOs split across tools. Useful for security teams, painful for ops.
With OTel: one pipeline, all signals. Trace context propagates across the boundary. Existing dashboards already understand the data. Security events become first-class operational data.
Protocol - OTLP/HTTP only, port 4318 not 4317
fastapi-guard speaks OTLP over HTTP/protobuf. gRPC isn't wired up. Pointing the exporter at a gRPC-only receiver fails silently in some collectors, or with an obscure HTTP error in others.
- Use:
http://otel-collector:4318- OTLP/HTTP, default HTTP port. Most collectors (otel-collector, Tempo, Honeycomb, Datadog Agent) accept it out of the box. - Don't use:
http://otel-collector:4317- OTLP/gRPC. gRPC is the OTel default in many tutorials, but this code path doesn't import the gRPC exporter. Wrong protocol = no data.
Endpoint suffix handling: you pass the base URL. The exporter appends /v1/traces and /v1/metrics per signal. /v1/logs is recognized but no logs exporter is instantiated (see Logs section below).
Spans - guard.event.{event_type}
Every detection, ban, and rate-limit trip becomes a span. The name pattern lets you filter for security activity with a single prefix match.
Concrete examples:
guard.event.penetration_attemptguard.event.ip_bannedguard.event.rate_limit_exceeded
Core attributes (every span):
guard.event_type- what firedguard.ip_address- resolved client IPguard.action_taken- block / log / throttle / banguard.reason- human-readable causeguard.endpoint- route pathguard.method- HTTP verbguard.status_code- response status
Filter on guard.* in your tracing UI to scope to security-relevant traffic. Pair with guard.event_type for per-class breakdowns.
Enrichment - optional attributes for richer queries
When enable_enrichment is True, every span gets additional context. Adds cardinality, so enable deliberately.
Scoping: guard.project_id, guard.service_name, guard.deployment_environment
Scoring: guard.threat_score, guard.rule_id, guard.rule_version
Behavioral context: guard.behavior_key (SHA-256 of IP | service | time_bucket, collapses noisy traffic from one source into a queryable group without exposing raw IPs in span data), guard.recent_event_count
Metrics - three signals, all tagged with endpoint
Beyond spans, the agent records three top-level metrics. Each carries the endpoint label out of the box.
| Type | Name | Unit | What |
|---|---|---|---|
| Histogram | guard.request.duration | seconds | Time each request spent in the middleware pipeline. Use p95/p99 to spot detection overhead spikes. |
| Counter | guard.request.count | events | Total requests evaluated. Use as a denominator for error rates. |
| Counter | guard.error.count | events | Requests the middleware rejected. Spikes here usually correlate with a specific attack class. |
Logs - not exported via OTLP
In this version, OTLP carries spans and metrics only. The /v1/logs path is recognized in endpoint parsing, but no logs exporter is instantiated. Python's standard logging keeps emitting locally. You choose how to ship.
If you want logs in your OTel pipeline, run a sidecar log shipper that tails your app's stdout/stderr and forwards as OTel logs: Fluent Bit, Vector, OpenTelemetry Collector (filelogreceiver), Promtail.
Resource attributes - service identity and environment tagging
Two SecurityConfig fields shape what your collector sees as the source: otel_service_name (the identity) and otel_resource_attributes (additional tags).
otel_service_name - auto-applied as the service.name resource attribute. Defaults to "guard-core" if unset. Set this per environment to avoid prod and staging spans collapsing into one bucket.
otel_resource_attributes - a dict of additional resource attributes. Merges additively with the auto-set service.name. If your dict includes "service.name", it WILL override the auto-set value.
Wiring
SecurityConfig(
enable_otel=enable_otel,
otel_service_name=otel_service_name,
otel_exporter_endpoint=otel_endpoint or None,
otel_resource_attributes={
"deployment.environment": deployment_environment,
},
)enable_otel- master switch. False = exporter not constructed at all.otel_service_name- becomesservice.nameon every signal. Set per env.otel_exporter_endpoint- base URL of your OTLP/HTTP receiver (port 4318).otel_resource_attributes- extra dict merged onto resource. Used here for env tagging.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Spans not arriving in the collector | Pointed at port 4317 (gRPC) instead of 4318 (HTTP) | Switch to port 4318. Confirm collector has OTLP/HTTP receiver enabled. |
Service name shows up as unknown or guard-core | otel_service_name not set explicitly - defaults to guard-core | Set per environment in app.py (e.g., from an env var). |
| Want logs alongside spans and metrics | Not supported via OTLP in this version | Run a sidecar log shipper (Fluent Bit, Vector, otel-collector filelogreceiver) |
OTEL_RESOURCE_ATTRIBUTES env var is ignored | Expected - this code path only reads SecurityConfig fields | Move env-var-sourced attributes into otel_resource_attributes at startup. |