Skip to content

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:

  1. Renders a ConfigMap (<release>-otel-vars) with the standard OTEL_* environment variables built from your Helm values.
  2. Renders a Secret (<release>-otel-secrets) holding OTEL_EXPORTER_OTLP_HEADERS if you configure authentication headers, or references a secret you manage yourself.
  3. 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:

ServiceSignals exported
api, external-api, worker, worker-importers, beat-worker, automation-consumer, agent-consumer, webhook-consumer, outbox-pollerTraces and metrics over OTLP; JSON logs on stdout are enriched with trace and span IDs
live, live-exporter, siloTraces, 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

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:

bash
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:4317

Or in your values file:

yaml
observability:
  otel:
    enabled: true
    endpoint: http://otel-collector.observability.svc.cluster.local:4317

If 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:

ValueDefaultMaps toDescription
enabledfalseOTEL_ENABLEDMaster switch. When false, no OTel resources or environment variables are created.
endpoint""OTEL_EXPORTER_OTLP_ENDPOINTOTLP receiver URL. An https:// endpoint uses a secure connection.
protocolgrpcOTEL_EXPORTER_OTLP_PROTOCOLgrpc or http/protobuf.
headers""OTEL_EXPORTER_OTLP_HEADERSExporter headers such as ingestion credentials, in key=value,key=value form. Stored in a Kubernetes Secret.
environment""OTEL_ENVIRONMENTSets the deployment.environment.name resource attribute (for example, production).
resourceAttributes""OTEL_RESOURCE_ATTRIBUTESAdditional resource attributes, in key=value,key=value form.
sampleralways_onOTEL_TRACES_SAMPLERTrace sampler: always_on, parentbased_traceidratio, traceidratio, or always_off.
samplerArg"1.0"OTEL_TRACES_SAMPLER_ARGSampling ratio for the ratio-based samplers. Ignored by always_on.
debugConsolefalseOTEL_DEBUG_CONSOLEAlso prints spans to stdout. For debugging only.
frontend.enabledfalseFRONTEND_OTEL_ENABLEDEnables browser tracing for the web, admin, and space apps. Requires frontend.endpoint.
frontend.endpoint""FRONTEND_OTLP_ENDPOINTBrowser-reachable OTLP/HTTP endpoint. This value is public — see Browser tracing.
frontend.headersx-otlp-browser=1FRONTEND_OTLP_HEADERSHeaders sent by the browser exporter. These are visible to anyone using the app, so never put secrets here.

A production-style example:

yaml
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:

bash
--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:

yaml
external_secrets:
  otel_env_existingSecret: my-otel-headers

Sampling

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:

yaml
observability:
  otel:
    sampler: parentbased_traceidratio
    samplerArg: "0.1" # keep 10% of traces

parentbased_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:

yaml
observability:
  otel:
    enabled: true
    endpoint: http://otel-collector.observability.svc.cluster.local:4317
    frontend:
      enabled: true
      endpoint: https://otlp-browser.example.com

Keep 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/traces and /v1/logs to the endpoint you configure.
  • The receiver must allow cross-origin requests (CORS) from your Plane domain.
  • frontend.endpoint and frontend.headers are 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.headers non-empty. A custom header forces the browser exporter to send over XHR instead of navigator.sendBeacon — beacon requests include credentials, which fail CORS against a wildcard Access-Control-Allow-Origin and silently break browser export. The default x-otlp-browser=1 exists 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

  1. Confirm the ConfigMap rendered with your values:

    bash
    kubectl get configmap <release>-otel-vars -n plane -o yaml
  2. Check the API pod's startup logs for the confirmation line:

    bash
    kubectl 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).

  3. Generate some traffic in Plane and look for spans from api in 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.