From dc4bef92f573137fdfb3c352b881dc9da90bacba Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 20 Aug 2026 19:47:51 +0530 Subject: [PATCH 1/2] docs(developer-docs): add OpenTelemetry configuration guide Document the opt-in OpenTelemetry support added in the plane-enterprise Helm chart 3.3.0: enable steps, full observability.otel.* values reference, auth headers (chart-managed Secret or existing secret), sampling, browser tracing caveats, and verification steps. Adds the page to the self-hosting Configure sidebar. Co-Authored-By: Claude Fable 5 --- .../developer-docs/docs/.vitepress/config.mts | 1 + .../docs/self-hosting/govern/opentelemetry.md | 173 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 apps/developer-docs/docs/self-hosting/govern/opentelemetry.md diff --git a/apps/developer-docs/docs/.vitepress/config.mts b/apps/developer-docs/docs/.vitepress/config.mts index 5a74acc0..bc2c23fe 100644 --- a/apps/developer-docs/docs/.vitepress/config.mts +++ b/apps/developer-docs/docs/.vitepress/config.mts @@ -421,6 +421,7 @@ export default extendConfig( ], }, { text: "Session expiry", link: "/self-hosting/govern/god-mode/session-expiry" }, + { text: "OpenTelemetry", link: "/self-hosting/govern/opentelemetry" }, { text: "External secrets", link: "/self-hosting/govern/external-secrets" }, { text: "External reverse proxy", link: "/self-hosting/govern/reverse-proxy" }, { text: "Private storage buckets", link: "/self-hosting/govern/private-bucket" }, diff --git a/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md b/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md new file mode 100644 index 00000000..a06ee981 --- /dev/null +++ b/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md @@ -0,0 +1,173 @@ +--- +title: Configure OpenTelemetry +description: Export traces, metrics, and logs from a self-hosted Plane instance to any OpenTelemetry-compatible backend using the plane-enterprise Helm chart. +keywords: plane opentelemetry, otel, observability, distributed tracing, otlp, traces, metrics, plane kubernetes observability, self-hosting +--- + +# Configure OpenTelemetry + +Plane ships with built-in [OpenTelemetry](https://opentelemetry.io/) (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 (`-otel-vars`) with the standard `OTEL_*` environment variables built from your Helm values. +2. Renders a Secret (`-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: + +| 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](#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-enterprise` Helm chart version 3.3.0 or later. See the [Kubernetes installation guide](/self-hosting/methods/kubernetes) to install or upgrade. +- An OTLP endpoint reachable from the pods in your cluster — typically an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) running in the same cluster. + +::: tip 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: + +| 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](#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: + +```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 `-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](/self-hosting/govern/external-secrets)), 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 -otel-vars -n plane -o yaml + ``` + +2. Check the API pod's startup logs for the confirmation line: + + ```bash + kubectl logs deploy/-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`. From acc0031035aa86f683a5cfa3d4b4da2913867269 Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 20 Aug 2026 19:53:56 +0530 Subject: [PATCH 2/2] docs(developer-docs): document the required keys for otel_env_existingSecret Spell out the Secret shape for external_secrets.otel_env_existingSecret: the OTEL_EXPORTER_OTLP_HEADERS data key, a kubectl and an ExternalSecret example, and the envFrom/non-optional caveats. Co-Authored-By: Claude Fable 5 --- .../docs/self-hosting/govern/opentelemetry.md | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md b/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md index a06ee981..4dd2d847 100644 --- a/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md +++ b/apps/developer-docs/docs/self-hosting/govern/opentelemetry.md @@ -109,13 +109,55 @@ If your backend requires an ingestion key, you have two options. --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](/self-hosting/govern/external-secrets)), create a Secret containing the key `OTEL_EXPORTER_OTLP_HEADERS` and point the chart at it. The chart then skips creating its own Secret: +**Bring your own secret.** If you manage secrets externally (for example, with the [External Secrets Operator](/self-hosting/govern/external-secrets)), point the chart at a Secret you own. The chart then skips creating its own Secret and references yours instead: ```yaml external_secrets: otel_env_existingSecret: my-otel-headers ``` +The Secret is injected into the pods with `envFrom`, so the data key must be named exactly like the environment variable it supplies: + +| Key | Required | Value | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------ | +| `OTEL_EXPORTER_OTLP_HEADERS` | Yes | Exporter headers in `key=value,key=value` form, for example `x-api-key=your_ingestion_key` | + +Created manually, it looks like this: + +```bash +kubectl create secret generic my-otel-headers \ + --namespace plane \ + --from-literal=OTEL_EXPORTER_OTLP_HEADERS='x-api-key=your_ingestion_key' +``` + +Or as an `ExternalSecret` target synced by the External Secrets Operator: + +```yaml +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: otel-external-secret + namespace: plane +spec: + refreshInterval: 1m + secretStoreRef: + name: cluster-aws-secretsmanager + kind: ClusterSecretStore + target: + name: my-otel-headers # referenced by external_secrets.otel_env_existingSecret + creationPolicy: Owner + data: + - secretKey: OTEL_EXPORTER_OTLP_HEADERS + remoteRef: + key: prod/secrets/otel + property: OTEL_EXPORTER_OTLP_HEADERS +``` + +Keep the following in mind: + +- The Secret must live in the same namespace as the Plane release and is referenced as non-optional — if it doesn't exist yet (for example, ESO hasn't synced it), the instrumented pods won't start until it appears. +- Because the Secret is applied with `envFrom`, any other keys in it also become environment variables on every instrumented pod. Keep it limited to the `OTEL_*` values you intend to set. + ## 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: