Files

Optional harness telemetry

Instance-owned OpenTelemetry traces, metrics and lifecycle logs. Disabled by default; no global subscriber/provider or default vendor destination. The server reference composes export with optional local recording. Recording has its own bounded writer and independently configurable SQLite or PostgreSQL connection. Enabling recording does not enable remote metadata/content. Neither system is a lossless billing ledger or security audit log.

Configure an exporter

Add telemetry to the server-reference JSON configuration:

{
  "telemetry": {
    "endpoint": "http://127.0.0.1:4318",
    "deployment": "development"
  }
}

Use a collector base URL, optionally with a path. The exporter appends /v1/traces, /v1/metrics and /v1/logs. HTTPS is required except loopback HTTP. URL credentials, queries, fragments and signal-specific endpoint paths reject at startup. Unreachable exporters do not block agent execution or service readiness. Authenticated health responses expose local queue/export-failure counters.

For Langfuse, select traces only. Configure authentication using a named environment variable containing Basic <base64(public-key:secret-key)>:

{
  "telemetry": {
    "endpoint": "https://cloud.langfuse.com/api/public/otel",
    "deployment": "production",
    "signals": ["traces"],
    "acknowledgement": "langfuse_v4",
    "headers": [
      {"name": "Authorization", "value_env": "OW_TELEMETRY_AUTH"},
      {"name": "x-langfuse-ingestion-version", "value": "4"}
    ]
  }
}

Supply the secret through the application's secret manager or deployment secret; never put it in JSON, a bundle, URL or process argument. Header values are resolved at startup; rotating them requires restarting the exporter. Nonsecret protocol headers can use value. Authentication headers require value_env. Do not reuse model/service credentials. Langfuse ingests traces; send all signals to an OTel collector when you also need metrics/logs and configure its destinations separately. See Langfuse's OTLP integration.

The transport has verified TLS, no redirects/ambient proxies, no inherited OTEL headers or automatic resource detection, no exporter retries, a three-second HTTP deadline and a 64-KiB response cap. Custom CA configuration is not exposed; use a trusted local collector or app-owned components when required. Binary and declared JSON OTLP acknowledgements are validated. acknowledgement: "langfuse_v4" explicitly accepts Langfuse v4's identified ingestion-job receipt as well as OTLP responses; it requires traces-only export. The default "otlp" rejects that vendor envelope. Partial rejection, warnings, malformed or oversized acknowledgements and non-success HTTP responses count as failed batches. Receiver error text is never copied into diagnostic errors. HTTP success is not proof that a downstream vendor has durably indexed the data.

Attribute and privacy contract

attributes.json is the machine-readable registry for exported keys, types, capture classes and allowed metric dimensions. Its schema is version 1, with a selected OTel GenAI 1.41.0 attribute subset; this is not a claim of complete or latest GenAI semantic-convention conformance. The generator produces checked-in Rust constants:

python3 harness/telemetry/scripts/generate-attributes.py --check

Python is a development-time generator only; building/shipping the Rust harness requires no Python runtime. The registry can later generate constants for other languages; the Python harness is unchanged.

With default content.capture: "off", exports contain fixed operations/outcomes, hashed identities, timestamps/durations, numeric usage and derived operation/type hints. session.id and gen_ai.conversation.id contain the same SHA-256 session hash as openworker.session.hash. These are pseudonymous, not anonymous. Raw session IDs, prompts, messages, reasoning, paths, URLs, credentials and model/tool names are excluded. Error status text is constant, never an exception string.

"content": {"capture": "metadata"} opts into registered, validated model/tool identifiers and sandbox/policy metadata. "full" also admits registered payload keys, bounded by max_bytes and optional best-effort secret redaction. Full capture is sensitive and redaction cannot guarantee secret removal. Unknown keys, reserved typed keys, URL/path-shaped metadata and repeated-attribute growth are rejected. Recording metadata and remote capture remain independently controlled.

The standard cache-creation key is gen_ai.usage.cache_creation.input_tokens. M5 replaces the old nonstandard gen_ai.usage.cache_write.input_tokens wire key; local recording's cache_write field is unchanged. Consumers of the old wire key must update their queries. Model names and content compatibility keys retain their existing names; no full migration to structured GenAI message events is claimed. See the pinned convention.

