OpenTelemetry integration

Wiring fastapi-guard's spans and metrics into your existing OTel pipeline - Grafana, Tempo, Honeycomb, Datadog, whatever you already run.

4 min read · verified against [email protected]

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_attempt
  • guard.event.ip_banned
  • guard.event.rate_limit_exceeded

Core attributes (every span):

  • guard.event_type - what fired
  • guard.ip_address - resolved client IP
  • guard.action_taken - block / log / throttle / ban
  • guard.reason - human-readable cause
  • guard.endpoint - route path
  • guard.method - HTTP verb
  • guard.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.

TypeNameUnitWhat
Histogramguard.request.durationsecondsTime each request spent in the middleware pipeline. Use p95/p99 to spot detection overhead spikes.
Counterguard.request.counteventsTotal requests evaluated. Use as a denominator for error rates.
Counterguard.error.counteventsRequests 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 - becomes service.name on 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

SymptomCauseFix
Spans not arriving in the collectorPointed 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-coreotel_service_name not set explicitly - defaults to guard-coreSet per environment in app.py (e.g., from an env var).
Want logs alongside spans and metricsNot supported via OTLP in this versionRun a sidecar log shipper (Fluent Bit, Vector, otel-collector filelogreceiver)
OTEL_RESOURCE_ATTRIBUTES env var is ignoredExpected - this code path only reads SecurityConfig fieldsMove env-var-sourced attributes into otel_resource_attributes at startup.