Skip to content

Telemetry

TornadoAPI Guard emits security events and request metrics through a composable telemetry pipeline provided by guard-core. Events can be muted, metrics can be muted, individual security-check logs can be muted, and exports to OpenTelemetry and Logfire are opt-in.

The middleware installs and tears down the telemetry pipeline automatically when middleware.initialize() and middleware.shutdown() are called — you only set fields on SecurityConfig.

Two-tier model

Telemetry ships in two tiers:

Tier Prerequisites What every exporter sees
Raw (free) enable_otel=True and/or enable_logfire=True All event types, all metrics, W3C traceparent / tracestate propagation, muting. No guard.* enrichment fields.
Enriched (guard-agent tier) enable_agent=True + enable_enrichment=True Everything in Raw, plus per-event guard.project_id, guard.service.name, guard.deployment.environment, guard.threat_score, guard.rule.id + guard.rule.version, guard.behavior.correlation_key, guard.behavior.recent_event_count. Metrics also inherit project/service/environment tags.

Setting enable_enrichment=True without enable_agent=True raises ValidationError.

Config surface

Ten SecurityConfig fields control telemetry:

Field Type Default Purpose
muted_event_types set[str] set() Suppress these event types from every exporter.
muted_metric_types set[str] set() Suppress these metric types from every exporter.
muted_check_logs set[str] set() Suppress pipeline + in-check log output for these checks.
enable_otel bool False Enable OpenTelemetry span/metric export (requires guard-core[otel]).
otel_service_name str "guard-core" Service name for OpenTelemetry resource.
otel_exporter_endpoint str \| None None OTLP/HTTP endpoint. None uses OTel's default (localhost:4318).
otel_resource_attributes dict[str, str] {} Extra OpenTelemetry resource attributes (e.g. deployment.environment, service.version).
enable_logfire bool False Enable Logfire export (requires guard-core[logfire]).
logfire_service_name str "guard-core" Service name for Logfire.
enable_enrichment bool False Populate guard.* metadata on every event and metric. Requires enable_agent=True.

All three mute fields validate their contents at config time. Unknown values raise ValidationError and the error message lists the valid values.

Valid mute values

Drawn from constants in guard_core.core.events.event_types:

  • muted_event_types — access_denied, authentication_failed, behavior_violation, cloud_blocked, content_filtered, country_blocked, csp_violation, custom_request_check, decoding_error, decorator_violation, dynamic_rule_applied, dynamic_rule_updated, emergency_mode_activated, emergency_mode_block, geo_lookup_failed, https_enforced, ip_banned, ip_blocked, ip_unbanned, path_excluded, pattern_added, pattern_detected, pattern_removed, penetration_attempt, rate_limited, redis_connection, redis_error, security_bypass, security_headers_applied, user_agent_blocked
  • muted_metric_types — error_rate, request_count, response_time
  • muted_check_logs — authentication, cloud_ip_refresh, cloud_provider, custom_request, custom_validators, emergency_mode, https_enforcement, ip_security, rate_limit, referrer, request_logging, request_size_content, required_headers, route_config, suspicious_activity, time_window, user_agent

Muting events, metrics, and check logs

import tornado.web
from tornadoapi_guard import SecurityConfig, SecurityHandler, SecurityMiddleware

config = SecurityConfig(
    muted_event_types={"penetration_attempt"},
    muted_metric_types={"response_time"},
    muted_check_logs={"rate_limit", "user_agent"},
)

middleware = SecurityMiddleware(config=config)

async def startup() -> None:
    await middleware.initialize()

app = tornado.web.Application(
    [(r"/", SecurityHandler)],
    security_middleware=middleware,
)
  • muted_event_types short-circuits events before any exporter sees them.
  • muted_metric_types short-circuits metrics before any exporter sees them.
  • muted_check_logs suppresses both the pipeline's block/error log entries and the in-check log_activity() calls — both are gated on the same set.

Enabling OpenTelemetry

uv add tornadoapi-guard "guard-core[otel]"
poetry add tornadoapi-guard "guard-core[otel]"
pip install tornadoapi-guard "guard-core[otel]"
from tornadoapi_guard import SecurityConfig, SecurityMiddleware

config = SecurityConfig(
    enable_otel=True,
    otel_service_name="guard-prod",
    otel_exporter_endpoint="http://otel-collector.internal:4318",
    otel_resource_attributes={
        "deployment.environment": "prod",
        "service.version": "1.0.0",
    },
)

middleware = SecurityMiddleware(config=config)

async def startup() -> None:
    await middleware.initialize()

