Configure OpenTelemetry Commercial Edition
Plane ships with built-in OpenTelemetry (OTel) instrumentation across its backend services. When enabled, Plane exports traces, metrics, and logs over OTLP to any OpenTelemetry-compatible backend you run — an OpenTelemetry Collector, Grafana Tempo, Datadog, Honeycomb, and others.
OpenTelemetry support is off by default and is fully opt-in. When disabled, no OTel configuration is rendered and your deployment is unchanged.
INFO
This guide covers Kubernetes deployments using the plane-enterprise Helm chart version 3.3.0 or later. Plane does not deploy a collector for you — you bring your own OTLP endpoint.
How it works
When you enable OpenTelemetry, the Helm chart:
- Renders a ConfigMap (
<release>-otel-vars) with the standardOTEL_*environment variables built from your Helm values. - Renders a Secret (
<release>-otel-secrets) holdingOTEL_EXPORTER_OTLP_HEADERSif you configure authentication headers, or references a secret you manage yourself. - Injects both into Plane's backend workloads, along with a per-service
OTEL_SERVICE_NAME, so each service reports under its own name.
The following services are instrumented:
| Service | Signals exported |
|---|---|
api, external-api, worker, worker-importers, beat-worker, automation-consumer, agent-consumer, webhook-consumer, outbox-poller | Traces and metrics over OTLP; JSON logs on stdout are enriched with trace and span IDs |
live, live-exporter, silo | Traces, logs, and metrics over OTLP |
space (server-side rendering) | Logs and request spans over OTLP/HTTP |
pi-api, pi-worker, pi-beat (Plane AI) | Logs over OTLP/HTTP |
web, admin, space (browser) | Optional browser traces and logs — see Browser tracing |
The Python API instruments Django, Celery, PostgreSQL, Redis, and outgoing HTTP calls; trace context propagates from incoming HTTP requests through Celery tasks. The Node services (live, silo) use auto-instrumentation for HTTP, Express, PostgreSQL, Redis, and AMQP, and bridge their application logs into the OTLP logs pipeline with trace correlation.
Prerequisites
- The
plane-enterpriseHelm chart version 3.3.0 or later. See the Kubernetes installation guide to install or upgrade. - An OTLP endpoint reachable from the pods in your cluster — typically an OpenTelemetry Collector running in the same cluster.
Choosing the protocol
The Django and Node services support both OTLP/gRPC (usually port 4317) and OTLP/HTTP (usually port 4318). The Plane AI (pi-*) services and space server-side rendering export over OTLP/HTTP only. If you run Plane AI, expose your collector's HTTP receiver and use protocol: http/protobuf with the HTTP port so every service can export to the same endpoint.
Enable OpenTelemetry
Enable it with two values — the toggle and the endpoint:
helm upgrade plane-app plane/plane-enterprise \
--namespace plane \
--reuse-values \
--set observability.otel.enabled=true \
--set observability.otel.endpoint=http://otel-collector.observability.svc.cluster.local:4317Or in your values file:
observability:
otel:
enabled: true
endpoint: http://otel-collector.observability.svc.cluster.local:4317If endpoint is left empty, the services skip OTel setup even when enabled is true, so nothing is exported.
Configuration reference
All settings live under observability.otel in the chart values:
| Value | Default | Maps to | Description |
|---|---|---|---|
enabled | false | OTEL_ENABLED | Master switch. When false, no OTel resources or environment variables are created. |
endpoint | "" | OTEL_EXPORTER_OTLP_ENDPOINT | OTLP receiver URL. An https:// endpoint uses a secure connection. |
protocol | grpc | OTEL_EXPORTER_OTLP_PROTOCOL | grpc or http/protobuf. |
headers | "" | OTEL_EXPORTER_OTLP_HEADERS | Exporter headers such as ingestion credentials, in key=value,key=value form. Stored in a Kubernetes Secret. |
environment | "" | OTEL_ENVIRONMENT | Sets the deployment.environment.name resource attribute (for example, production). |
resourceAttributes | "" | OTEL_RESOURCE_ATTRIBUTES | Additional resource attributes, in key=value,key=value form. |
sampler | always_on | OTEL_TRACES_SAMPLER | Trace sampler: always_on, parentbased_traceidratio, traceidratio, or always_off. |
samplerArg | "1.0" | OTEL_TRACES_SAMPLER_ARG | Sampling ratio for the ratio-based samplers. Ignored by always_on. |
debugConsole | false | OTEL_DEBUG_CONSOLE | Also prints spans to stdout. For debugging only. |
frontend.enabled | false | FRONTEND_OTEL_ENABLED | Enables browser tracing for the web, admin, and space apps. Requires frontend.endpoint. |
frontend.endpoint | "" | FRONTEND_OTLP_ENDPOINT | Browser-reachable OTLP/HTTP endpoint. This value is public — see Browser tracing. |
frontend.headers | x-otlp-browser=1 | FRONTEND_OTLP_HEADERS | Headers sent by the browser exporter. These are visible to anyone using the app, so never put secrets here. |
A production-style example:
observability:
otel:
enabled: true
endpoint: https://otlp.vendor.example.com:4317
protocol: grpc
environment: production
resourceAttributes: "cluster=eu-1"
sampler: parentbased_traceidratio
samplerArg: "0.25"Authentication headers
If your backend requires an ingestion key, you have two options.
Let the chart manage the secret. Set observability.otel.headers and the chart creates the <release>-otel-secrets Secret for you:
--set observability.otel.headers='x-api-key=your_ingestion_key'Bring your own secret. If you manage secrets externally (for example, with the External Secrets Operator), create a Secret containing the key OTEL_EXPORTER_OTLP_HEADERS and point the chart at it. The chart then skips creating its own Secret:
external_secrets:
otel_env_existingSecret: my-otel-headersSampling
The chart defaults to always_on, which exports every trace. That's the right starting point for evaluating the integration, but on a busy instance you'll likely want head sampling to control volume and cost:
observability:
otel:
sampler: parentbased_traceidratio
samplerArg: "0.1" # keep 10% of tracesparentbased_traceidratio respects the sampling decision of an incoming trace context, so distributed traces stay complete.
Browser tracing
Backend telemetry stays inside your cluster, but you can optionally have the web, admin, and space apps report traces and logs from users' browsers:
observability:
otel:
enabled: true
endpoint: http://otel-collector.observability.svc.cluster.local:4317
frontend:
enabled: true
endpoint: https://otlp-browser.example.comKeep the following in mind:
- The endpoint must be reachable from your users' browsers and must be an OTLP/HTTP receiver — browsers can't speak gRPC. The apps append
/v1/tracesand/v1/logsto the endpoint you configure. - The receiver must allow cross-origin requests (CORS) from your Plane domain.
frontend.endpointandfrontend.headersare served to every visitor through Plane's public instance configuration. Treat them as public values and use a dedicated, rate-limited receiver rather than credentials you care about.- Keep
frontend.headersnon-empty. A custom header forces the browser exporter to send over XHR instead ofnavigator.sendBeacon— beacon requests include credentials, which fail CORS against a wildcardAccess-Control-Allow-Originand silently break browser export. The defaultx-otlp-browser=1exists for exactly this reason.
Browser tracing is read by the Plane API and delivered to the apps at runtime — you don't need to rebuild any images to turn it on or off.
Verify the setup
Confirm the ConfigMap rendered with your values:
bashkubectl get configmap <release>-otel-vars -n plane -o yamlCheck the API pod's startup logs for the confirmation line:
bashkubectl logs deploy/<release>-api -n plane | grep "OpenTelemetry configured"You should see something like
OpenTelemetry configured: service=api, endpoint=..., protocol=grpc, sampler=always_on(1.0).Generate some traffic in Plane and look for spans from
apiin your backend. Traces from a single request should span the API, Celery workers, and database calls.
If nothing arrives, check that the endpoint is reachable from a pod in the Plane namespace, and that the protocol matches the receiver port — gRPC receivers usually listen on 4317 and HTTP receivers on 4318.

