Skip to main content
OpenHands Enterprise (OHE) ships with Laminar as its built-in tracing backend. Every conversation emits OpenTelemetry traces that flow to the in-cluster Laminar service. If your organization already operates a different OpenTelemetry-compatible observability platform — Langfuse, Honeycomb, Tempo, Datadog, or any backend that speaks OTLP — you can redirect all OHE conversation traces to it without modifying OHE source or patching the Helm chart. The change is a set of environment variables on the runtime pod. This guide walks an operator through pointing OHE at an external observability platform and confirms what you get versus the built-in Laminar experience.
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:
There are two integration paths:
  • 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.
Both paths leave OHE stock. The only change is pod environment variables.

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 openhands namespace.
  • 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):
The lmnr SDK resolves its trace exporter like this:
  1. If LMNR_BASE_URL is set, the SDK routes to Laminar and ignores any OTEL_EXPORTER_OTLP_TRACES_* variables. This is the default state.
  2. If LMNR_BASE_URL is not set, the SDK falls back to the standard OpenTelemetry environment variables and emits OTLP directly to whatever endpoint you configure.
The switch is LMNR_BASE_URL. As long as it is set, the runtime keeps sending traces to Laminar and ignores your OTEL_* variables. To redirect traces to your own backend, you must unset LMNR_BASE_URL (and the other LMNR_* connection variables) and set the OTEL_EXPORTER_OTLP_TRACES_* variables. Setting only the OTEL_* variables while Laminar is still enabled has no effect.
The SDK reads these variables in standard OpenTelemetry precedence (highest first): 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.
Disable the bundled Laminar and set the OpenTelemetry exporter variables under the top-level env block in your values.yaml:
Supply any secret values (API keys, Basic auth strings) as a Kubernetes secret rather than committing them in values.yaml:
Then reference the secret in values.yaml and redeploy:
Restart the runtime pod after the upgrade so the new environment is picked up:

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.
Compute the Basic auth value with:
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.
Langfuse maps the OpenTelemetry 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 the x-honeycomb-team header.
Set the Honeycomb dataset by adding x-honeycomb-dataset=<your-dataset> to the headers value, comma-separated.

Grafana Tempo

Tempo accepts OTLP over gRPC or HTTP. For gRPC:
For HTTP:
Tempo does not require auth on the OTLP receiver by default. If you put Tempo behind a gateway that requires auth, add the header to 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:
Headers are comma-separated 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
Each conversation is grouped under a single trace ID (the OpenHands conversation UUID), so all spans from one conversation — across every agent step, LLM call, and tool execution — appear together. For LLM spans, the 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 the openhands namespace and point the runtime at it instead of directly at your backend.
Point the runtime at the collector’s OTLP receiver:
Collector config (otel-collector-config.yaml):
This is also how you keep Laminar running as a secondary sink while sending traces to your own platform: add a second exporter pointing at the in-cluster Laminar service.

Keep Laminar and add a second backend

If you want traces in both Laminar and your own backend, do not unset LMNR_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

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:
You should see the 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.
  • Confirm the endpoint URL is reachable from inside the cluster:
    A 405 (Method Not Allowed) on GET is 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 401 for a bad key. Langfuse requires HTTP Basic with publicKey:secretKey; a bearer token returns 401 Invalid public key.
  • Confirm the protocol matches your endpoint. Most backends require http/protobuf. Use grpc/protobuf only if your backend exposes a gRPC OTLP receiver.
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.
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.
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