If otel_resource_attributes contains a service.name key it overrides otel_service_name (last-write-wins). Prefer setting the service name via otel_service_name and use otel_resource_attributes only for environment/version/region tags.

Incoming W3C traceparent headers are continued automatically — guard spans become children of the caller's trace. tracestate headers are forwarded alongside when present.

Three instruments are emitted when OTel is enabled:

  • guard.request.duration (histogram, seconds)
  • guard.request.count (counter)
  • guard.error.count (counter)

Any other metric type produces a one-line warning and is dropped.

Enabling Logfire

uv add tornadoapi-guard "guard-core[logfire]"
poetry add tornadoapi-guard "guard-core[logfire]"
pip install tornadoapi-guard "guard-core[logfire]"
from tornadoapi_guard import SecurityConfig, SecurityMiddleware

config = SecurityConfig(
    enable_logfire=True,
    logfire_service_name="guard-prod",
)

middleware = SecurityMiddleware(config=config)

async def startup() -> None:
    await middleware.initialize()

Events are emitted as logfire.span("guard.event.<event_type>", ...) and metrics as structured logs via logfire.info("guard.metric.<metric_type>", value=..., endpoint=..., **tags). When both enable_otel and enable_logfire are set, Logfire also observes the OpenTelemetry instruments automatically via its OTel bridge.

Both OTel and Logfire spans receive enrichment fields as span attributes when enable_enrichment=True (see next section).

Enabling enrichment

from tornadoapi_guard import SecurityConfig, SecurityMiddleware

config = SecurityConfig(
    enable_agent=True,
    agent_api_key="your-api-key",
    agent_project_id="proj-prod",
    enable_enrichment=True,
    enable_otel=True,
    otel_service_name="api",
    otel_resource_attributes={"deployment.environment": "prod"},
)

middleware = SecurityMiddleware(config=config)

async def startup() -> None:
    await middleware.initialize()

When enable_enrichment=True, every event and every metric routed through the middleware picks up the following guard.* keys inside CompositeAgentHandler:

Key Type Source Applied to
guard.project_id str agent_project_id events + metrics
guard.service.name str otel_service_name events + metrics
guard.deployment.environment str otel_resource_attributes["deployment.environment"] events + metrics
guard.threat_score int (0-100) Deterministic map of event_type → score (penetration_attempt=90, ip_banned=70, medium=50, rate_limited=20, default=20) events only
guard.rule.id str DynamicRuleManager.match_event(event) when the cached rule matched events only
guard.rule.version int Same source as guard.rule.id events only
guard.behavior.correlation_key str (16-char hex) SHA-256 prefix of ip \| service \| floor(now/300) — stable within a 5-minute window events only
guard.behavior.recent_event_count int Total events observed from the IP across all endpoints in the last 5 minutes events only

All fields are nullable; absence = unavailable context. Every exporter (guard-agent, OTel, Logfire) sees the same enriched payload. See the guard-core telemetry reference for the full behavior spec.

Incoming traceparent

When enable_otel=True and the request carries a W3C traceparent header, the guard event bus copies it into the event's metadata. The OTel handler extracts it with TraceContextTextMapPropagator and sets the resumed context as the parent of guard.event.<event_type> spans. This preserves the upstream trace across the guard layer.

Troubleshooting

Spans don't show up in your OTel backend

  1. Verify enable_otel=True is set on SecurityConfig.
  2. Check python -c "import opentelemetry.sdk" — if it raises ImportError, the handler logs opentelemetry-sdk not installed, OTEL handler disabled on startup.
  3. Confirm otel_exporter_endpoint points to an OTLP/HTTP receiver on port 4318 (not 4317 — that's gRPC).
  4. Confirm await middleware.initialize() runs before the Tornado server starts serving — the telemetry pipeline only comes up inside initialize().
  5. Confirm every protected route inherits from SecurityHandler; plain tornado.web.RequestHandler subclasses bypass the pipeline and emit no events.

Events aren't muted even though muted_event_types is set

Confirm the value is in the Valid mute values list above. A typo silently fails validation if the field is used elsewhere — check the startup logs for a ValidationError.

logfire.info() warning: "No logs or spans will be created until logfire.configure() has been called"

Guard Core configures Logfire inside middleware.initialize(). If the warning appears at runtime, initialize() has not yet completed — verify it is awaited during application startup.

ValidationError on startup mentioning an unknown check/event/metric

The error message lists the valid values. Common typos: "suspicious" instead of "suspicious_activity", "latency" instead of "response_time".

Reference

The full field schema and pipeline behaviour is maintained in the guard-core telemetry reference.