OpenTelemetry Integration
Edictum instruments the pipeline with OpenTelemetry spans and metrics.
Right page if: you need the exact OTel span attribute names, metric names, or `configure_otel()` parameters for building dashboards or querying your observability backend. Wrong page if: you want a setup walkthrough with Grafana -- see https://docs.edictum.ai/docs/guides/observability. For local audit event sinks (Stdout, File), see https://docs.edictum.ai/docs/reference/audit-sinks. Gotcha: if opentelemetry is not installed, all instrumentation degrades to a silent no-op with zero overhead -- no code changes needed. `configure_otel()` is a no-op if a TracerProvider already exists (pass `force=True` to override).
Edictum instruments the pipeline with OpenTelemetry spans and metrics.
When opentelemetry is not installed, all instrumentation degrades to silent no-ops
with zero overhead.
Installation
pip install edictum[otel]This installs the opentelemetry-api and opentelemetry-sdk packages. You will
also need an exporter for your backend (e.g. opentelemetry-exporter-otlp for OTLP,
opentelemetry-exporter-jaeger for Jaeger).
What Gets Instrumented
GovernanceTelemetry creates an OTel tracer named "edictum" and a meter named
"edictum". These produce spans and counters that track every tool call through
the pipeline.
Spans
Each tool call produces one span:
tool.execute {tool_name}For example, a call to the Bash tool produces a span named
tool.execute Bash. The span begins when Edictum starts evaluating the
envelope and ends after post-execution checks complete (or after denial, if
the call is denied).
Span Attributes
Attributes are set at different lifecycle stages.
Set at span creation (pre-execution):
| Attribute | Type | Description |
|---|---|---|
tool.name | string | Name of the tool |
tool.side_effect | string | Side-effect classification: pure, read, write, irreversible |
tool.call_index | int | Sequential call number within the run |
governance.environment | string | Deployment environment |
governance.run_id | string | Unique run identifier |
Set during contract evaluation:
| Attribute | Type | Description |
|---|---|---|
governance.action | string | Decision outcome: allowed, denied, would_deny, approved |
governance.reason | string | Denial reason (only set when denied) |
governance.would_deny_reason | string | Reason for would-deny in observe mode (only set when action is would_deny) |
edictum.policy_version | string | SHA-256 hash of the active YAML contract file |
Set after tool execution (post-execution):
| Attribute | Type | Description |
|---|---|---|
governance.tool_success | bool | Whether the tool call succeeded |
governance.postconditions_passed | bool | Whether all postconditions passed |
Postcondition Findings (v0.5.1+)
When a postcondition produces a finding, two things happen. On the tool.execute span, the governance.postconditions_passed attribute is set to false, making it easy to filter for tool calls that triggered postcondition warnings in your observability backend. Separately, the AuditEvent dataclass includes the full finding details in its contracts_evaluated list -- but this data lives on the audit event, not on the OTel span. The edictum.evaluate span only carries summary attributes (edictum.verdict, edictum.verdict.reason). For the full per-contract breakdown, query the audit log. See findings.md for the structured Finding interface.
edictum.evaluate Span
A second span edictum.evaluate is emitted per evaluation with detailed governance context:
| Attribute | Type | Description |
|---|---|---|
edictum.tool.name | string | Tool name |
edictum.tool.args | string | JSON-serialized tool arguments |
edictum.verdict | string | Evaluation verdict |
edictum.verdict.reason | string | Reason for the verdict |
edictum.decision.name | string | Contract ID that produced the decision |
edictum.decision.source | string | Decision source (precondition, sandbox, session, etc.) |
edictum.side_effect | string | Tool side-effect classification |
edictum.environment | string | Execution environment |
edictum.mode | string | Enforcement mode (enforce or observe) |
edictum.policy_version | string | SHA-256 hash of the active bundle |
edictum.policy_error | bool | true if a contract evaluation error occurred (fail-closed) |
edictum.principal.role | string | Principal role (if set) |
edictum.principal.user_id | string | Principal user ID (if set) |
edictum.principal.org_id | string | Principal org ID (if set) |
edictum.principal.ticket_ref | string | Principal ticket ref (if set) |
edictum.session.attempt_count | int | Session attempt counter |
edictum.session.execution_count | int | Session execution counter |
Denied calls (and approval-denied / approval-timeout) set the span status to ERROR. All other outcomes set OK.
Metrics
Two counters are registered under the edictum meter:
| Metric Name | Type | Labels | Description |
|---|---|---|---|
edictum.calls.denied | Counter | tool.name | Incremented each time a tool call is denied |
edictum.calls.allowed | Counter | tool.name | Incremented each time a tool call is allowed |
These counters let you build dashboards that answer questions like:
- What percentage of tool calls are being denied?
- Which tools trigger the most denials?
- How does denial rate change after a contract update?
Quick Setup with configure_otel()
The simplest way to enable OTel is the configure_otel() helper from the
edictum.otel module. Call it once at startup:
from edictum.otel import configure_otel
from edictum import Edictum
configure_otel(
service_name="my-agent",
endpoint="http://localhost:4317",
)
guard = Edictum(...)
# Enforcement spans are now emitted to the configured OTLP endpointParameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
service_name | str | "edictum-agent" | OTel service name resource attribute |
endpoint | str | "http://localhost:4317" | OTLP collector endpoint |
protocol | str | "grpc" | Transport protocol: "grpc", "http", or "http/protobuf". Any non-"grpc" value selects the HTTP exporter. When HTTP is selected and endpoint is the default, it auto-adjusts to http://localhost:4318/v1/traces. |
resource_attributes | dict | None | None | Additional OTel resource attributes |
edictum_version | str | None | None | Edictum version tag |
insecure | bool | True | Use plaintext for gRPC. Set to False for TLS-enabled collectors. Has no effect on the HTTP exporter (use https:// in endpoint instead). |
force | bool | False | Replace an existing TracerProvider. By default, configure_otel() is a no-op if a provider is already set. |
If a TracerProvider is already configured (e.g. by the host application or
another SDK), configure_otel() is a no-op. This prevents Edictum from
clobbering an existing OTel setup. Pass force=True to override.
Standard OTel environment variables take precedence over function arguments:
| Env Var | Overrides |
|---|---|
OTEL_SERVICE_NAME | service_name |
OTEL_EXPORTER_OTLP_ENDPOINT | endpoint |
OTEL_EXPORTER_OTLP_PROTOCOL | protocol |
OTEL_RESOURCE_ATTRIBUTES | Merged with resource_attributes (env wins on conflict) |
Configure via environment variables if you prefer:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
export OTEL_SERVICE_NAME="my-agent"Advanced Setup with OTLP Exporter
For full control over tracer and meter providers (e.g., custom exporters, metric readers, or resource attributes), configure them directly:
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
# Traces
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)
trace.set_tracer_provider(tracer_provider)
# Metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint="http://localhost:4317"),
export_interval_millis=10_000,
)
meter_provider = MeterProvider(metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
# Now import and use Edictum — telemetry activates automatically
from edictum import Edictum
guard = Edictum(...)
# GovernanceTelemetry picks up the global tracer and meter providersGraceful No-Op Behavior
If opentelemetry is not installed, GovernanceTelemetry operates as a complete
no-op:
start_tool_span()returns an internal_NoOpSpanobject that silently accepts all attribute and event callsrecord_denial()andrecord_allowed()do nothing- No exceptions are raised
- No performance cost beyond a single
_HAS_OTELboolean check per call
This means you can leave telemetry wired into your pipeline configuration unconditionally. When deploying to an environment without OTel, there is no need to change code or configuration -- Edictum simply skips all instrumentation.
from edictum.telemetry import GovernanceTelemetry
telemetry = GovernanceTelemetry()
# Without opentelemetry installed:
span = telemetry.start_tool_span(envelope) # returns _NoOpSpan
span.set_attribute("governance.action", "allowed") # silently ignored
span.end() # silently ignored
telemetry.record_allowed(envelope) # silently ignoredCorrelating with Application Traces
Edictum spans participate in the standard OTel context propagation. If your application already creates spans (e.g. for an HTTP request or an agent loop iteration), Edictum spans appear as children of whatever span is active when the pipeline runs. This gives you a single trace that shows:
HTTP POST /agent/run [your app]
└─ agent.loop.iteration [your app]
└─ tool.execute Bash [edictum]
governance.action = "allowed"
governance.tool_success = trueNo additional configuration is required for this to work. The standard OTel context propagation handles span parenting automatically.
Last updated on