Counting and context

Operation Meaning
api.request / command.admission HTTP response preparation or committed command admission, not SSE lifetime or applied approval.
agent.execute An execution segment of a logical turn; parking ends the segment.
model.requested Start marker/event; no completed request usage.
model.attempt One main/reviewer/compaction provider request, including retries and failed streams.
tool.execute Admitted execution; missing effects/result remain unknown.
policy.review Authorization/reviewer path, including waiting/denied outcomes.
approval.* / question.* Committed transitions and measured human-wait intervals.
environment.operation Execution-environment preparation, not complete provider lifecycle coverage.
storage.call / worker.* / session.recovery* Bounded storage, worker ownership and committed recovery activity.

Usage belongs only to completed model-request records. Parent/turn/tool spans do not repeat it. Known zero remains zero; unknown stays absent. Input/output totals include cache/reasoning subsets, which must not be added again. Cache creation, cache read and reasoning counts remain optional. Each attempt has its own identity and main/auto-approver/compaction purpose. Vendor ingestion may normalize counters; compare vendor totals with local recording before using them for reporting.

Models are CLIENT spans with gen_ai.operation.name=chat and langfuse.observation.type=generation, even when names/content are off. Start markers are events. Derived vendor hints cannot be overridden by string metadata. Stable turn context survives durable parking/resume and worker replacement; segments have distinct span IDs and explicit links. Task-local child context requires explicit propagation. Session/turn hashes correlate auxiliary lifecycle activity without inventing parentage. Invalid-clock intervals stay visible as unknown timing in recording/logs and do not manufacture invalid-duration spans.

Metrics and bounds

Metric dimensions are limited to operation, outcome, model purpose and token direction. No model/tool name, prompt, user/session ID or trace ID is a metric dimension. Resources include explicit deployment/role, generated process instance, service version and registry/convention versions. Keep cumulative samples separate by process instance; do not sum repeated snapshots from the same stream.

  • openworker.operation.count / .duration: completed work and active seconds; transition events have counts, not duration samples.
  • openworker.model.tokens: known input/output per model request and purpose.
  • openworker.model.retry.count: attempts numbered above one.
  • openworker.model.first_output.duration: first visible output latency, including transport/queue/preparation; not provider-internal generation time.
  • openworker.telemetry.dropped / .export_failures: local drop and failed-batch snapshots, not exact HTTP request counts or a durable global ledger.
  • Fixed server gauges and last model/tool error timestamps support operational charts; see operations guidance.

The execution queue holds up to 2,048 messages plus 128 in flight. The trace/log processors also cap queues at 2,048 and batches at 128. Producers never wait for receivers. Full queues drop visibly. No per-session in-memory span registry, durable export spool or automatic agent/tool retry is introduced. All admitted records are sampled; configurable sampling is not implemented.

Shutdown fences producers and drains within a total budget (normally five seconds). An in-flight batch may complete later; a custom blocking sink cannot be forcibly killed. Recording commits survive restart, but queues/crashes/outages can lose observations. Missing telemetry never authorizes replay of an uncertain tool.

Embedded integration

The core crate has no OTel SDK/exporter dependency unless otel/otlp is enabled. Existing runtime/stdio/Harness SDK defaults stay off; no new high-level SDK API. Trusted hosts can pass Telemetry to Runtime::open_instrumented and PostgreSQL Config.telemetry. otlp::Config::start(role) builds instance-owned exporters; construct it on a blocking thread when embedding in Tokio.

otel::OtelSink::new accepts app-owned processors/providers. It calls completed spans' on_end, not global sampler/on_start hooks. Keep ownership of shared providers: the sink flushes but does not explicitly shut them down. Official Rust OTel components are pinned to 0.33.1. Custom sinks own any extra fields they add.

Reproduce qualification

See export qualification for the pinned local collector/Langfuse fixture, fault tests, recording reconciliation and limits. The fixture uses synthetic measurements, no paid models or real tools. Fresh-cloud, Windows VM and multi-AZ qualification remain separate work.