Edictum
Reference

OpenTelemetry Integration

Edictum instruments the pipeline with OpenTelemetry spans and metrics.

AI Assistance

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):

AttributeTypeDescription
tool.namestringName of the tool
tool.side_effectstringSide-effect classification: pure, read, write, irreversible
tool.call_indexintSequential call number within the run
governance.environmentstringDeployment environment
governance.run_idstringUnique run identifier

Set during contract evaluation:

AttributeTypeDescription
governance.actionstringDecision outcome: allowed, denied, would_deny, approved
governance.reasonstringDenial reason (only set when denied)
governance.would_deny_reasonstringReason for would-deny in observe mode (only set when action is would_deny)
edictum.policy_versionstringSHA-256 hash of the active YAML contract file

Set after tool execution (post-execution):

AttributeTypeDescription
governance.tool_successboolWhether the tool call succeeded
governance.postconditions_passedboolWhether 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:

AttributeTypeDescription
edictum.tool.namestringTool name
edictum.tool.argsstringJSON-serialized tool arguments
edictum.verdictstringEvaluation verdict
edictum.verdict.reasonstringReason for the verdict
edictum.decision.namestringContract ID that produced the decision
edictum.decision.sourcestringDecision source (precondition, sandbox, session, etc.)
edictum.side_effectstringTool side-effect classification
edictum.environmentstringExecution environment
edictum.modestringEnforcement mode (enforce or observe)
edictum.policy_versionstringSHA-256 hash of the active bundle
edictum.policy_errorbooltrue if a contract evaluation error occurred (fail-closed)
edictum.principal.rolestringPrincipal role (if set)
edictum.principal.user_idstringPrincipal user ID (if set)
edictum.principal.org_idstringPrincipal org ID (if set)
edictum.principal.ticket_refstringPrincipal ticket ref (if set)
edictum.session.attempt_countintSession attempt counter
edictum.session.execution_countintSession 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 NameTypeLabelsDescription
edictum.calls.deniedCountertool.nameIncremented each time a tool call is denied
edictum.calls.allowedCountertool.nameIncremented 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 endpoint

Parameters:

ParameterTypeDefaultDescription
service_namestr"edictum-agent"OTel service name resource attribute
endpointstr"http://localhost:4317"OTLP collector endpoint
protocolstr"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_attributesdict | NoneNoneAdditional OTel resource attributes
edictum_versionstr | NoneNoneEdictum version tag
insecureboolTrueUse plaintext for gRPC. Set to False for TLS-enabled collectors. Has no effect on the HTTP exporter (use https:// in endpoint instead).
forceboolFalseReplace 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 VarOverrides
OTEL_SERVICE_NAMEservice_name
OTEL_EXPORTER_OTLP_ENDPOINTendpoint
OTEL_EXPORTER_OTLP_PROTOCOLprotocol
OTEL_RESOURCE_ATTRIBUTESMerged 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 providers

Graceful No-Op Behavior

If opentelemetry is not installed, GovernanceTelemetry operates as a complete no-op:

  • start_tool_span() returns an internal _NoOpSpan object that silently accepts all attribute and event calls
  • record_denial() and record_allowed() do nothing
  • No exceptions are raised
  • No performance cost beyond a single _HAS_OTEL boolean 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 ignored

Correlating 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 = true

No additional configuration is required for this to work. The standard OTel context propagation handles span parenting automatically.

Last updated on

On this page