§ Reference · Data and formats
Telemetry
OpenTelemetry traces and metrics for every command, resource and provider call, configured with the standard OTEL variables.
On this page
pravo emits OpenTelemetry traces and metrics. Both are off by default and cost almost nothing when off. The standard OTEL_* environment variables configure them, so any collector, backend or CI system that speaks OTLP works without pravo-specific settings.
| Variable | Effect |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
Setting any of them turns the matching signals on. |
OTEL_EXPORTER_OTLP_PROTOCOL, ..._TRACES_PROTOCOL, ..._METRICS_PROTOCOL |
http/protobuf (default) or grpc. |
OTEL_EXPORTER_OTLP_HEADERS, ..._TIMEOUT, ..._CERTIFICATE |
Passed to the exporters as the OpenTelemetry specification defines. |
OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER |
otlp, console (JSON on standard error) or none. Any other value turns the signal off. |
OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES |
The identity of the resource. The service is pravo unless you say otherwise. |
OTEL_SDK_DISABLED=true |
Turns everything off. |
TRACEPARENT, TRACESTATE |
A trace started by the caller continues in pravo. See Propagation. |
Export never affects a command. A collector that is down costs at most the flush timeout, five seconds, produces one warning on standard error and does not change the exit code. With --json, telemetry never writes to standard output.
Work in an empty directory.
package demo
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
hello: $local.#Resource.local_file & {
filename: "hello.txt"
content: "s3cr3t-value"
}
Traces
One trace per command. The command span is the root, and everything else nests under it. With the console exporter, each span is one JSON object on standard error:
provider ValidateProviderConfig
provider ConfigureProvider
provider ValidateResourceConfig
provider PlanResourceChange
resource hello
engine plan
pravo plan
The spans end in this order: the provider calls, the resource, the engine, the command. Their nesting:
pravo apply command: actor, reason, exit code, version
└── engine apply the run: transaction id, added, changed, destroyed
├── resource pet one per resource and phase (plan, apply)
│ ├── provider ValidateResourceConfig
│ ├── provider PlanResourceChange
│ └── provider ApplyResourceChange
└── resource hello
The address in resource <address> is the CUE path of the resource, such as pet, api.config or servers[0]. pravo plan and pravo drift have an engine plan span in the same position. Commands that start a plan inside them (apply without a file, why) show both. Under pravo mcp, each tool call is a span and the pravo process it runs continues the same trace.
| Span | Attributes |
|---|---|
command (pravo plan, ...) |
pravo.command, pravo.version, pravo.actor, pravo.reason, pravo.json, pravo.exit_code |
engine plan, engine apply |
pravo.actor, pravo.reason, vcs.revision, pravo.destroy, pravo.txn. For plan: pravo.plan.add, .change, .destroy and pravo.nodes.total, .planned, .skipped, .refreshed, .fresh, .not_read, .drifted, pravo.ephemeral.opened, pravo.function.calls. For apply: what was added, changed and destroyed. |
resource <address> |
pravo.resource.address, pravo.resource.type, pravo.mode, pravo.provider, pravo.phase, pravo.action, pravo.skipped, pravo.refreshed, pravo.drifted, pravo.deferred |
mcp tools/call <tool> |
mcp.tool, pravo.actor, pravo.exit_code |
provider <RPC> |
rpc.system, rpc.service, rpc.method, pravo.provider, pravo.protocol, pravo.resource.type, pravo.warnings |
{"pravo.command":"pravo plan","pravo.version":"0.1.0-dev","pravo.actor":"","pravo.reason":"","pravo.json":false,"pravo.exit_code":0}
{"pravo.resource.address":"hello","pravo.resource.type":"local_file","pravo.mode":"managed","pravo.provider":"local","pravo.phase":"plan","pravo.action":"create","pravo.skipped":false,"pravo.refreshed":false,"pravo.drifted":false,"pravo.deferred":false}
Failures are recorded on the span that failed, with its error, so the first red span in a trace is where the run stopped.
What spans never contain
Addresses, types, actions, counts and outcomes are recorded. Attribute values never are: not configuration, not state, not provider ids, not outputs. A resource's values can be secrets, and a trace backend is rarely where they belong. Error messages are recorded as they are printed, so a provider that puts a value in its own error text puts it in the span too.
The plan shows the content of the file. The trace does not hold it:
1
0
Debug events
The events of the debug log (-v) are attached to the spans they belong to. A task event sits on its resource span, with the action and the reasons. The events plan, tasks run and settled sit on the engine span. The events of loading the specification, opening the state, reading the schemas and choosing the providers sit on the command span. They are recorded whenever traces are on, whether or not -v is set.
{"Name":"resource hello","events":["configuring provider","task"]}
{"Name":"engine plan","events":["plan","tasks run","settled"]}
{"Name":"pravo plan","events":["loaded","state opened","provider version from the lock file","provider found installed","schema from the cache","schemas ready"]}
Metrics
| Instrument | Type | Attributes |
|---|---|---|
pravo.run.duration |
histogram, seconds | pravo.command, pravo.exit_code |
pravo.provider.rpc.duration |
histogram, seconds | pravo.provider, rpc.method, error |
pravo.resource.operations |
counter | pravo.mode, pravo.resource.type, pravo.action, pravo.outcome |
pravo.plan.nodes |
counter | pravo.disposition: planned, skipped, refreshed, fresh, not_read, drifted. fresh counts the records that were not read and are within their refresh age, not_read the records that were not read and are older, and neither counts any under -refresh=false. |
pravo.lock.acquire.duration |
histogram, seconds | pravo.locks |
pravo.rejections |
counter | pravo.reason: stale, locked, policy |
pravo.ephemeral.opened |
counter | pravo.resource.type |
pravo.function.calls |
counter | pravo.function |
A command lives for seconds, so metrics are exported once, when it ends. An instrument appears only after something was recorded on it.
pravo.plan.nodes
pravo.provider.rpc.duration
pravo.resource.operations
pravo.run.duration
A policy rejection is a run that exited with code 5: a plan that violated a policy named with --policy, a plain apply under a policy, or a policy file that could not be used. A module that does not match its lock also exits with code 5 and is counted under the same reason. A locked rejection is exit code 4, a lock or a reservation held by another actor. A stale rejection is exit code 3.
The counter measures refusals. It cannot measure omission: a run that was not given --policy is not checked, and nothing is counted for it. Whether a run was given --policy can be read afterwards from the command line recorded on its transaction (pravo state log). That record is evidence and not a control.
Finding a run again
When tracing is on, the trace id is stored where you already look:
- every transaction in the state has a
trace_id(pravo state log --json); pravo plan --jsondocuments carrytrace_id;- apply and destroy events (
--json) carrytrace_idon every line.
When tracing is off, the field is omitted. Given a transaction in the log, open its trace. Given a trace, the pravo.txn attribute of the engine span names the transaction.
the document carries the trace id of the command
Propagation
- In.
TRACEPARENTandTRACESTATEin the environment, the convention that CI systems and orchestrators use for command-line tools, make the command span a child of the caller's span. A pipeline's trace then shows pravo's work inside it. - Through MCP.
pravo mcpcreates a span per tool call and passes its context to the pravo process it runs. An agent's tool call, the plan or apply it caused and the provider calls under it are one trace. A call that the server refuses before it runs anything, because an argument is empty or begins with-, starts no process and has no span. - Out. Provider processes do not receive trace context. Their own telemetry, if any, is separate.
{"trace":"4bf92f3577b34da6a3ce929d0e0e4736","parent":"00f067aa0ba902b7"}
A collector that is down
pravo: telemetry export problem (further ones are not shown): traces export: Post "http://127.0.0.1:1/v1/traces": dial tcp 127.0.0.1:1: connect: connection refused
exit=0
Not included
Logs are not exported as an OpenTelemetry signal. The transaction log and the --json event stream cover the same ground and carry the trace id. There are no metrics for the contents of resources, by the same rule as spans.
See also
- Find out why a run fails follows a command in a trace.
- Global flags, environment and exit codes lists the other variables.