diff --git a/charts/plane-enterprise/Chart.yaml b/charts/plane-enterprise/Chart.yaml index df097b2..2b5e506 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.6.1 +version: 2.7.0 appVersion: "2.6.3" home: https://plane.so/ diff --git a/charts/plane-enterprise/questions.yml b/charts/plane-enterprise/questions.yml index 7429d97..ee3da6c 100644 --- a/charts/plane-enterprise/questions.yml +++ b/charts/plane-enterprise/questions.yml @@ -1505,3 +1505,76 @@ questions: type: string default: "" group: "External Secrets" + +- variable: observability.otel.enabled + label: "Enable OpenTelemetry" + description: "Export traces, logs and metrics from the backend services over OTLP. Off by default." + type: boolean + default: false + group: "OpenTelemetry" + show_subquestion_if: true + subquestions: + - variable: observability.otel.endpoint + label: "OTLP Endpoint" + description: "OTLP collector endpoint. An https:// endpoint uses secure gRPC." + type: string + default: "" + - variable: observability.otel.protocol + label: "OTLP Protocol" + type: enum + options: + - "grpc" + - "http/protobuf" + default: "grpc" + - variable: observability.otel.headers + label: "OTLP Exporter Headers" + description: "Extra OTLP exporter headers as k1=v1,k2=v2 (e.g. a collector ingestion/auth key)." + type: string + default: "" + - variable: observability.otel.environment + label: "Deployment Environment" + description: "Emitted as deployment.environment.name (e.g. prod, staging). Use for cross-service environment filtering." + type: string + default: "" + - variable: observability.otel.resourceAttributes + label: "Extra Resource Attributes" + description: "Additional OTel resource attributes as k1=v1,k2=v2 (merged after environment)." + type: string + default: "" + - variable: observability.otel.sampler + label: "Trace Sampler" + description: "always_on captures every trace (recommended for test/debug). In production use parentbased_traceidratio; unlike always_on it honours an upstream traceparent's sampling decision." + type: enum + options: + - "always_on" + - "parentbased_traceidratio" + - "traceidratio" + - "always_off" + default: "always_on" + - variable: observability.otel.samplerArg + label: "Trace Sampler Ratio" + description: "Sampling ratio 0.0-1.0 for ratio-based samplers. Ignored by always_on." + type: string + default: "1.0" + show_if: "observability.otel.sampler=parentbased_traceidratio || observability.otel.sampler=traceidratio" + - variable: observability.otel.debugConsole + label: "Print Spans to Stdout (debug)" + type: boolean + default: false + - variable: observability.otel.frontend.enabled + label: "Enable Browser Tracing" + description: "Browser tracing for web/admin/space, served to browsers by the API." + type: boolean + default: false + - variable: observability.otel.frontend.endpoint + label: "Browser OTLP Endpoint" + description: "Public OTLP/HTTP endpoint the browser posts to (must be internet-reachable and CORS-enabled)." + type: string + default: "" + show_if: "observability.otel.frontend.enabled=true" + - variable: observability.otel.frontend.headers + label: "Browser OTLP Headers" + description: "A non-empty value forces the browser exporter onto XHR (required cross-origin; sendBeacon fails CORS)." + type: string + default: "x-otlp-browser=1" + show_if: "observability.otel.frontend.enabled=true" diff --git a/charts/plane-enterprise/templates/_helpers.tpl b/charts/plane-enterprise/templates/_helpers.tpl index 8566725..992b35f 100644 --- a/charts/plane-enterprise/templates/_helpers.tpl +++ b/charts/plane-enterprise/templates/_helpers.tpl @@ -208,3 +208,35 @@ Caller must nindent to the correct depth. value: "/ca-bundle/custom-ca-bundle.crt" {{- end }} {{- end -}} + +{{/* +OpenTelemetry — returns "true" when observability.otel.enabled is set, else "". +*/}} +{{- define "plane.otel.enabled" -}} +{{- if and .Values.observability .Values.observability.otel .Values.observability.otel.enabled -}}true{{- end -}} +{{- end -}} + +{{/* +envFrom entry for the shared OTEL ConfigMap. Call with the root context and +nindent to the envFrom list depth, e.g. + {{- include "plane.otel.envFrom" $ | nindent 10 }} +*/}} +{{- define "plane.otel.envFrom" -}} +{{- if eq (include "plane.otel.enabled" .) "true" -}} +- configMapRef: + name: {{ .Release.Name }}-otel-vars + optional: false +{{- end -}} +{{- end -}} + +{{/* +Per-workload OTEL_SERVICE_NAME (overrides the shared ConfigMap so each workload +reports its own service.name). Call with a dict and nindent, e.g. + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "api") | nindent 10 }} +*/}} +{{- define "plane.otel.serviceEnv" -}} +{{- if eq (include "plane.otel.enabled" .ctx) "true" -}} +- name: OTEL_SERVICE_NAME + value: {{ .service | quote }} +{{- end -}} +{{- end -}} diff --git a/charts/plane-enterprise/templates/config-secrets/otel.yaml b/charts/plane-enterprise/templates/config-secrets/otel.yaml new file mode 100644 index 0000000..8b58431 --- /dev/null +++ b/charts/plane-enterprise/templates/config-secrets/otel.yaml @@ -0,0 +1,42 @@ +{{- if eq (include "plane.otel.enabled" .) "true" }} +# Shared OpenTelemetry env for the backend workloads. Mounted via envFrom; each +# workload additionally sets an inline OTEL_SERVICE_NAME (see plane.otel.serviceEnv). +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ .Release.Namespace }} + name: {{ .Release.Name }}-otel-vars +data: + OTEL_ENABLED: "1" + {{- with .Values.observability.otel.endpoint }} + OTEL_EXPORTER_OTLP_ENDPOINT: {{ . | quote }} + {{- end }} + OTEL_EXPORTER_OTLP_PROTOCOL: {{ .Values.observability.otel.protocol | default "grpc" | quote }} + {{- with .Values.observability.otel.headers }} + OTEL_EXPORTER_OTLP_HEADERS: {{ . | quote }} + {{- end }} + {{- $parts := list }} + {{- with .Values.observability.otel.environment }}{{- $parts = append $parts (printf "deployment.environment.name=%s" .) }}{{- end }} + {{- with .Values.observability.otel.resourceAttributes }}{{- $parts = append $parts . }}{{- end }} + {{- if $parts }} + OTEL_RESOURCE_ATTRIBUTES: {{ join "," $parts | quote }} + {{- end }} + {{- if .Values.observability.otel.debugConsole }} + OTEL_DEBUG_CONSOLE: "1" + {{- end }} + {{- with .Values.observability.otel.sampler }} + OTEL_TRACES_SAMPLER: {{ . | quote }} + {{- end }} + {{- with .Values.observability.otel.samplerArg }} + OTEL_TRACES_SAMPLER_ARG: {{ . | quote }} + {{- end }} + {{- if .Values.observability.otel.frontend.enabled }} + FRONTEND_OTEL_ENABLED: "1" + {{- with .Values.observability.otel.frontend.endpoint }} + FRONTEND_OTLP_ENDPOINT: {{ . | quote }} + {{- end }} + {{- with .Values.observability.otel.frontend.headers }} + FRONTEND_OTLP_HEADERS: {{ . | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/plane-enterprise/templates/workloads/api.deployment.yaml b/charts/plane-enterprise/templates/workloads/api.deployment.yaml index d2c6ea0..310f05b 100644 --- a/charts/plane-enterprise/templates/workloads/api.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/api.deployment.yaml @@ -91,12 +91,14 @@ spec: name: {{ if not (empty .Values.external_secrets.silo_env_existingSecret) }}{{ .Values.external_secrets.silo_env_existingSecret }}{{ else }}{{ .Release.Name }}-silo-secrets{{ end }} optional: false {{- end }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CAEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "api") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/automation-consumer.deployment.yaml b/charts/plane-enterprise/templates/workloads/automation-consumer.deployment.yaml index 17c9896..7a696cb 100644 --- a/charts/plane-enterprise/templates/workloads/automation-consumer.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/automation-consumer.deployment.yaml @@ -52,9 +52,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.opensearch_existingSecret) }}{{ .Values.external_secrets.opensearch_existingSecret }}{{ else }}{{ .Release.Name }}-opensearch-secrets{{ end }} optional: false - {{- if .Values.extraEnv }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (eq (include "plane.otel.enabled" .) "true") }} env: + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "automation-consumer") | nindent 10 }} + {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} + {{- end }} {{- end }} serviceAccount: {{ .Release.Name }}-srv-account diff --git a/charts/plane-enterprise/templates/workloads/beat-worker.deployment.yaml b/charts/plane-enterprise/templates/workloads/beat-worker.deployment.yaml index b79a1da..95273bf 100644 --- a/charts/plane-enterprise/templates/workloads/beat-worker.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/beat-worker.deployment.yaml @@ -53,9 +53,13 @@ spec: name: {{ if not (empty .Values.external_secrets.silo_env_existingSecret) }}{{ .Values.external_secrets.silo_env_existingSecret }}{{ else }}{{ .Release.Name }}-silo-secrets{{ end }} optional: false {{- end }} - {{- if .Values.extraEnv }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (eq (include "plane.otel.enabled" .) "true") }} env: + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "beat-worker") | nindent 10 }} + {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} + {{- end }} {{- end }} serviceAccount: {{ .Release.Name }}-srv-account diff --git a/charts/plane-enterprise/templates/workloads/live.deployment.yaml b/charts/plane-enterprise/templates/workloads/live.deployment.yaml index a95657f..fa6d81c 100644 --- a/charts/plane-enterprise/templates/workloads/live.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/live.deployment.yaml @@ -94,11 +94,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.live_env_existingSecret) }}{{ .Values.external_secrets.live_env_existingSecret }}{{ else }}{{ .Release.Name }}-live-secrets{{ end }} optional: false - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CANodeEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "live") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/outbox-poller.deployment.yaml b/charts/plane-enterprise/templates/workloads/outbox-poller.deployment.yaml index e7c3dc2..af917ec 100644 --- a/charts/plane-enterprise/templates/workloads/outbox-poller.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/outbox-poller.deployment.yaml @@ -46,9 +46,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.app_env_existingSecret) }}{{ .Values.external_secrets.app_env_existingSecret }}{{ else }}{{ .Release.Name }}-app-secrets{{ end }} optional: false - {{- if .Values.extraEnv }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (eq (include "plane.otel.enabled" .) "true") }} env: + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "outbox-poller") | nindent 10 }} + {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} + {{- end }} {{- end }} serviceAccount: {{ .Release.Name }}-srv-account diff --git a/charts/plane-enterprise/templates/workloads/pi-api.deployment.yaml b/charts/plane-enterprise/templates/workloads/pi-api.deployment.yaml index be50d20..6597f2a 100644 --- a/charts/plane-enterprise/templates/workloads/pi-api.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/pi-api.deployment.yaml @@ -86,11 +86,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.opensearch_existingSecret) }}{{ .Values.external_secrets.opensearch_existingSecret }}{{ else }}{{ .Release.Name }}-opensearch-secrets{{ end }} optional: false - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CAEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "pi-api") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/pi-beat.deployment.yaml b/charts/plane-enterprise/templates/workloads/pi-beat.deployment.yaml index 7f64ae1..11b3ca3 100644 --- a/charts/plane-enterprise/templates/workloads/pi-beat.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/pi-beat.deployment.yaml @@ -63,11 +63,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.opensearch_existingSecret) }}{{ .Values.external_secrets.opensearch_existingSecret }}{{ else }}{{ .Release.Name }}-opensearch-secrets{{ end }} optional: false - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CAEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "pi-beat") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/pi-worker.deployment.yaml b/charts/plane-enterprise/templates/workloads/pi-worker.deployment.yaml index 599c747..62ab495 100644 --- a/charts/plane-enterprise/templates/workloads/pi-worker.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/pi-worker.deployment.yaml @@ -63,11 +63,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.opensearch_existingSecret) }}{{ .Values.external_secrets.opensearch_existingSecret }}{{ else }}{{ .Release.Name }}-opensearch-secrets{{ end }} optional: false - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CAEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "pi-worker") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/silo.deployment.yaml b/charts/plane-enterprise/templates/workloads/silo.deployment.yaml index 9346755..d0ce4ae 100644 --- a/charts/plane-enterprise/templates/workloads/silo.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/silo.deployment.yaml @@ -120,11 +120,13 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CANodeEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "silo") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/templates/workloads/space.deployment.yaml b/charts/plane-enterprise/templates/workloads/space.deployment.yaml index 861e2b8..242dc72 100644 --- a/charts/plane-enterprise/templates/workloads/space.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/space.deployment.yaml @@ -56,9 +56,16 @@ spec: limits: memory: {{ .Values.services.space.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.services.space.cpuLimit | default "500m" | quote}} - {{- if .Values.extraEnv }} + {{- if eq (include "plane.otel.enabled" .) "true" }} + envFrom: + {{- include "plane.otel.envFrom" $ | nindent 10 }} + {{- end }} + {{- if or .Values.extraEnv (eq (include "plane.otel.enabled" .) "true") }} env: + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "space") | nindent 10 }} + {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} + {{- end }} {{- end }} serviceAccount: {{ .Release.Name }}-srv-account serviceAccountName: {{ .Release.Name }}-srv-account diff --git a/charts/plane-enterprise/templates/workloads/worker.deployment.yaml b/charts/plane-enterprise/templates/workloads/worker.deployment.yaml index 8a7531a..9319a60 100644 --- a/charts/plane-enterprise/templates/workloads/worker.deployment.yaml +++ b/charts/plane-enterprise/templates/workloads/worker.deployment.yaml @@ -68,12 +68,14 @@ spec: name: {{ if not (empty .Values.external_secrets.silo_env_existingSecret) }}{{ .Values.external_secrets.silo_env_existingSecret }}{{ else }}{{ .Release.Name }}-silo-secrets{{ end }} optional: false {{- end }} + {{- include "plane.otel.envFrom" $ | nindent 10 }} - {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) }} + {{- if or .Values.extraEnv (include "plane.s3CAEnabled" .) (eq (include "plane.otel.enabled" .) "true") }} env: {{- with (include "plane.s3CAEnvVars" .) }} {{ . | indent 10 }} {{- end }} + {{- include "plane.otel.serviceEnv" (dict "ctx" $ "service" "worker") | nindent 10 }} {{- if .Values.extraEnv }} {{- toYaml .Values.extraEnv | nindent 10 }} {{- end }} diff --git a/charts/plane-enterprise/values.yaml b/charts/plane-enterprise/values.yaml index 6913c4b..68ea3a4 100644 --- a/charts/plane-enterprise/values.yaml +++ b/charts/plane-enterprise/values.yaml @@ -636,3 +636,46 @@ extraEnv: [] # value: "http://proxy.example.com:8080" # - name: NO_PROXY # value: "localhost,127.0.0.1,.example.com" + +# OpenTelemetry (traces + logs + metrics). Off by default. When enabled, a shared +# ConfigMap (-otel-vars) is mounted via envFrom into the backend +# workloads (api, worker, beat-worker, automation-consumer, outbox-poller, silo, +# live, space, pi-api, pi-beat, pi-worker), and each gets an inline +# OTEL_SERVICE_NAME so it reports its own service.name. web/admin are NOT wired: +# their only OTel is browser tracing, configured from the API's instance config +# via the frontend.* keys below — not per-workload env. +observability: + otel: + enabled: false + # OTLP collector endpoint. An https:// endpoint => secure gRPC. + endpoint: '' + protocol: grpc + # Extra OTLP exporter headers, "k1=v1,k2=v2" (e.g. a collector ingestion key). + headers: '' + # Deployment environment tag. Emitted as deployment.environment.name (the + # CURRENT semconv key — matches the node/pi/browser services for cross-service + # filtering). Leave blank to omit. + environment: '' + # Any additional resource attributes, "k1=v1,k2=v2" (merged after environment). + resourceAttributes: '' + # Print spans to stdout (debug only). + debugConsole: false + # Trace sampler. always_on = export every span the service sees and, unlike + # parentbased_*, does NOT defer to an upstream traceparent's sampled flag — so + # browser-initiated POST/user-action traces aren't silently dropped (the "only + # GET shows up" symptom). For prod use parentbased_traceidratio with a ratio + # AND ensure browser tracing samples consistently. samplerArg is ignored by + # always_on. + sampler: always_on + samplerArg: '1.0' + # Browser/client tracing for web/admin/space — served to browsers by the API + # (read only by the API container). Turns on when enabled AND endpoint is set. + frontend: + enabled: false + # PUBLIC OTLP/HTTP endpoint the browser posts to (must be internet-reachable + # and CORS-enabled for the Plane web origin; the client appends /v1/traces). + endpoint: '' + # A non-empty header is REQUIRED cross-origin: it forces the web exporter + # onto XHR instead of navigator.sendBeacon (sendBeacon sends credentials, + # which CORS rejects against a wildcard ACAO). Value is arbitrary. + headers: 'x-otlp-browser=1'