From e286fd416d5ea0ca3dee2dd2598edd2e836979ad Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Wed, 3 Jun 2026 13:40:08 +0530 Subject: [PATCH] feat(plane-enterprise): native OpenTelemetry APM support Add first-class OTEL configuration to the backend instead of relying on ad-hoc extraEnv. Bumps chart version to 2.5.1. - values.yaml: new `observability.otel` block (off by default) with a nested `collector` sub-block for an optional bundled OTLP collector. - config-secrets/app-env.yaml: when `observability.otel.enabled`, inject OTEL_* into the backend `-app-vars` ConfigMap (scoped to the six workloads that envFrom it: api, worker, beat-worker, automation-consumer, outbox-poller, migrator). Auth headers go into `-app-secrets`. When the endpoint is blank and the bundled collector is enabled, the backend auto-targets the in-cluster collector Service. - templates/observability/otel-collector.yaml: bundled collector (ConfigMap/Service/Deployment), gated on `observability.otel.collector.enabled`, with an overridable config that defaults to an OTLP-in -> debug-out pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) --- charts/plane-enterprise/Chart.yaml | 2 +- .../templates/config-secrets/app-env.yaml | 26 ++++ .../observability/otel-collector.yaml | 120 ++++++++++++++++++ charts/plane-enterprise/values.yaml | 44 +++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 charts/plane-enterprise/templates/observability/otel-collector.yaml diff --git a/charts/plane-enterprise/Chart.yaml b/charts/plane-enterprise/Chart.yaml index 66fc603c..3cfa6771 100644 --- a/charts/plane-enterprise/Chart.yaml +++ b/charts/plane-enterprise/Chart.yaml @@ -5,7 +5,7 @@ description: Meet Plane. An Enterprise software development tool to manage issue type: application -version: 2.5.0 +version: 2.5.1 appVersion: "2.6.1" home: https://plane.so/ diff --git a/charts/plane-enterprise/templates/config-secrets/app-env.yaml b/charts/plane-enterprise/templates/config-secrets/app-env.yaml index c3e14746..93562f18 100644 --- a/charts/plane-enterprise/templates/config-secrets/app-env.yaml +++ b/charts/plane-enterprise/templates/config-secrets/app-env.yaml @@ -43,6 +43,10 @@ stringData: {{- if include "plane.s3CAEnabled" . }} AWS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt" {{- end }} + + {{- if and .Values.observability.otel.enabled .Values.observability.otel.headers }} + OTEL_EXPORTER_OTLP_HEADERS: {{ .Values.observability.otel.headers | quote }} + {{- end }} {{- end }} --- @@ -104,3 +108,25 @@ data: {{- else}} CORS_ALLOWED_ORIGINS: "http://{{ .Values.license.licenseDomain }},https://{{ .Values.license.licenseDomain }}" {{- end }} + + {{- /* OpenTelemetry APM for the Django backend (api, worker, beat-worker, + automation-consumer, outbox-poller, migrator). No-op in the app unless + OTEL_ENABLED=1 and an endpoint is set. Auth headers, if any, live in the + app-secrets Secret above. See docs/otel-api-observability in plane-ee. */}} + {{- if .Values.observability.otel.enabled }} + OTEL_ENABLED: "1" + OTEL_SERVICE_NAME: {{ .Values.observability.otel.serviceName | default "plane-api" | quote }} + {{- if .Values.observability.otel.endpoint }} + OTEL_EXPORTER_OTLP_ENDPOINT: {{ .Values.observability.otel.endpoint | quote }} + {{- else if .Values.observability.otel.collector.enabled }} + OTEL_EXPORTER_OTLP_ENDPOINT: "http://{{ .Release.Name }}-otel-collector.{{ .Release.Namespace }}.svc.cluster.local:4317" + {{- else }} + OTEL_EXPORTER_OTLP_ENDPOINT: "" + {{- end }} + OTEL_EXPORTER_OTLP_PROTOCOL: {{ .Values.observability.otel.protocol | default "grpc" | quote }} + OTEL_TRACES_SAMPLER: {{ .Values.observability.otel.tracesSampler | default "parentbased_traceidratio" | quote }} + OTEL_TRACES_SAMPLER_ARG: {{ .Values.observability.otel.tracesSamplerArg | default "0.1" | quote }} + {{- if .Values.observability.otel.resourceAttributes }} + OTEL_RESOURCE_ATTRIBUTES: {{ .Values.observability.otel.resourceAttributes | quote }} + {{- end }} + {{- end }} diff --git a/charts/plane-enterprise/templates/observability/otel-collector.yaml b/charts/plane-enterprise/templates/observability/otel-collector.yaml new file mode 100644 index 00000000..73ed52db --- /dev/null +++ b/charts/plane-enterprise/templates/observability/otel-collector.yaml @@ -0,0 +1,120 @@ +{{- /* + Bundled OpenTelemetry Collector. Rendered only when OTEL is enabled AND the + bundled collector is requested. Self-hosters pointing at an external collector + (Datadog Agent, Grafana Cloud, an existing cluster collector) leave + observability.otel.collector.enabled=false and these resources are omitted. + + When enabled with an empty observability.otel.endpoint, the backend auto-targets + this collector's in-cluster Service (see config-secrets/app-env.yaml). +*/}} +{{- if and .Values.observability.otel.enabled .Values.observability.otel.collector.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ .Release.Namespace }} + name: {{ .Release.Name }}-otel-collector-config + labels: + app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-otel-collector +data: + config.yaml: | +{{- if .Values.observability.otel.collector.config }} +{{ .Values.observability.otel.collector.config | indent 4 }} +{{- else }} + # Default pipeline: receive OTLP, log via `debug`. Override the whole config + # with observability.otel.collector.config to export to a real backend. + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + processors: + batch: + timeout: 10s + send_batch_size: 1024 + exporters: + debug: + verbosity: detailed + service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [debug] +{{- end }} + +--- + +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Release.Namespace }} + name: {{ .Release.Name }}-otel-collector + labels: + app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-otel-collector +spec: + type: ClusterIP + ports: + - name: otlp-grpc + port: 4317 + protocol: TCP + targetPort: 4317 + - name: otlp-http + port: 4318 + protocol: TCP + targetPort: 4318 + selector: + app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-otel-collector + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: {{ .Release.Namespace }} + name: {{ .Release.Name }}-otel-collector + {{- include "plane.labelsAndAnnotations" .Values.observability.otel.collector }} +spec: + replicas: {{ .Values.observability.otel.collector.replicas | default 1 }} + selector: + matchLabels: + app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-otel-collector + template: + metadata: + namespace: {{ .Release.Namespace }} + labels: + app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-otel-collector + annotations: + timestamp: {{ now | quote }} + spec: + {{- include "plane.podScheduling" .Values.observability.otel.collector }} + containers: + - name: {{ .Release.Name }}-otel-collector + imagePullPolicy: {{ .Values.observability.otel.collector.pullPolicy | default "IfNotPresent" }} + image: {{ .Values.observability.otel.collector.image | default "otel/opentelemetry-collector-contrib:0.115.1" }} + args: ["--config=/etc/otel/config.yaml"] + ports: + - name: otlp-grpc + containerPort: 4317 + - name: otlp-http + containerPort: 4318 + resources: + requests: + memory: {{ .Values.observability.otel.collector.memoryRequest | default "128Mi" | quote }} + cpu: {{ .Values.observability.otel.collector.cpuRequest | default "100m" | quote }} + limits: + memory: {{ .Values.observability.otel.collector.memoryLimit | default "512Mi" | quote }} + cpu: {{ .Values.observability.otel.collector.cpuLimit | default "500m" | quote }} + volumeMounts: + - name: config + mountPath: /etc/otel + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-otel-collector-config +{{- end }} diff --git a/charts/plane-enterprise/values.yaml b/charts/plane-enterprise/values.yaml index d4ecfaec..3a82187a 100644 --- a/charts/plane-enterprise/values.yaml +++ b/charts/plane-enterprise/values.yaml @@ -613,3 +613,47 @@ extraEnv: [] # value: "http://proxy.example.com:8080" # - name: NO_PROXY # value: "localhost,127.0.0.1,.example.com" + +# OpenTelemetry APM for the Django backend (api, worker, beat-worker, +# automation-consumer, outbox-poller, migrator). Off by default — when +# enabled, OTEL env vars are injected into the backend app-vars ConfigMap. +# Point `endpoint` at any OTLP collector. See docs/otel-api-observability +# in the plane-ee repo for the full guide. +observability: + otel: + enabled: false + # Service name reported to your APM backend. + serviceName: plane-api + # OTLP receiver, e.g. http://otel-collector..svc.cluster.local:4317 + endpoint: "" + # grpc | http/protobuf + protocol: grpc + # Standard OTEL head sampler. Set tracesSamplerArg to "1.0" to capture every request. + tracesSampler: parentbased_traceidratio + tracesSamplerArg: "0.1" + # Extra resource attrs, e.g. "deployment.environment=prod,service.version=v2.6.1" + resourceAttributes: "" + # Auth headers for SaaS backends (rendered into app-secrets), e.g. "Authorization=Bearer%20" + headers: "" + # Bundled in-cluster OpenTelemetry Collector. When enabled, the chart deploys + # a collector Deployment/Service/ConfigMap and — if `endpoint` above is empty — + # the backend auto-targets it. Leave disabled to export to an external + # collector (Datadog Agent, Grafana Cloud, an existing cluster collector). + collector: + enabled: false + image: otel/opentelemetry-collector-contrib:0.115.1 + pullPolicy: IfNotPresent + replicas: 1 + memoryRequest: 128Mi + cpuRequest: 100m + memoryLimit: 512Mi + cpuLimit: 500m + # Optional full collector config override (YAML string). When empty, a + # default OTLP-in → `debug`-out pipeline is used (good for verifying spans; + # replace `debug` with a real exporter for production). + config: "" + nodeSelector: {} + tolerations: [] + affinity: {} + labels: {} + annotations: {}