Skip to main content

Metrics pipeline

Metrics needs configuration on both sides of the connector: a gateway and a storage backend on the hub, and a collector on each control plane you want to collect from. This page covers both. See Metrics for what the feature does and how to query it.

Hub doesn't run the metrics store. You point the gateway at a Prometheus-compatible backend you already operate, and the query API reads back from the same backend.

Prerequisites

Before you start, have the following ready:

  • A running Hub installation. See Installing Hub.
  • Helm access to the Hub release, so you can run helm upgrade. See the chart reference for what <chart-ref> stands for.
  • At least one connected control plane or space. See Connect a control plane and Connect a space.
  • A Prometheus-compatible backend that accepts remote-write and serves the Prometheus HTTP query API, reachable from the Hub cluster.
  • The feature flag server enabled. The server is on by default; the Metrics gate it serves isn't. See Feature flags.

Step 1: Choose a backend

The gateway writes through one exporter, and the query API reads from one URL. Both come from hub-core.otelGateway.metrics.

backendWrites withUse it for
prometheusPrometheus remote-writeSelf-hosted Prometheus, Thanos, Mimir, or any remote-write endpoint.
gmpThe native Google Managed Prometheus exporterGoogle Managed Prometheus.
ampRemote-writeAmazon Managed Prometheus.

queryURL is the base URL the query API reads from, and it's separate from the write endpoint on every backend. For self-hosted Prometheus the two usually share a host. For managed Prometheus they don't.

Step 2: Configure the hub

Enable the Metrics gate, the OTel gateway, and a backend. The gate turns on the metrics.hub.upbound.io API group; the gateway receives, labels, and forwards.

  1. Add the gate and the gateway to your values.yaml.

    hub-core:
    api:
    featureFlags:
    gates:
    Metrics: true
    otelGateway:
    enabled: true
    metrics:
    backend: prometheus
    queryURL: http://prometheus-server.monitoring.svc
    prometheus:
    writeEndpoint: http://prometheus-server.monitoring.svc/api/v1/write
    insecure: true
  2. Confirm your Prometheus accepts remote-write. Prometheus rejects it unless you start it with the receiver enabled:

    prometheus --web.enable-remote-write-receiver

Apply the values with an upgrade:

helm upgrade hub <chart-ref> \
--namespace hub \
--values values.yaml

Confirm the gateway is running:

kubectl --namespace hub get pods --selector app.kubernetes.io/component=otel-gateway

Step 3: Turn on collection for control planes

The hub side receives, but nothing sends until you enable the collector on the connector. The collector is off by default in the hub-connector chart.

Upgrade the connector on the control plane:

helm upgrade hub-connector oci://xpkg.upbound.io/upbound/hub-connector \
--kube-context "$CONTROL_PLANE_CONTEXT" \
--namespace upbound-system \
--reuse-values \
--set collector.enabled=true \
--set rsm.enabled=true

What the collector scrapes

The collector discovers Pods in collector.scrapeNamespaces and keeps only those annotated prometheus.io/scrape: "true". It scrapes port 8080 and path /metrics unless the Pod overrides them with prometheus.io/port and prometheus.io/path.

ValueDefaultDescription
collector.scrapeNamespaces[crossplane-system, upbound-system]Namespaces to discover Pods in. Upstream Crossplane installs into crossplane-system; UXP installs into upbound-system.
collector.scrapeInterval15sHow often to scrape each target.
collector.metricAllowlistThe curated setMetric-name regexps that may leave the control plane.

Turn on resource state metrics

rsm.enabled deploys resource-state-metrics, a SIG Instrumentation project that turns resource status conditions into kube_customresource_resource_condition gauges. It's optional and off by default. The collector scrapes it as an extra target, and the kube_customresource_.* entry in the default allowlist lets those series through.

rsm.apiGroups selects which API groups it watches, and defaults to "*".

warning

