Tutorial 40: OpenTelemetry Observability for Agent Governance

April 24, 2026 · View on GitHub

Time: 10 minutes · Level: Intermediate · Prerequisites: Tutorial 36 (govern basics)

What You'll Build

Full observability for your governed agents: traces showing every policy evaluation, metrics for deny rates and latency, and integration with your existing monitoring stack (Datadog, Grafana, Azure Monitor).

Why This Matters

When agents run in production, you need to answer:

  • How many actions is the policy engine evaluating per second?
  • What's the P99 evaluation latency?
  • Which rules deny the most actions?
  • Which agents trigger the most approvals?

AGT's OTel integration answers all of these with zero custom code.


Step 1: Enable OTel (One Line)

from agentmesh.governance import enable_otel

enable_otel(service_name="customer-service-agent")

That's it. All governance operations now emit OTel spans and metrics.

Step 2: What Gets Emitted

Spans

Every governance operation creates a span with rich attributes:

Span: agt.policy.evaluate
  ├── agt.agent.id = "customer-service-agent-1"
  ├── agt.policy.stage = "pre_tool"
  ├── agt.policy.action = "deny"
  ├── agt.policy.rule = "block-pii-export"
  └── agt.policy.name = "org-baseline"

Span: agt.approval.request
  ├── agt.agent.id = "financial-agent-2"
  ├── agt.policy.rule = "approve-large-transfer"
  ├── agt.approval.outcome = "approved"
  └── agt.approval.approver = "jane@company.com"

Span: agt.trust.verify
  ├── agt.agent.id = "partner-agent-x"
  ├── agt.trust.score = 0.85
  └── agt.trust.tier = "trusted"

Metrics

MetricTypeLabelsDescription
agt.policy.evaluationsCounteraction, stageTotal evaluations
agt.policy.denialsCounterrule, tool, stageDenial count
agt.policy.latency_msHistogramaction, stageEvaluation latency
agt.approval.requestsCounterrule, outcomeApproval workflow count

Step 3: Use in Your Agent Code

from agentmesh.governance import (
    enable_otel,
    govern,
    trace_policy_evaluation,
    trace_trust_verification,
    record_denial,
)

# Enable at startup
enable_otel(service_name="my-agent")

# govern() automatically emits spans for every call
safe_tool = govern(my_tool, policy="policy.yaml")
safe_tool(action="read")   # → span emitted with action=allow
safe_tool(action="export")  # → span emitted with action=deny, denial metric recorded

Step 4: Manual Tracing (Advanced)

For custom governance code outside govern():

from agentmesh.governance import trace_policy_evaluation, trace_trust_verification

# Trace a custom policy evaluation
with trace_policy_evaluation(agent_id="agent-1", stage="pre_tool") as result:
    decision = engine.evaluate("agent-1", context, stage="pre_tool")
    result["action"] = decision.action
    result["rule"] = decision.matched_rule
    result["allowed"] = decision.allowed
# Span automatically closed with attributes populated

# Trace a trust verification
with trace_trust_verification(agent_id="partner-agent") as result:
    score = trust_manager.verify("partner-agent")
    result["score"] = score.value
    result["tier"] = score.tier

Step 5: Connect to Your Backend

Grafana / Prometheus

from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.prometheus import PrometheusMetricReader

# Prometheus scrape endpoint at :8000/metrics
reader = PrometheusMetricReader()

Azure Monitor

from azure.monitor.opentelemetry import configure_azure_monitor

configure_azure_monitor(connection_string="InstrumentationKey=...")
enable_otel(service_name="my-agent")

Datadog

# Set environment variables:
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317

enable_otel(service_name="my-agent")

Step 6: Example Dashboard Queries

PromQL: Deny Rate by Rule (Last Hour)

sum(rate(agt_policy_denials_total[1h])) by (agt_policy_rule)

PromQL: P99 Evaluation Latency

histogram_quantile(0.99, rate(agt_policy_latency_ms_bucket[5m]))

PromQL: Approval Rate

sum(rate(agt_approval_requests_total{agt_approval_outcome="approved"}[1h]))
/
sum(rate(agt_approval_requests_total[1h]))

Zero Overhead When Disabled

If you don't call enable_otel(), all tracing functions are no-ops:

# This works fine — no spans, no metrics, no performance impact
with trace_policy_evaluation(agent_id="a") as r:
    r["action"] = "allow"
# Context manager completes, result dict populated, zero OTel overhead

Semantic Attributes Reference

AttributeTypeDescription
agt.agent.idstringAgent identifier
agt.policy.rulestringMatched rule name
agt.policy.actionstringallow / deny / warn / require_approval
agt.policy.stagestringpre_input / pre_tool / post_tool / pre_output
agt.policy.namestringPolicy name
agt.trust.scorefloatTrust verification score (0.0–1.0)
agt.trust.tierstringTrust tier (untrusted / provisional / trusted / verified)
agt.tool.namestringTool that triggered the evaluation
agt.approval.outcomestringapproved / rejected
agt.approval.approverstringIdentity of the approver

What to Try Next

  • Tutorial 41: Advisory layer with OTel tracing (see advisory decisions in your dashboard)
  • Tutorial 37: Multi-stage pipeline (trace each stage independently)