Skip to content

Commit 97e35b9

Browse files
TehreemNisaclaude
authored andcommitted
feat(crossplane-observability): ship the optional core metrics Service
The chart offers `coreServiceMonitor` but never shipped anything for it to select. Crossplane core serves /metrics with `metrics.enabled=true`, but upstream ships no Service in front of that port, so on any cluster without a hand-applied one the monitor matches nothing and every core rule and panel evaluates against no data — silently. Found live on eu-2: the addon deployed clean (14 objects, 21 rules health=ok) and the dashboard was still empty, because eu-2 has no crossplane-metrics Service, no ServiceMonitor/PodMonitor and no inventory exporter. us-2 only works because three unowned `kubectl apply`ed platform objects happen to exist there. Probing the eu-2 core pod IP directly returned HTTP 200 and 1.84 MB of metrics, so the data was there all along with nothing to scrape it. Adds `prometheus.monitors.coreMetricsService` (default off), a headless Service reproducing the object us-2 proved works. Two couplings are derived rather than restated, so the pair cannot drift apart: * the Service name defaults to `crossplane.core.job`, because prometheus-operator takes a ServiceMonitor's job label from the Service — the same label the alert and recording expressions filter on; * its labels are `crossplane.core.serviceMonitorSelector.matchLabels`, so the monitor this chart ships always matches the Service it ships. `selector` stays install-specific and must be set per cluster: the upstream chart labels core pods `release: <helm release name>`, which is `crossplane` on eu-2 but `crossplane-operator` on us-2. Verified: rendered Service is label-, port- and headless-identical to us-2's working hand-applied one; `kubectl apply --dry-run=server` on eu-2 accepts it and the default selector matches the live core pod. `./tests/validate.sh` green (25 objects strict-validated, up from 24; 29 rules; 39 panel queries). Also documents in docs/values-stakater-cloud-example.yaml that "the cluster already scrapes Crossplane" is a per-cluster assumption, not a Stakater Cloud guarantee, plus the PodMonitor job-label trap (`<ns>/<podmonitor-name>`, which does not match the example's `crossplane.providers.job`). Refs SA-8539, #283 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 96b492a commit 97e35b9

6 files changed

Lines changed: 96 additions & 3 deletions

File tree

crossplane-observability/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: crossplane-observability
33
description: A Helm chart for Crossplane observability (ServiceMonitor + PrometheusRule + Grafana dashboard), integrated with OpenShift user-workload monitoring.
44
type: application
5-
version: 0.1.6
5+
version: 0.1.7
66
appVersion: "0.0.1"

crossplane-observability/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ UWM evaluates these `PrometheusRule`s in `thanos-ruler-user-workload`.
5252
### 2. Crossplane metrics exposed
5353

5454
- **Core:** start core with `--set metrics.enabled=true` (Helm) so `/metrics` is served.
55+
Upstream ships **no Service** in front of that port, so a `ServiceMonitor` has nothing to
56+
select. Either point `crossplane.core.serviceMonitorSelector` at a metrics Service your
57+
cluster already has, or let this chart create one with
58+
`prometheus.monitors.coreMetricsService.enabled=true` (set its `selector` to your core pod
59+
labels — the upstream chart labels them `app: crossplane`, `release: <release name>`).
5560
- **Providers:** expose a metrics port on each provider via its `DeploymentRuntimeConfig`.
5661
Every crossplane-runtime provider emits `crossplane_managed_resource_*` natively
5762
(ready/synced/TTR/drift) — no external exporter is needed for leaf-MR health.
@@ -61,6 +66,7 @@ Then turn on the scrapers (both default to `false`):
6166
```yaml
6267
prometheus:
6368
monitors:
69+
coreMetricsService: { enabled: true } # only if the cluster has no metrics Service
6470
coreServiceMonitor: { enabled: true }
6571
providerPodMonitor: { enabled: true }
6672
```
@@ -223,6 +229,8 @@ These need cluster/Grafana context an agent can't safely guess:
223229
| `grafana.compositeAlerts.enabled` | `false` | Story 4.1 composite alerts as **Grafana-managed** rules (use on UWM instead of the PrometheusRule composite alerts). |
224230
| `grafana.compositeAlerts.datasourceUid` | `""` | **Required when enabled** — UID of the cross-namespace Prometheus/Thanos datasource in Grafana. |
225231
| `grafana.namespace` | release ns | Namespace to create the GrafanaDashboard in. |
232+
| `prometheus.monitors.coreMetricsService.enabled` | `false` | Create the headless metrics Service for Crossplane core (upstream ships none, so `coreServiceMonitor` has nothing to select without it). Its name defaults to `crossplane.core.job` — which is what makes the `job` label match — and its labels to `crossplane.core.serviceMonitorSelector.matchLabels`. |
233+
| `prometheus.monitors.coreMetricsService.selector` | `{app: crossplane, release: crossplane}` | Pod labels of the Crossplane core Deployment. **Install-specific** — the upstream chart sets `release: <helm release name>`. |
226234
| `prometheus.monitors.coreServiceMonitor.enabled` | `false` | Scrape Crossplane core. |
227235
| `prometheus.monitors.providerPodMonitor.enabled` | `false` | Scrape provider pods. |
228236
| `prometheus.recordingRules.enabled` | `true` | Emit the SLO recording rules. |

crossplane-observability/docs/values-stakater-cloud-example.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,33 @@
66

77
# The cluster already scrapes core + providers via its own ServiceMonitor/PodMonitor, so the
88
# chart's monitors stay OFF; the rules just point at the existing Prometheus jobs.
9+
#
10+
# ⚠️ CONFIRM this per cluster — it is NOT true of every Stakater Cloud cluster. Those
11+
# monitors and the `crossplane-metrics` Service they select are hand-applied platform
12+
# objects, not part of any chart, so a cluster can be missing them entirely and every rule
13+
# and panel then evaluates against nothing. Check with:
14+
# kubectl get servicemonitor,podmonitor -A | grep -i crossplane
15+
# kubectl get svc -n crossplane-system | grep metrics
16+
# If they are absent, turn the chart's own scrape on instead (see the commented block
17+
# below) rather than hand-applying more unowned objects.
918
prometheus:
1019
monitors:
1120
coreServiceMonitor:
1221
enabled: false
1322
providerPodMonitor:
1423
enabled: false
24+
# If the cluster has NO crossplane-metrics Service, uncomment these three blocks. The
25+
# Service is named after `crossplane.core.job` below, so the core rules keep matching;
26+
# set `selector` to your core pods' labels (the upstream chart uses
27+
# `release: <helm release name>` — `crossplane-operator` on us-2, `crossplane` on eu-2)
28+
# and override `crossplane.providers.job` to
29+
# `crossplane-system/<release>-crossplane-observability-providers`, because a PodMonitor's
30+
# job label is `<namespace>/<podmonitor-name>` and will not match the value set below.
31+
# coreMetricsService:
32+
# enabled: true
33+
# selector:
34+
# app: crossplane
35+
# release: crossplane
1536
rules:
1637
fleet:
1738
# On UWM, keep the PrometheusRule composite alerts OFF — Grafana handles them
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{{- if and ( .Values.prometheus.monitors.coreMetricsService ) ( .Values.prometheus.monitors.coreMetricsService.enabled ) }}
2+
{{- $svc := .Values.prometheus.monitors.coreMetricsService }}
3+
{{- $sm := .Values.prometheus.monitors.coreServiceMonitor | default dict }}
4+
{{- $sel := .Values.crossplane.core.serviceMonitorSelector | default dict }}
5+
# Headless Service in front of the Crossplane core pods' /metrics port.
6+
#
7+
# Crossplane core serves /metrics (with `metrics.enabled=true`) but upstream ships NO Service
8+
# for it, so `coreServiceMonitor` selects nothing unless the cluster happens to have one
9+
# hand-applied. Enable this and the chart owns both halves of the scrape.
10+
#
11+
# Two couplings are derived rather than restated, so the pair cannot drift apart:
12+
# * the Service NAME becomes the `job` label on every core metric (prometheus-operator
13+
# derives a ServiceMonitor's job from the Service), so it defaults to
14+
# `crossplane.core.job` — the value the alert and recording expressions filter on;
15+
# * its LABELS are the ServiceMonitor's own selector `matchLabels`, so the monitor this
16+
# chart ships always matches the Service this chart ships. (Only `matchLabels` is
17+
# reflected — a selector written with `matchExpressions` must be matched by hand.)
18+
apiVersion: v1
19+
kind: Service
20+
metadata:
21+
name: {{ $svc.name | default .Values.crossplane.core.job }}
22+
namespace: {{ include "crossplane-observability.namespace" . }}
23+
labels:
24+
{{- /* the monitor's selector labels win over the chart defaults — see the note above */}}
25+
{{- merge (deepCopy ($sel.matchLabels | default dict)) (fromYaml (include "crossplane-observability.labels" .)) | toYaml | nindent 4 }}
26+
spec:
27+
# Headless: this Service exists only to produce Endpoints for the scrape. Nothing dials
28+
# it by name, so it needs no ClusterIP.
29+
clusterIP: None
30+
selector:
31+
{{- toYaml $svc.selector | nindent 4 }}
32+
ports:
33+
- name: {{ $sm.port | default "metrics" }}
34+
port: {{ $svc.port }}
35+
targetPort: {{ $svc.targetPort }}
36+
protocol: TCP
37+
{{- end }}

crossplane-observability/tests/validate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ green() { printf "\033[32m%s\033[0m\n" "$*"; }
2424
step() { printf "\n\033[1m== %s ==\033[0m\n" "$*"; }
2525

2626
ALL_FLAGS=(
27+
--set prometheus.monitors.coreMetricsService.enabled=true
2728
--set prometheus.monitors.coreServiceMonitor.enabled=true
2829
--set prometheus.monitors.providerPodMonitor.enabled=true
2930
--set upjet.enabled=true

crossplane-observability/values.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ crossplane:
3333
# Label selector for the core metrics Service (used only when coreServiceMonitor is
3434
# enabled). Default targets the metrics Service by component; the upstream metrics
3535
# Service has no per-instance label, so don't require one.
36+
# `prometheus.monitors.coreMetricsService` labels the Service it creates with exactly
37+
# these matchLabels, so leaving both at their defaults always pairs.
3638
serviceMonitorSelector:
3739
matchLabels:
3840
app.kubernetes.io/name: crossplane
@@ -148,11 +150,35 @@ grafana:
148150
# Prometheus configuration
149151
# ---------------------------------------------------------------------------
150152
prometheus:
151-
# Scrape configuration. Both default to disabled — turn them on once the matching
153+
# Scrape configuration. All default to disabled — turn them on once the matching
152154
# metrics ports are confirmed exposed (see docs/roadmap.md, Decision 2).
153155
monitors:
156+
# Headless Service in front of the core pods' /metrics port. Upstream Crossplane
157+
# serves /metrics but ships NO Service for it, so `coreServiceMonitor` below selects
158+
# nothing unless the cluster has one hand-applied. Enable this to have the chart own
159+
# both halves of the core scrape.
160+
coreMetricsService:
161+
enabled: false
162+
# Service name. Prometheus-operator derives a ServiceMonitor's `job` label from the
163+
# Service, so this name IS the job label — it defaults to `crossplane.core.job` so
164+
# the alert/recording expressions match without a second setting to keep in sync.
165+
# Override only if your Prometheus relabels the job.
166+
name: ""
167+
# Pod labels of the Crossplane core Deployment. Varies by install: the upstream
168+
# chart sets `release: <helm release name>`, so this default fits a release named
169+
# `crossplane`. Check with
170+
# kubectl get pod -n <ns> -l app=crossplane --show-labels
171+
selector:
172+
app: crossplane
173+
release: crossplane
174+
# Port the core pods serve /metrics on. Crossplane's Deployment does not declare it
175+
# as a containerPort, which is fine — a Service may target a raw port number. The
176+
# port NAME is taken from `coreServiceMonitor.port` so the two always agree.
177+
port: 8080
178+
targetPort: 8080
154179
# Scrapes Crossplane core /metrics (controller-runtime, workqueue, circuit
155-
# breaker, function-pipeline metrics).
180+
# breaker, function-pipeline metrics). Needs a metrics Service to select — either
181+
# `coreMetricsService` above, or one already present on the cluster.
156182
coreServiceMonitor:
157183
enabled: false
158184
port: metrics

0 commit comments

Comments
 (0)