At "*" the component emits a series per condition per resource in the control plane, including core Kubernetes objects. Around 1,000 resources produces about 9,000 series. Narrow rsm.apiGroups to the Crossplane groups you care about before enabling it on a large control plane.

Step 4: Verify

  1. Query the API group to confirm it responds. With the gate on, /apis/metrics.hub.upbound.io/v1alpha1 returns:

    {
    "kind": "APIResourceList",
    "apiVersion": "v1",
    "groupVersion": "metrics.hub.upbound.io/v1alpha1",
    "resources": [
    {
    "name": "queries",
    "singularName": "",
    "namespaced": false,
    "kind": "Query",
    "verbs": [
    "create"
    ]
    }
    ]
    }
  2. Query for anything the pipeline should be producing. Metrics take a scrape interval or two to appear after you enable the collector.

    curl -sk -H "Authorization: Bearer $TOKEN" \
    -X POST "<hub-url>/apis/metrics.hub.upbound.io/v1alpha1/queries" \
    -H 'Content-Type: application/json' \
    -d '{
    "apiVersion": "metrics.hub.upbound.io/v1alpha1",
    "kind": "Query",
    "query": {
    "promql": "count(controller_runtime_reconcile_total) by (control_plane_name)",
    "start": "2026-07-29T12:00:00Z",
    "end": "2026-07-29T13:00:00Z",
    "step": "5m"
    }
    }' | jq '.results.series[].labels'

    One series per collecting control plane means the whole path works.

Add metrics to the allowlist

Both sides filter, so a metric needs adding in two places. The collector's collector.metricAllowlist decides what leaves a control plane, and the gateway's otelGateway.metricAllowlist re-checks the same set on arrival as a safety net. A metric allowed on only one side never reaches storage.

Entries are regular expressions matched against the metric name. Histograms expand into _bucket, _sum, and _count series, so match them with a trailing .*.

  1. Add the metric to the gateway allowlist. Repeat the default entries, since Helm replaces lists rather than merging them:

    hub-core:
    otelGateway:
    metricAllowlist:
    - "controller_runtime_reconcile_total"
    - "controller_runtime_reconcile_errors_total"
    - "controller_runtime_reconcile_time_seconds.*"
    - "upjet_resource_external_api_calls_total"
    - "function_run_function_seconds.*"
    - "kube_customresource_.*"
    - "my_provider_custom_metric.*"
  2. Add the same entry to collector.metricAllowlist on every connector that should send it, listing the defaults again for the same reason.

  3. Upgrade both releases.

warning

Every label combination on an allowed metric becomes a stored time series, on every control plane in the fleet. Check that the metric you're adding doesn't carry per-resource or per-request labels before you add it.

Troubleshooting

SymptomFix
404 on the metrics API groupEnable the Metrics gate (hub-core.api.featureFlags.gates.Metrics=true).
hub-core fails to start after enabling the gateThe gate needs hub-core.otelGateway.enabled=true. hub-core exits when the gate is on and the gateway URLs are unset.
Chart fails to render with a missing-backend errorSet hub-core.otelGateway.metrics.backend to prometheus, gmp, or amp.
403 on every queryThe caller has no authorized control planes. Grant access to a realm or a control plane. See RBAC.
Queries succeed but return no seriesConfirm collector.enabled=true on the connector, that the target Pods carry prometheus.io/scrape: "true", and that their namespace is in collector.scrapeNamespaces.
A specific metric never arrivesAdd it to both collector.metricAllowlist and otelGateway.metricAllowlist.
Writes work, queries fail on Amazon Managed PrometheusAnnotate api.serviceAccount with an IRSA role granting aps:QueryMetrics. The gateway's role only covers writes.
Queries fail on Google Managed PrometheusqueryURL must point at a Prometheus-compatible read endpoint. Google Managed Prometheus doesn't serve one directly, so deploy the GMP frontend proxy and use its address.

See also