This guide is for OpenHands Enterprise operators who want to use an
external OTLP backend instead of, or in addition to, the bundled Laminar. If
you want to enable the bundled Laminar, see
Analytics instead. For SDK-level tracing concepts and
the full list of OTLP backends the OpenHands SDK supports, see
Observability & Tracing.
Overview
OHE’s tracing layer is the Laminar Python SDK (lmnr), which is a thin wrapper
over the OpenTelemetry SDK. The lmnr SDK respects standard
OTEL_EXPORTER_OTLP_TRACES_* environment variables whenever its own
Laminar-specific LMNR_BASE_URL is not set. That gives you a clean switch with
no code changes:
- Direct (recommended). Point the runtime straight at your OTLP/HTTP backend. No extra infrastructure. Use this when your backend speaks OTLP/HTTP, which Langfuse, Honeycomb, Tempo, and Datadog all do.
- Collector tap (optional). Put an OpenTelemetry Collector between the runtime and your backend. Use this when you need batching, retry, fan-out to multiple backends, or a non-OTLP destination.
Prerequisites
Before you start, confirm:- OHE is installed and reachable. You can sign in at
https://app.<your-base-domain>. - Your observability backend is reachable from the OHE cluster. The runtime
pod makes outbound HTTP/S calls to the backend, so DNS and network paths must
resolve from inside the
openhandsnamespace. - You have an ingest endpoint and credentials on your backend. You need the OTLP traces URL and whatever auth the backend expects (an API key, Basic auth, or a bearer token).
- You have cluster access to edit Helm values or the Replicated Admin Console, and can restart the runtime pod.
Choose your backend
The configuration is the same for every OTLP/HTTP backend. Only the endpoint URL, auth header, and protocol differ.Langfuse
Self-hosted or Cloud. OTLP/HTTP with Basic auth. Maps OHE LLM spans to
Langfuse generations with model, tokens, and cost.
Honeycomb
OTLP/HTTP with a header API key. High-cardinality distributed tracing.
Grafana Tempo
OTLP/gRPC or HTTP. Open-source trace storage, queried from Grafana.
Generic OTLP
Any backend that accepts OTLP. Jaeger, Datadog, New Relic, Splunk, and more.
How tracing works in OHE
The runtime pod sets these environment variables by default when Laminar is enabled (see Analytics):lmnr SDK resolves its trace exporter like this:
- If
LMNR_BASE_URLis set, the SDK routes to Laminar and ignores anyOTEL_EXPORTER_OTLP_TRACES_*variables. This is the default state. - If
LMNR_BASE_URLis not set, the SDK falls back to the standard OpenTelemetry environment variables and emits OTLP directly to whatever endpoint you configure.
OTEL_EXPORTER_OTLP_TRACES_*, then OTEL_EXPORTER_OTLP_*, then
OTEL_*. Setting the _TRACES_ variants is the most explicit and recommended
form.
Configure OHE
Pick the path that matches how OHE is deployed.- Standalone Helm (Kubernetes)
- Replicated VM (Admin Console)
Disable the bundled Laminar and set the OpenTelemetry exporter variables
under the top-level Supply any secret values (API keys, Basic auth strings) as a Kubernetes
secret rather than committing them in Then reference the secret in Restart the runtime pod after the upgrade so the new environment is picked
up:
env block in your values.yaml:values.yaml:values.yaml and redeploy:Backend-specific configuration
The three values you need differ per backend: the endpoint URL, the auth header, and the protocol.Langfuse
Langfuse v3 and v4 expose an OTLP/HTTP ingestion endpoint. Authentication is HTTP Basic, with the Langfuse public key as the username and the secret key as the password.Langfuse v4 self-hosted installs default to events-only mode, which
accepts traces on
/api/public/otel/v1/traces but does not expose the
legacy GET /api/public/traces endpoint. Read trace data with
GET /api/public/v2/observations instead. The Langfuse UI reads from the
same store, so traces appear in the UI regardless of mode.gen_ai.* semantic conventions that the lmnr
SDK emits onto its own observation model, so LLM calls render as GENERATION
observations with model, token usage, and input/output content. See
What you get below.
Honeycomb
Honeycomb accepts OTLP/HTTP with the API key in thex-honeycomb-team header.
x-honeycomb-dataset=<your-dataset> to the
headers value, comma-separated.
Grafana Tempo
Tempo accepts OTLP over gRPC or HTTP. For gRPC:OTEL_EXPORTER_OTLP_TRACES_HEADERS.
Generic OTLP
For any backend that accepts OTLP (Jaeger, Datadog, New Relic, Splunk Observability, and others), set the endpoint and protocol your backend documents, plus any auth header it requires:key=value pairs, URL-encoded. Most backends
accept a single Authorization or X-API-Key header.
What you get
A single OHE conversation produces one trace with a nested span tree. The shape is the same whether the traces land in Laminar or in your external backend:conversation
conversation.run
agent.step
llm.completion
tool.execute
agent.step
llm.completion
lmnr SDK emits standard OpenTelemetry gen_ai.* semantic
conventions:
Backends that understand these conventions render LLM calls as first-class
generation spans with model, token usage, and prompt content. In Langfuse,
LLM spans become GENERATION observations; tool spans become TOOL
observations; the conversation root becomes an AGENT observation. The
nesting, trace ID, session ID, and user ID are all preserved.
Cost calculation
Laminar computes cost from the token usage on each LLM span. External backends do the same, but only when the model is registered in the backend’s model catalog with pricing. If a model is missing from the catalog, the span still appears with token counts, but cost is blank.After pointing OHE at Langfuse, add each model your runtime uses (for example,
claude-sonnet-4-5-20250929, gpt-4o) to Langfuse’s Settings → Models
table with input and output token prices. Until you do, cost columns are
empty even though token usage is captured.Optional: OTel Collector tap
If you want batching, retry, fan-out to multiple backends, or a non-OTLP destination, deploy an OpenTelemetry Collector in theopenhands namespace and
point the runtime at it instead of directly at your backend.
otel-collector-config.yaml):
Keep Laminar and add a second backend
If you want traces in both Laminar and your own backend, do not unsetLMNR_BASE_URL. Instead, deploy an OTel Collector as above and configure the
runtime to send to the collector, with the collector exporting to both
Laminar and your backend. This preserves the built-in Laminar experience
(including the Admin Console Traces tab and Laminar signals) while mirroring
the same traces to your platform.
Troubleshooting
Traces still go to Laminar, not my backend
Traces still go to Laminar, not my backend
LMNR_BASE_URL is still set. As long as it is present, the lmnr SDK
routes to Laminar and ignores OTEL_* variables. Confirm the runtime pod
does not have LMNR_BASE_URL set:OTEL_* variables and no LMNR_BASE_URL. If
LMNR_BASE_URL is still present, the Laminar block in your values.yaml
or Admin Console is still enabled. Disable it and restart the pod.No traces appear in my backend
No traces appear in my backend
-
Confirm the endpoint URL is reachable from inside the cluster:
A
405(Method Not Allowed) onGETis fine — it means the endpoint exists. A timeout or connection refused means DNS or network policy is blocking the path. -
Confirm the auth header is correct. Most OTLP backends return
401for a bad key. Langfuse requires HTTP Basic withpublicKey:secretKey; a bearer token returns401 Invalid public key. -
Confirm the protocol matches your endpoint. Most backends require
http/protobuf. Usegrpc/protobufonly if your backend exposes a gRPC OTLP receiver.
Traces appear but cost is blank
Traces appear but cost is blank
The token usage is captured, but the model is not in your backend’s model
catalog. Add the model with pricing in your backend’s settings (in
Langfuse, Settings → Models). See Cost calculation.
LLM spans show tokens but no input/output content
LLM spans show tokens but no input/output content
The
lmnr SDK emits input content under gen_ai.input.messages and output
under gen_ai.completion (or gen_ai.output depending on the provider
instrumentation). If your backend maps a different attribute name, the
content field is blank while token counts still populate. This is a
backend-side mapping difference, not an OHE issue. Real OHE conversations
use the lmnr Anthropic and OpenAI auto-instrumentation, which emits the
standard attribute names.VM install has no OTEL env var fields
VM install has no OTEL env var fields
The Replicated Admin Console does not currently expose
OTEL_EXPORTER_OTLP_TRACES_* fields directly. Uncheck Enable Analytics
to clear the LMNR_* variables, then use the Replicated custom environment
variable feature to add the OTEL_* variables. If your version does not
expose custom environment variables, contact OpenHands support.Reference
- Built-in Laminar setup: Analytics
- SDK tracing concepts and OTLP backends: Observability & Tracing
- OpenTelemetry OTLP exporter environment variables: OTEL spec
- Langfuse OTLP ingestion: Langfuse docs
- OpenTelemetry Collector configuration: OTel Collector docs

