A Label Vocabulary for Materialize Metrics#
| agent | Claude Opus 5.5 |
|---|---|
| author | Heather Lapointe |
| lastmod | 2026-11-02 00:00:00 +0000 UTC |
| publishdate | 2026-10-02 00:00:00 +0000 UTC |
| status | Ready |
This doc proposes one vocabulary of label names for the metrics Materialize emits, a list of reserved names those metrics must not carry, and a two-pass path to both.
The first pass is relabeling in this repository’s shipped scrapers, on this repository’s release cadence.
The second pass is adoption in MaterializeInc/materialize, where the metrics are registered, until the relabeling has nothing left to do.
It owes the label-family harmonization item in DEP-207.
It also writes down the relabeling contract that DEP-109 planned and was canceled before writing.
The central claim is that relabeling is how this repository makes a promise about labels it does not emit. Upstream metric labels are an unstable interface by policy, so nothing a consumer writes against them can be promised today. A translation layer owned here turns that into a surface this repository can commit to. Consumers are written once, against the canonical names. The second pass changes who produces those names and nothing about what they are, so a consumer cannot observe it happening.
A second claim decides what moves upstream. The identity of the thing measured belongs to the emitter, and the identity of the process doing the emitting belongs to the pipeline. A Materialize cluster id means the same thing in every deployment, so Materialize should emit it under the canonical name. A Kubernetes cluster, a namespace, and an environment’s name differ by deployment, so they stay target and external labels permanently. This is DEP-109’s principle restated: a label that is the same across deployment modes is emitted correctly at the source, and one that varies is normalized in the pipeline.
The doc also settles how every consumer keys a Materialize environment, which several consumers are waiting on.
The key is environment_id, the spec.environmentId UUID of the Materialize resource, and it is carried on every series.
The display name is environment_name, carried on mz_environment_info and joined in rather than stamped everywhere.
Both are read from the Materialize resource, which is the only place either is declared.
materialize_cloud_organization_name is retired: it is the resource’s metadata.name, copied through a pod label whose name describes Materialize Cloud’s billing model rather than the value it holds.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The keywords carry the obligations on emitters, on the shipped monitors, and on the vocabulary file. They appear on the reserved list, on the form of each relabeling rule, and on the order of the upstream renames.
Goals#
Functional requirements, framed as value-first user stories. Priority tags (Must / Should / Could) are relative to the first shipped version.
The stakeholders are operators reading the shipped dashboards, customers who bring their own metrics backend, the authors of dashboards and alerts in this repository, Materialize engineers adding metrics upstream, and Materialize Cloud as it adopts this stack.
- [Must] As any consumer of this stack’s telemetry, whether Console, the tenant-scoped read path, Cloud or a customer’s own dashboards, I want one authoritative key for a Materialize environment, so that every consumer scopes to an environment the same way.
- [Must] As an operator, I want that key to be unique within my Kubernetes cluster and never to change, so that an environment’s history stays one set of series for its whole life.
- [Must] As an operator, I want one label name for a Materialize cluster’s id on every metric that describes a cluster, so that a query written against one family works against the next without
label_replace. - [Must] As an operator running more than one Kubernetes cluster, I want
clusteron every series to name the Kubernetes cluster, so that alerts from two clusters never collapse into one incident. - [Must] As a customer with my own metrics backend, I want Materialize’s metrics to carry no label my backend claims for itself, so that ingest neither renames them nor lets them overwrite its own identity labels.
- [Must] As a dashboard or alert author, I want the canonical names in the scrape output before upstream changes anything, so that dashboards and rules migrate once rather than once per Materialize release.
- [Must] As a customer with dashboards of my own, I want a renamed label to keep its old name for a deprecation window, so that my dashboards move on my schedule rather than on upgrade.
- [Must] As a customer routing alerts in my own Alertmanager, I want the labels on shipped alerts to change only under the deprecation cycle, so that a routing tree written against them does not silently stop matching.
- [Should] As a customer scraping Materialize with my own Prometheus, I want the canonical names to come from the shipped monitors themselves, so that I get them without running the bundled pipeline.
- [Should] As an operator, I want an
_infojoin to add a label that says what it names, so that the cluster and replica info metrics can be joined in one expression. - [Should] As an operator, I want to rename an environment’s display name without splitting its series, so that a name is a label for humans rather than part of every series’ identity.
- [Should] As an operator diagnosing an outage, I want an environment’s name to resolve while its environmentd is down, so that the dashboard for the broken environment still says which one it is.
- [Should] As a Materialize engineer adding a metric, I want CI to refuse a reserved label name and to flag a non-canonical spelling of a canonical concept, so that the vocabulary does not erode one metric at a time.
- [Should] As a customer routing alerts, I want one documented list of the labels on shipped metrics and alerts, so that routing has a contract to be written against.
- [Could] As a Materialize Cloud engineer, I want Cloud’s scrape path to produce the same vocabulary, so that a query written for self-managed answers the same question in Cloud.
- [Could] As an operator, I want the build version to carry one label name across environmentd, clusterd, balancerd and persist, so that a version-skew panel needs no per-component cases.
Technical BLUF#
- One cluster id, five label names.
instance_idon environmentd’s controller metrics,compute_instanceon the adapter’s,cluster_idon the_infofamily,compute_cluster_idfrom the SQL exporter, andcluster_environmentd_materialize_cloud_cluster_idon everything clusterd emits. Replica id has three names, cluster name three, and a catalog object at least six. - The long form is named for its transport, not its meaning.
cluster_environmentd_materialize_cloud_cluster_idis the Kubernetes pod-label keycluster.environmentd.materialize.cloud/cluster-idpassed through the scraper’s sanitizer bypodTargetLabels. Nobody upstream chose that name. - Upstream has already chosen the canonical spelling three times.
mz_cluster_infoandmz_replica_info, the federated/metrics/publicendpoint, and the upstream cluster-metrics design doc all usecluster_id,replica_id,cluster_nameandreplica_name. This design adopts that vocabulary and finishes it. clusteris emitted today and it is destructive. clusterd stampscluster="compute"as a const label onmz_timely_step_duration_secondsandmz_cluster_handle_command_duration_seconds. Remote-writeexternal_labelsand both rulers fillclusteronly where it is absent, so those series lose the Kubernetes-cluster identity the stack puts on everything else. No query reads either family, so the fix is free today.- Alias rules copy, never rename, and they retire themselves. A rule fills
cluster_idfrominstance_idonly wherecluster_idis empty andinstance_idis not. When upstream emitscluster_id, the rule matches nothing. A 1:1 copy adds no series. - The rules belong in the shipped monitors, not the gateway. The Prometheus Scrapers component is consumed directly by customers’ own Prometheus and by the GMP flavor. A rule in the Alloy gateway reaches only the bundled path.
- The transpiler has to learn to rename a pod label first.
podTargetLabelscan only copy a pod label under its sanitized key. The operator flavor needsrelabelings, GMP needstargetLabels.fromPod[].to, and the GMP flavor drops target relabelings silently today. - Process identity stays in the pipeline.
cluster,namespace,pod, the environment label, and clusterd’s own cluster and replica ids are target or external labels, and remain so after the second pass. - The upstream hook already exists.
bin/gen-metrics-catalogwalks everymetric!invocation, records its label keys, and CI diffs the result. A reserved-name and canonical-spelling check is a lint over a file that is already generated. - The federated endpoint has a latent duplicate-label bug.
add_replica_labelsappendscluster_idandreplica_idwithout checking whether the series already has them. It is harmless only because no clusterd metric does yet. The second pass fixes it before any emitter adopts a canonical name. - Alert labels get the strictest treatment. Rules pass
instance_idand the long-form ids through to alerts, and an external routing tree may match on them. Rules aggregate by both names through the overlap, so an alert never loses a label inside the window. - An environment is keyed by
environment_id, read fromMaterialize.spec.environmentId. It is a UUID that orchestratord already resolves on first reconcile and already refuses to duplicate within a Kubernetes cluster, and the CRD forbids changing it because persist’s layout depends on it. Every series from an environment’s processes carries it. - An environment is named by
environment_name, onmz_environment_infoonly. The name ismetadata.nametoday and could become a mutablespec.environmentName. Names are not unique and may change, so they are joined in, not stamped on every series, which is the same rule this doc applies to cluster and replica names. - The environment key needs one small upstream change before this repo can relabel it.
No pod label carries
spec.environmentIdtoday. orchestratord addingmaterialize.cloud/environment-idto the labels it puts on every pod, and emittingmz_environment_infoandmz_environment_active_generation, is a single change and the first item of upstream work. %%{mzEnvironmentFilter}is the switch. The query registry names the fragment 250 times and writes the environment label out 24 times, 22 of them in the two alert files. Moving the fragment frommaterialize_cloud_organization_nametoenvironment_idis one change, made once the supported operator floor carries the pod label.- Cloud is the documented exception to global uniqueness. Cloud sets
spec.environmentIdto the organization’s UUID, which repeats in every region the organization uses. The globally unique key is therefore(cluster, environment_id), which is one more reasonclusteris reserved.
Non-goals#
- Cardinality. Series counts, per-shard label bounding (PER-101), and dropping families belong to another team. Every change here is a rename or a 1:1 copy, and none adds or removes a series.
- Metric names. The
v2_mz_prefix, units and suffixes are the same class of problem with a different mechanism, and%%{mzSqlPrefix}already handles the prefix. - Label values.
status="ok"besidestatus="success"is real and is left to a later pass. See open questions. - The log vocabulary. Loki stream labels are a committed surface with rules of their own. The environment key reaches logs as structured metadata; the rest of the log pipeline’s names are a follow-up.
- OpenTelemetry-native emission. If Materialize emits OTLP itself one day, this vocabulary maps onto a
materialize.*attribute namespace. Designing that mapping is not this doc. - Organization labels. An organization is a Materialize Cloud billing concept with no counterpart in self-managed. Cloud MAY stamp one as a pipeline label of its own; the vocabulary does not define it.
- Cloud’s scrape configuration. This doc says what Cloud would need in order to converge, not how Cloud’s Pulumi gets there.
- Tenancy classification. Which families are environment-scoped is DEP-272’s question. This doc names the labels that classification reads.
What exists today#
| Capability | State | Where |
|---|---|---|
Explicit podTargetLabels allowlist on every Materialize monitor | ✅ Shipped | packages/prometheus-scrapers/podmonitor-{environmentd,clusterd,sql,materialize-operator}.yaml |
| Kubernetes-cluster identity on every series and alert | ✅ Shipped | cluster, from clusterName, through remote-write external_labels, --label on the Thanos ruler, and alert_relabel_configs on the Loki ruler |
| Renaming target labels from meta labels into chosen names | ✅ Shipped, cAdvisor only | scrapeconfig-cadvisor.yaml maps node labels to node, topology_kubernetes_io_* and friends |
| A unique, immutable environment id | ✅ Shipped upstream | Materialize.spec.environmentId; orchestratord fills it from the license key or a random UUID on first reconcile and refuses a duplicate within the Kubernetes cluster |
| The environment id on any pod label or metric | ❌ Missing | orchestratord’s pod labels carry the resource’s name, namespace and resource id, and not spec.environmentId |
| An environment label on every series | ⚠️ Shipped, misnamed | materialize_cloud_organization_name, which is metadata.name copied from the materialize.cloud/organization-name pod label |
An environment _info metric | ❌ Missing | No series carries an environment’s name independently of its own processes |
metricRelabelings on any Materialize monitor | ❌ None | All four declare relabelings: [] or omit it, and none has metric rules |
| A canonical name for any Materialize entity | ❌ Missing | Five names for a cluster id; see one concept, many names |
| A reserved-label list | ❌ Missing | Nothing in docs/content names one |
| Label-name validation upstream | ❌ Missing | metric! takes free-form string literals; gen-metrics-catalog records label keys and checks none |
_info payload labels qualified by entity | ⚠️ Partial | cluster_id and replica_id are; name, type and size are not, so two joins collide |
| GMP flavor honouring GMP’s protected labels | ❌ Missing | metricRelabelings are cloned unfiltered; target relabelings are dropped without a warning |
| A machine-readable record of labels in use | ⚠️ Partial | docs/assets/metrics/metrics.yaml lists labels per metric, but only those in selector matchers, not by, on, group_left or joins |
| Documentation of the label families | ⚠️ Partial | The style guide’s label-family section, written as a field guide to the inconsistency rather than a fix for it |
| A label contract for alert routing | ❌ Owed | Listed under the alerting design’s documentation deliverables |
One concept, many names#
The live install has 77 distinct label names across environmentd, clusterd and the SQL exporter, not counting target labels. Most are dimensions of a single measurement and are fine. The identity labels are the problem, because identity labels are join keys.
| Concept | Names in use | Where each comes from |
|---|---|---|
| Materialize environment | materialize_cloud_organization_name · organization_name · materialize_cloud_organization_id · mz_context_org_name | the resource’s metadata.name, through a pod label, on metrics · the same, as log structured metadata · Cloud only, cut from the name by a regex · Cloud only, from analytics annotations |
| Materialize cluster id | instance_id · compute_instance · cluster_id · compute_cluster_id · cluster_environmentd_materialize_cloud_cluster_id | environmentd controller metrics (33 metric! sites) · mz_determine_timestamp and two siblings in the adapter · the _info family and /metrics/public · the SQL exporter · clusterd, from the pod label |
| Materialize cluster name | name · cluster_name · compute_cluster_name | mz_cluster_info · /metrics/public, and this repository’s own mzClusterName join · the SQL exporter |
| Replica id | replica_id · compute_replica_id · cluster_environmentd_materialize_cloud_replica_id | controller metrics and mz_replica_info · the SQL exporter · clusterd, from the pod label |
| Replica name | name · replica_name · compute_replica_name · replica_full_name | mz_replica_info · /metrics/public · the SQL exporter, twice |
| Catalog object | object_id · global_id · collection_id · source_id · sink_id · id | mz_object_info (both of the first two) · controller and wallclock-lag metrics · storage statistics · mz_storage_regressed_offset_known |
| Persist shard | shard · shard_id | persist-client (34 sites) · storage statistics (11) |
| The compute or storage half of clusterd | cluster · server_name | two const labels in src/compute/src/metrics.rs · the CTP transport metrics |
| Build version | version · mz_version | const labels on mz_start_time_environmentd, mz_persist_metadata_seconds and mz_balancer_metadata_seconds · the SQL exporter |
The consumers in this repository carry the cost.
The cluster picker’s value comes from compute_cluster_id and is matched against three different label names, chosen per query: the long form 24 times, instance_id 18 times and compute_cluster_id 6 times.
The query author has to know which family a metric belongs to, and the style guide exists largely to tell them.
The mzClusterName and mzObjectName helpers exist because a name join needs a label_replace first.
The rendered env-top dashboard spells instance_id 254 times and the long form 156 times, and the rendered rules 86 and 113 times.
The long form is named for its transport#
The Materialize orchestrator labels every clusterd pod cluster.environmentd.materialize.cloud/cluster-id.
The monitors copy that pod label with podTargetLabels, and prometheus-operator sanitizes the key into a label name.
cluster_environmentd_materialize_cloud_cluster_id is therefore the label’s route into Prometheus, spelled out.
It says nothing about what the value is, and it cannot be shortened without a rule that renames it, which no shipped monitor has.
Materialize Cloud reaches the same names by another route: a blanket labelmap over every pod label and every pod annotation, which also produces the _size, _scale, _workers, _replica_role, _cluster_name and _replica_name variants.
The shipped monitors copy only the id pair, which is why those variants are absent on self-managed.
name means four things#
name is the shard’s name on 28 persist families, the cluster’s name on mz_cluster_info, the replica’s name on mz_replica_info, and the object’s name on mz_object_info.
On an _info metric a label is the payload of a group_left, so an unqualified one collides with the next join:
# Even with the ids aligned, the second join's `name` overwrites the first's:
# the result carries the replica's name and has lost the cluster's.
mz_compute_replica_history_dataflow_count
* on (cluster_id) group_left (name) mz_cluster_info
* on (cluster_id, replica_id) group_left (name) mz_replica_infotype and size have the same problem.
mz_cluster_info and mz_replica_info both carry size, meaning the cluster’s managed size on one and the replica’s size on the other.
Architecture#
flowchart LR
subgraph upstream["MaterializeInc/materialize"]
reg["metric! registrations<br/>environmentd · clusterd · balancerd"]
orch["orchestratord<br/>pod labels incl. materialize.cloud/environment-id<br/>mz_environment_info"]
lint["gen-metrics-catalog lint<br/>second pass"]
end
subgraph here["materialize-monitoring · Prometheus Scrapers component"]
vocab[("labels.yaml<br/>canonical · aliases · reserved")]
mon["Shipped monitors<br/>relabelings + metricRelabelings<br/>operator · GMP · classic"]
end
subgraph pipe["Pipeline"]
scr["Any scraper<br/>bundled Alloy · customer Prometheus · GMP"]
ext["Identity stamps<br/>cluster · tenant · replica labels"]
end
consumers["Dashboards · alert rules · customer queries and routing"]
reg -->|"/metrics"| scr
orch -.->|"meta labels · /metrics"| scr
vocab -->|"generated rules"| mon
mon -->|"consumed by"| scr
scr --> ext --> consumers
vocab -.->|"published; checked against"| lint
lint -.-> regThree properties of this diagram carry the design.
The vocabulary is one file, and everything else is generated from it.
The monitors’ rules in all three flavors, the docs table, the query-registry check and the upstream lint all read labels.yaml.
A canonical name added in one of those places and not the others is the drift this design exists to remove.
The rules sit in the monitors, upstream of every scraper.
A customer running their own Prometheus or GMP consumes the monitors directly, and the bundled gateway is one consumer among several.
Identity stamps are added after the rules, so nothing a rule does can touch cluster.
The arrow into the upstream registrations is dotted. The second pass changes who produces the canonical names, not what they are. Nothing to the right of the monitors changes when it happens.
Reserved labels#
A reserved label is a label name that some system on the path from emitter to reader assigns a meaning of its own. Materialize metrics MUST NOT carry a reserved label. Where one is emitted today, the shipped monitors MUST move its value to a canonical name and drop the reserved label, with no overlap. A collision is a defect, and the colliding name has no consumer worth preserving.
| Label | Claimed by | What a collision does | Emitted by Materialize today |
|---|---|---|---|
cluster | This stack’s clusterName stamp and Alertmanager group_by; GMP’s prometheus_target resource; Mimir’s HA tracker; the kube-prometheus mixins | The external label and the rulers’ stamp are skipped, so the series loses its Kubernetes-cluster identity. See below | Yes, cluster="compute" on two clusterd families |
namespace · pod · container · endpoint · service · node | prometheus-operator target labels; GMP (namespace); kube-state-metrics | Renamed to exported_<name> at scrape with honor_labels: false, or overwrites the target’s identity with it on | No |
job · instance | Prometheus target identity; OpenTelemetry’s service.name and service.instance.id; GMP | As above; the gateway’s OTLP bridge also refuses a sample without both | No |
le · quantile | Histogram buckets and summaries | Valid only on series of those types | quantile on a gauge; see deliberately unchanged |
project_id · location | GMP’s prometheus_target resource | GMP refuses any relabeling rule that writes them | No |
tenant_id | Thanos Receive’s tenant label, stamped on every series here as default-tenant | Receive’s tenancy and any read-path enforcement built on it | No; balancerd’s tenant is a different name |
replica · prometheus_replica · receive_replica · ruler_replica · __replica__ | High-availability deduplication in Thanos Query and Mimir | Deduplication drops or merges series that are not replicas of one another | No |
prometheus | prometheus-operator’s default external label | As cluster | No |
service · host · env · version · source · device · team | Datadog’s reserved tag keys | Datadog reads them as unified service tagging and log-source facets on the OTLP path to Datadog | Yes, version as a const label on three metadata families and a variable label on two persist families; source on twelve metric! sites |
service_* · k8s_* · cloud_* · host_* · deployment_* · telemetry_* · process_* | OpenTelemetry resource attributes, as promoted to labels | Competes with the promoted attribute of the same name | No |
alertname · severity · audience · component | This stack’s alert routing; a rule’s static labels override a series’ labels | The metric’s value is silently replaced on every alert the rule raises | No |
flavor · network_component | This repository’s dependency adapters and CNI monitors | Detection of the adapter or the dataplane | No |
exported_* · __* | Prometheus itself | Collision output and internal labels respectively | No |
The list is closed by the vocabulary file and open to additions. A name added to it is added for every emitter at once, which is the reason it lives in one place.
cluster is live today#
src/compute/src/metrics.rs registers two families with const_labels: {"cluster" => "compute"}.
On the live install every other series carries cluster="default", from clusterName, and these two carry cluster="compute".
The stamp fills cluster only where it is absent, in remote-write external_labels, in the Thanos ruler’s --label, and in the Loki ruler’s alert_relabel_configs.
That rule is correct, because it lets a destination’s own externalLabels override the default.
It is also exactly why an emitted cluster wins.
The consequences depend on the destination:
| Destination | Effect |
|---|---|
| Any multi-cluster store | Both families answer “which Kubernetes cluster” with compute, for every cluster |
| This stack’s Alertmanager | group_by: [alertname, cluster, namespace] would group an alert on either family across every cluster into one notification, and PagerDuty’s dedup_key would merge them into one incident |
| Mimir with HA deduplication on | The HA tracker keys on cluster by default, so senders in different Kubernetes clusters look like replicas of one sender, and only one of them has its samples accepted |
| GMP | GMP sets cluster during collection and refuses rules that write it; how it resolves a metric label of that name is untested |
| Datadog | The Datadog chain does not stamp cluster today. The Datadog dashboards design adds the same fill-if-absent stamp, so both families would answer compute there too until they move |
No query in the registry reads either family, so moving the label costs nothing today.
The canonical name is server_name, which the CTP transport metrics in src/service/src/transport/metrics.rs already use for the same compute-or-storage distinction.
Names to avoid#
A second tier is not reserved by any one system and is still ambiguous wherever Materialize’s metrics share a store with anything else. New metrics SHOULD NOT use these names, and existing uses SHOULD move to a qualified name through the alias mechanism.
| Name | Why it is ambiguous | Qualified replacement |
|---|---|---|
name | Four meanings across Materialize families; cAdvisor’s container name; first in Loki’s service_name discovery list after service and app | cluster_name · replica_name · object_name · shard_name |
type | Object type, source type, message type and a compute dimension, on different families | object_type · source_type · sink_type · message_type |
id | cAdvisor’s cgroup path; meaningless without an entity | source_id on mz_storage_regressed_offset_known |
size | The cluster’s size or the replica’s, depending on the family; size_class is unrelated | cluster_size · replica_size |
version | Datadog-reserved; means the process’s build on some families and the writer’s build on persist’s part-version families | build_version, and a persist-chosen name for the second meaning |
source | Datadog-reserved; also a Materialize catalog noun, which it never means here | Per family: which listener on the HTTP and balancer metrics, which collector on mz_metrics_resource_usage |
instance_id | Reads as the id of Prometheus’s instance, and as a VM in cloud vocabularies (gce_instance, EC2’s InstanceId), which the provider pulls bring into the same store | cluster_id |
honeycomb | A vendor routing hint baked into emission, constant import on two persist families | Nothing; the label carries no information |
environment | Many installs already stamp it as an external label meaning production or staging | environment_id · environment_name |
The canonical vocabulary#
Five rules generate the table below.
- A label naming a Materialize entity MUST be
<entity>_<property>, where the entity is a catalog noun:cluster_id,replica_name,shard_id. - Every label on an
_infometric MUST be entity-qualified, because each one is the payload of agroup_left. - An identity label SHOULD take the name of the catalog column a reader would join it to in SQL.
- A dimension label, one that describes the measurement rather than the thing measured, MAY be unqualified unless its name is in names to avoid.
- Canonical names carry no vendor or product prefix.
The fifth rule is a choice, and the alternative was considered.
An mz_ prefix would make every identity label collision-proof by construction, and it reads naturally beside mz_* metric names.
It loses on two counts.
Materialize Cloud already stamps mz_cluster as the EKS cluster’s name, so mz_cluster_id would put a Materialize cluster one suffix away from a Kubernetes one in Cloud’s own store.
And upstream has already chosen the unprefixed spelling in its three newest pieces of work, so a prefix would mean renaming the _info family, which was designed as the join target and is the newest part of the contract.
| Canonical | Meaning | Joins to | Aliases it replaces | Emitted under this name today by |
|---|---|---|---|---|
environment_id | Materialize environment; see the environment key | Materialize.spec.environmentId; the UUID inside mz_environment_id() | materialize_cloud_organization_name as the scoping key · materialize_cloud_organization_id in Cloud | — |
environment_name | The environment’s display name, on mz_environment_info only | spec.environmentName if set, else metadata.name | materialize_cloud_organization_name as a display value | — |
cluster_id | Materialize cluster | mz_clusters.id | instance_id · compute_instance · compute_cluster_id · cluster_environmentd_materialize_cloud_cluster_id | _info family, /metrics/public |
cluster_name | Materialize cluster’s name | mz_clusters.name | name on mz_cluster_info · compute_cluster_name | /metrics/public; this repository’s mzClusterName join |
cluster_size | A managed cluster’s size | mz_clusters.size | size on mz_cluster_info | — |
replica_id | Cluster replica | mz_cluster_replicas.id | compute_replica_id · cluster_environmentd_materialize_cloud_replica_id | Controller metrics, mz_replica_info, /metrics/public |
replica_name | Replica’s name | mz_cluster_replicas.name | name on mz_replica_info · compute_replica_name | /metrics/public |
replica_size | Replica’s size | mz_cluster_replicas.size | size on mz_replica_info | — |
object_id | Catalog item | mz_objects.id | — | mz_object_info |
object_name · object_type | Catalog item’s name and type | mz_objects.name · mz_objects.type | name · type on mz_object_info | — |
collection_id | A collection’s global id | mz_internal.mz_object_global_ids.global_id | global_id on mz_object_info | Controller and wallclock-lag metrics |
source_id · sink_id · parent_source_id | Source, sink, and a subsource’s parent | mz_sources.id · mz_sinks.id | id on mz_storage_regressed_offset_known | Storage statistics |
source_type · sink_type | Their connection types | mz_sources.type · mz_sinks.type | type on mz_source_info / mz_sink_info | — |
shard_id | Persist shard | mz_internal.mz_storage_shards.shard_id | shard | Storage statistics |
shard_name | Persist shard’s name | — | name on mz_persist_* | — |
server_name | The compute or storage half of clusterd | — | cluster (reserved) | CTP transport metrics |
build_version · build_type | The emitting process’s build | mz_version() | version on the start-time and metadata families · mz_version | build_type already, everywhere version is |
worker_id · process · workload_class | Timely worker, replica process, workload class | — | — | Unchanged |
collection_id is preferred over global_id because it names an entity rather than a type, and because it is the spelling the controllers emit and the dashboards already group by.
The catalog is not consistent here either, and the alternative is recorded as an open question.
Deliberately unchanged#
| Label | Why it stays |
|---|---|
quantile on mz_dataflow_wallclock_lag_seconds | A hand-rolled summary on a gauge vector, which misuses a reserved name. Sixteen queries match on it, and the correct fix is a real histogram upstream, not a rename here. A recorded exception |
| The SQL exporter’s labels | The environmentd-sql job is slated for deletion with the Tier 2 work in the metrics contract. Aliases are added only for labels a shipped query reads, which are compute_cluster_id, compute_cluster_name and compute_replica_id |
app | A target label the gateway already derives from the pod’s app.kubernetes.io/name, app or k8s-app label |
Ids on series, names on _info#
An id is part of a series’ identity, and a name is not.
A label on a series is part of what makes it that series, so a label whose value changes ends one series and starts another, and every rate() across the change breaks.
Ids never change, so they MUST be on the series that describe their entity.
Names can change and are not unique, so a name SHOULD appear only on its entity’s _info metric, and consumers join it in.
The rule covers environments, clusters and replicas alike.
clusterd pods carry cluster-name and replica-name annotations, and Cloud’s annotation labelmap turns them into labels on every series.
The shipped monitors could copy them the same way, and this design deliberately does not.
The _info family reads the catalog on every scrape, so a name taken from it is current by construction.
An annotation is current only as of the pod’s last template change, and a renamed cluster is exactly the case where the two disagree.
/metrics/public appending cluster_name and replica_name to every federated series is an existing departure from the rule, listed under upstream work.
The environment key#
An environment is one Materialize resource: one environmentd and the clusters, balancers and consoles it runs.
| Label | Source | Requirement |
|---|---|---|
environment_id | Materialize.spec.environmentId, as the bare UUID in canonical lowercase hyphenated form | MUST be on every series an environment’s own processes emit. MUST be unique within a Kubernetes cluster. SHOULD be unique across every Materialize install. MUST NOT change for the life of the environment |
environment_name | spec.environmentName when that field exists and is set, otherwise metadata.name | SHOULD NOT change, and is not unique. SHOULD appear only on mz_environment_info. Consumers MUST NOT use it as a key |
Why spec.environmentId#
It is the only field the resource declares as the environment’s identity, and its upstream guarantees already match what a key needs.
| Property | Where it is guaranteed |
|---|---|
| Always present | orchestratord fills a nil id on first reconcile, from the license key’s environment id or from a random v4 UUID, and writes it back to the spec |
| Unique within the Kubernetes cluster | check_environment_id_conflicts lists every Materialize resource in the cluster and refuses to reconcile two with the same id |
| Immutable | The CRD’s own documentation: the value “MUST NOT be changed in an existing instance”, because persist’s storage layout depends on it |
The alternatives each fail one of those.
metadata.name is unique only within a namespace and is chosen by whoever writes the manifest.
The namespace is a deployment choice and repeats across clusters.
status.resourceId, which prefixes every pod name and reaches pods as materialize.cloud/mz-resource-id, is unique within the cluster but is derived status rather than declared spec, and nothing outside the cluster knows it.
The value is the UUID, not environmentd’s environment id#
environmentd is started with a composite --environment-id, <cloud_provider>-<region>-<uuid>-<ordinal>, which orchestratord builds with an ordinal of 0 and which mz_environment_id() returns in SQL.
Upstream’s EnvironmentId type calls the UUID component organization_id.
environment_id carries the UUID alone.
The provider and region are deployment facts with labels of their own, and the ordinal is a constant.
A consumer joining from SQL takes the UUID component of mz_environment_id().
Global uniqueness is a SHOULD, and Cloud is why#
| Where the id came from | Unique within the cluster | Unique globally |
|---|---|---|
| A license key carrying an environment id | ✅ Enforced | ✅ Unless one key is reused across installs |
| orchestratord’s random default | ✅ Enforced | ✅ |
| Set by hand in the manifest | ✅ Enforced | ⚠️ Unless a manifest was copied between clusters |
| Materialize Cloud | ✅ Enforced | ❌ Cloud uses the organization’s UUID, so it repeats in every region the organization uses |
The globally unique key is therefore (cluster, environment_id).
A store holding more than one Kubernetes cluster MUST scope by both.
That is already the stack’s behaviour, since cluster is stamped on every series, and it is one more reason cluster is reserved.
mz_environment_info#
mz_environment_info carries environment_id, environment_name and environment_namespace, the namespace the resource lives in, with a constant value of 1.
It is the only series that names an environment.
orchestratord is the right emitter, rather than environmentd, for three reasons. It already holds the resource, and every field the info metric carries is on it. It emits one series per resource, where environmentd would emit one per generation and two during every blue/green rollout. And it keeps emitting while environmentd is down, which is when a dashboard most needs to say which environment it is looking at.
The resource’s namespace is environment_namespace and not namespace, because orchestratord’s own namespace target label names the operator’s namespace and an emitted namespace would be renamed exported_namespace.
It replaces materialize_cloud_organization_namespace, which on an environment’s own series repeats the namespace target label.
Beside it, orchestratord emits mz_environment_active_generation, with the same labels.
Its value is the generation the environment serves, from status.activeGeneration.
Its generation label carries the same number, so a query can join it to the generation in a pod’s name.
kube-state-metrics publishes both from its customResourceState support, with no upstream change, by reading the Materialize resource.
It is the fallback if the orchestratord change slips, and it covers operator versions that predate it.
Its series are kube_mz_environment_info and kube_mz_environment_active_generation, with the same labels.
The kube_ prefix keeps the two producers from ever writing the same series where both run, and says which one a query reads.
Consumers read the bridge’s names until an operator release emits the upstream ones.
Every join reads the info metric through group by (cluster, environment_id, environment_name), which drops the labels particular to each producer.
A consumer that reads both producers during the overlap groups mz_environment_info or kube_mz_environment_info the same way,
which gives one series per environment.
The join is on (cluster, environment_id), never on environment_id alone, for the uniqueness reason above.
What consumers change#
| Consumer | Today | After |
|---|---|---|
%%{mzEnvironmentFilter} | materialize_cloud_organization_name=~"$environmentNameList" | environment_id=~"$environmentList" |
| The environment picker | label_values of the old label on mz_compute_commands_total, so an environment is listed only while its environmentd has reported within the dashboard’s range | mz_environment_info, with environment_id as the value and the name as the display text, the shape the cluster picker already uses. Names are not unique, so the display SHOULD include the namespace |
mzEnvironmentName in rules | A join through up on namespace | * on (cluster, environment_id) group_left (environment_name) group by (cluster, environment_id, environment_name) (mz_environment_info) |
The namespace picker and cAdvisorFilter | materialize_cloud_organization_namespace | environment_namespace from mz_environment_info for the selected environments |
| Shipped alerts | The old label passed through on most of them, written out or through mzEnvironmentName | environment_id always, and environment_name where the rule joins it |
| The tenant-scoped read path | Injects a matcher on the old label | Injects environment_id, and cluster where the store spans clusters |
| Log structured metadata | organization_name, from the same pod label | environment_id beside it, from the new pod label |
kube_pod_labels (DEP-253) | No Materialize labels | materialize.cloud/environment-id in the allowlist, so Kubernetes object metrics join to an environment |
| Datadog dashboards (DEP-115, designed in #577) | No set exists. A panel built from the registry shows ids, because an _info metric and the series it names spell the id differently, and Datadog cannot rename a tag inside a query | Names join by formula arithmetic, a * (b * 0 + 1), which matches on tag names. That needs the canonical id on both sides of each join, and cluster and environment_id on the series Datadog receives |
The transition is gated on the operator, not on a relabel#
Every other alias in this design is a fill-if-empty copy of the same value under a new name.
The environment key cannot be one, because environment_id and materialize_cloud_organization_name hold different values, and no relabeling rule can derive a UUID from a name.
Its overlap is therefore “both labels present”, and it starts when the operator puts the new pod label on the pods.
- orchestratord adds
materialize.cloud/environment-idtodefault_labels(), which reaches environmentd, balancerd and console pods directly and clusterd pods through the service labels orchestratord passes environmentd. It emitsmz_environment_infoandmz_environment_active_generation. The label changes the pod template, so it reaches an environment’s pods at its next rollout. - The shipped monitors add a target alias from that pod label to
environment_id. On an older operator the meta label is absent and the rule does nothing. - Compatibility declares the operator version that carries the label.
- Once the supported operator floor includes that version,
%%{mzEnvironmentFilter}, the picker, the namespace picker andmzEnvironmentNamemove to the new keys in one change. A**Deprecated:**note namesmaterialize_cloud_organization_name,_namespaceand_id. - After 30 days the old keys leave
podTargetLabels.
Step 4 is a render-time switch, like %%{mzSqlPrefix}.
If a fleet ever needs both keys at once, the fragment can render either, and the dashboards are built for one or the other.
The first pass: relabeling in the shipped monitors#
Where the rules live#
| Home | Reaches | Verdict |
|---|---|---|
The shipped monitors (relabelings, metricRelabelings) | Every consumer of the Prometheus Scrapers component: the bundled gateway, a customer’s prometheus-operator, the GMP flavor, the classic scrape config | Chosen |
The gateway’s otelcol.processor.filter "inputMetricProcessor", which the pipelines page names as the home for “dashboard-contract normalization” | The bundled path only | Rejected. A customer scraping with their own Prometheus would see a different vocabulary from the one the shipped dashboards are written against |
| Recording rules | Whatever reads the recorded name | Rejected. A rename needs no arithmetic, and a recorded copy doubles the series. The external-dependency design chose recording rules because its mapping needed ratios and unit conversion; this one does not |
label_replace at query time | One query | Today’s answer. It puts the cost on every consumer, which is the cost being removed |
The per-target placement has one cost: a rule appears in every monitor. Every alias renders on every Materialize monitor, whether or not that monitor’s process emits it. A rule whose alias is absent does nothing, and the rule keeps working if Materialize moves a metric from one process to another. Generating the rules from one file makes the replication free, and keeps a cross-cutting rule from becoming the one monitor that forgot it.
Four rule forms#
Every rule the vocabulary generates takes one of four forms.
| Form | Applies to | Rendered as |
|---|---|---|
| Target alias | A pod label renamed into a canonical target label: clusterd’s cluster and replica ids | A relabelings replace from __meta_kubernetes_pod_label_* (operator, classic); a targetLabels.fromPod entry with to (GMP) |
| Metric alias | An emitted label copied to its canonical name: instance_id, shard, name on a named family | A metricRelabelings fill-if-empty replace |
| Reserved move | An emitted reserved label: cluster | A metric alias to the canonical name, then a labeldrop of the reserved name |
| Drop | A constant label carrying no information: honeycomb | A labeldrop |
A metric alias MUST be fill-if-empty, and it MUST NOT overwrite a canonical label that is already present:
# Fill cluster_id from instance_id only where cluster_id is empty and instance_id is not.
# Relabel regexes are fully anchored, so "u1;u1" and ";" both fail to match.
- sourceLabels: [cluster_id, instance_id]
separator: ";"
regex: ";(.+)"
targetLabel: cluster_id
replacement: "$1"
action: replaceThat form is what makes the second pass invisible.
Once upstream emits cluster_id, the joined value is u1;u1 or u1;, the rule matches nothing, and the series is unchanged.
A plain replace would also produce the right value today, and would silently overwrite upstream’s value tomorrow if the two ever disagreed.
An alias scoped to some families adds __name__ to the source labels:
# `name` on mz_cluster_info is the cluster's name. Elsewhere it means something else.
- sourceLabels: [__name__, cluster_name, name]
separator: ";"
regex: "mz_cluster_info;;(.+)"
targetLabel: cluster_name
replacement: "$1"
action: replaceThe target alias reads a meta label, which exists only during target relabeling:
- sourceLabels: [__meta_kubernetes_pod_label_cluster_environmentd_materialize_cloud_cluster_id]
regex: "(.+)"
targetLabel: cluster_id
replacement: "$1"
action: replaceA copy of a label into another label with the same value cannot create a series, since the set of distinct label sets is unchanged. It adds index entries and bytes per series, which is the whole of the cost.
One vocabulary file#
The vocabulary lives in packages/prometheus-scrapers/labels.yaml, owned by the Prometheus Scrapers component, because that is the component whose output changes.
Its schema is packages/mzmon-lib/schemas/scrape/mzmon-labels.schema.yaml.
The file holds only what something reads, in the following shape:
monitors: [environmentd, clusterd, materialize-operator]
canonical:
- name: cluster_id
values: "[su][0-9]+"
aliases:
- label: instance_id
deprecatedIn: "1.6.0"
- label: cluster_environmentd_materialize_cloud_cluster_id
deprecatedIn: "1.6.0"
- name: cluster_name
aliases:
- label: name
families: [mz_cluster_info]
deprecatedIn: "1.6.0"gen-scrape-configs renders every alias into every endpoint of the monitors that monitors names, in every flavor.
The docsite renders the list of canonical labels from it.
The following fields wait for their first reader:
meaningandjoinsTo, for the customer-facing label contract in DEP-356.reservedanddrop, until more than clusterd’sclustermove andhoneycombdrop in DEP-347 need rendering.fromPodLabel, with target-alias rendering in DEP-349.
The query-registry check in DEP-356 reads the same file to reject a query matching on a retired alias or on a reserved label. The upstream lint in DEP-352 takes a published copy rather than reading this repository.
The three flavors#
| Flavor | Target alias | Metric rules | Gap today |
|---|---|---|---|
| prometheus-operator (deployed by the chart) | relabelings | metricRelabelings | None; the flavor is a verbatim passthrough |
| Classic scrape config | relabel_configs | metric_relabel_configs | None beyond the existing FIXME_target_port placeholder |
GMP PodMonitoring | targetLabels.fromPod[].to | metricRelabeling | Target relabelings are dropped silently; GMP refuses any rule writing project_id, location, cluster, namespace, job, instance or __address__, and refuses labelmap outright |
The GMP flavor MUST fail the render on a rule it cannot express, rather than dropping it.
A rule dropped silently is a dashboard that is empty on one flavor and fine on the others, which no render test can see.
Whether GMP accepts a labeldrop of cluster is untested.
If it does not, the GMP flavor cannot correct the cluster collision at scrape, and the upstream fix is the only one available to it, which is one more reason to land that fix first.
The overlap, and what retires it#
A metric label rename is graded as visible and non-paging by the deprecation policy, and the policy asks for a dual-publish shim where this repository’s layer allows one. The alias rules are that shim.
| Step | What changes | What a consumer sees |
|---|---|---|
| 1. Reserved moves and drops | cluster becomes server_name; honeycomb goes | Two families gain their Kubernetes-cluster identity. Nothing reads them |
| 2. Aliases ship | Every alias rule is added; both names present on every series | Nothing breaks. A **Deprecated:** release note names each alias and its replacement |
| 3. This repository migrates | Registry queries, dashboard selectors, variables and the mzClusterName / mzObjectName helpers move to canonical names | Dashboards unchanged in behaviour; the label_replace in each name join disappears |
| 4. Overlap holds | At least 30 days, measured from the release note | Customers move their own queries |
| 5. Aliases retire | labeldrop of each alias; the long-form keys leave podTargetLabels | Queries on old names go empty, as announced. A **Removed:** note |
Step 3 also decouples the dashboards from the Materialize version.
The canonical names are produced by the monitors against every Materialize release, so a dashboard on cluster_id works on a release that emits instance_id and on one that emits cluster_id.
Today a rename upstream would need a min-mz-version bump or a second arm in every affected query.
1.0 shipped before the first pass, so the pre-1.0 budget that listed label-family harmonization is spent. The migration still fits a minor release. Upstream label names are on the coordinated surface, which asks for disclosure and for a dual-publish shim where this repository’s layer allows one, and sets no major-version requirement. The alias overlap is that shim.
Alerts keep both labels through the window#
Alert labels are the strictest surface in the policy, because a routing tree that stops matching is silent.
Of the shipped rules, platform alerts pass instance_id through three times and the long-form ids five times, and workload alerts pass them seven times.
An external Alertmanager may route on any of them.
During the overlap, a rule that aggregates by (instance_id) MUST aggregate by (cluster_id, instance_id) instead.
Both labels are present and equal on every series, so the aggregation produces the same series and the alert carries both names.
At step 5 the alias drops out of the by clause with the rest.
The promtool tests that assert by (…) (ALERTS) on the old names change in the same pull request, which is the check that no alert lost a label early.
The second pass: adoption upstream#
The order keeps it invisible#
The second pass is a sequence, and the order matters more than the content.
- The lint lands first. It refuses reserved names outright and flags a non-canonical spelling of a canonical concept on any new metric, with the existing ones on an exception list that only shrinks.
- The federated endpoint stops appending blindly.
add_replica_labelsinsrc/environmentd/src/http/metrics_public.rspushescluster_id,replica_id,process,cluster_nameandreplica_namewithout checking for an existing label of the same name. A series carrying two values for one label name is invalid exposition. - Reserved names go. The two
clusterconst labels becomeserver_name, andhoneycombis removed. These need no coordination, since the monitors already move them. - Renames land, one family at a time, after the matching alias has shipped here. Upstream MAY rename at any point after step 2 of the overlap. The fill-if-empty rule absorbs the change, and the release note here has already told consumers the new name.
- The exception list empties. At that point every alias rule in the monitors is a no-op, and the next minor removes them.
The environment key sits outside this sequence.
orchestratord’s pod label and mz_environment_info add names and rename nothing, so they can land first and in parallel, and the environment transition is gated on the operator version rather than on an alias.
The constraint on step 4 is one-directional. Upstream renaming early is safe, because the alias covers it. Upstream renaming a family that has no alias here yet is the one ordering that produces a visible break, and the lint’s exception list is what makes that ordering visible before it merges.
The hook already exists#
src/metrics-catalog/src/main.rs, run as bin/gen-metrics-catalog, parses every metric! invocation with syn and writes each family’s label keys into doc/user/data/metrics.yml.
ci/test/lint-metrics-catalog.sh fails CI when that file is stale.
A label lint is therefore a pass over a file that CI already guarantees is current, and needs no new extraction.
The lint reads the reserved and canonical sections of labels.yaml.
Where that file lives, and which repository is its source of truth, is an open question.
Two label sources sit outside metric! and need the check separately:
| Source | Where | What it needs |
|---|---|---|
| Registry postprocessors | register_postprocessor in src/ore/src/metrics.rs; workload_class is appended this way in both environmentd and clusterd | The same reserved check, at registration |
| User-defined labels in a SQL metric sink | src/compute/src/sink/metric_sink.rs, which checks only the Prometheus grammar | A reserved-name refusal. A user who names a label cluster produces the same collision as the compute const label |
What stays in the pipeline#
Some labels never move upstream, and the second pass is done when everything else has.
| Label | Why it is the pipeline’s |
|---|---|
cluster, namespace, pod, container, job, instance | Process identity; differs by deployment, and owned by the scraper and the store |
environment_id on an environment’s own series | Environment identity for every process in it; a pod label orchestratord sets from the resource, copied by the monitors. orchestratord’s own series MAY carry it as a dimension, since there it names the thing measured |
cluster_id and replica_id on clusterd’s own series | Process identity for a clusterd process. If clusterd emitted them, a monitor’s target label of the same name would rename clusterd’s copy to exported_cluster_id, and /metrics/public would append a duplicate |
The last row is the subtle one.
cluster_id is canonical everywhere, and the second pass makes environmentd emit it on the families that describe clusters from outside.
clusterd describes itself, and its identity arrives the way every other process’s does.
Materialize Cloud#
Cloud’s scrape configuration (internal) reaches the long forms through labelmap and adds three external labels of its own: mz_cluster (the EKS cluster), mz_aws_region and mz_cloud_stack_name.
Its alerting reads cluster_environmentd_materialize_cloud_cluster_id and mz_cluster directly.
Cloud sets spec.environmentId to the organization’s UUID, and names each resource environment-<organization-id>-<ordinal>.
Cloud’s usage pipeline (internal) calls that resource name the environment_id and calls spec.environmentId the organization_id, which is the reverse of this vocabulary.
Cloud also has a design (internal) proposing to collapse its organization and environment labels into one environment_id through relabeling, without fixing which of the two values it carries.
This doc fixes it: environment_id is the resource’s spec.environmentId, in Cloud as everywhere, and the globally unique key is (cluster, environment_id) because Cloud repeats the UUID across regions.
Cloud converges by adopting the shipped monitors, which is already the Cloud adoption direction, and gets the aliases and the overlap with them.
Two decisions are Cloud’s alone.
The first is mz_cluster against cluster: this stack stamps cluster, the ecosystem expects cluster, and Cloud’s alerting would move with the adoption.
The second is whether Cloud sets spec.environmentName to the name a customer sees, since its metadata.name is an internal identifier and environment_name would otherwise show it.
Upstream work#
Work in MaterializeInc/materialize, in the order above.
Tracked in the Metric label standardization milestone of the CLOM project, with the upstream review in SQL-754.
The metrics owners are the SQL team’s Improve Prometheus Metrics project, with persist’s families coordinated with the persist team.
| Item | Why | Order |
|---|---|---|
orchestratord adds materialize.cloud/environment-id to default_labels() (DEP-348) | The only route for environment_id onto every series; everything environment-scoped waits on it | 1, additive |
orchestratord emits mz_environment_info{environment_id, environment_name, environment_namespace}, one series per resource (DEP-348) | The environment’s name, available while environmentd is down | 1, additive |
orchestratord emits mz_environment_active_generation with the same labels and generation, valued status.activeGeneration (DEP-348) | Which generation serves, which a rollout’s pod names cannot say | 1, additive |
Label lint over doc/user/data/metrics.yml, reading the vocabulary (DEP-352) | Stops the vocabulary eroding one metric at a time | 1 |
| Reserved check on postprocessor labels and on metric-sink user labels (DEP-352) | Two label sources the lint cannot see | 1 |
add_replica_labels replaces rather than appends (DEP-353) | A latent duplicate label name the day any clusterd series carries cluster_id | 2 |
cluster → server_name on the two compute families (DEP-359) | Reserved collision | 3 |
Remove honeycomb from the persist failure counters (DEP-359) | A vendor hint with one constant value | 3 |
instance_id and compute_instance → cluster_id (DEP-357) | The largest family of aliases | 4, after the alias ships here |
_info payload labels entity-qualified: cluster_name, replica_name, object_name, object_type, source_type, sink_type, cluster_size, replica_size (DEP-358) | Two info joins in one expression | 4 |
global_id → collection_id on mz_object_info (DEP-358) | One name for a collection’s id | 4 |
shard → shard_id, persist name → shard_name (DEP-362) | One name for a shard across persist and storage; coordinated with persist’s cardinality work so the two land as one change per family | 4 |
version → build_version on the start-time and metadata families (DEP-362) | Datadog-reserved; one name across components | 4 |
source qualified per family (DEP-362) | Datadog-reserved and a catalog noun | 4, with each owning team |
mz_dataflow_wallclock_lag_seconds as a real histogram or summary (DEP-364) | Retires the quantile exception | Independent |
An optional, mutable spec.environmentName on the Materialize resource, read by mz_environment_info in preference to metadata.name (DEP-363) | A display name a person can change without recreating the resource | Independent; a CRD change |
/metrics/public stops appending cluster_name and replica_name to every federated series (DEP-353) | Ids on series, names on _info; a cluster rename splits every series it touches | After the info joins are the norm |
orchestratord’s per-resource reconciliation metrics carry environment_id (DEP-365) | env-upgrade’s Reconciliation tab could then scope to one environment | Could; a change to the shared k8s-controller crate |
Chart-side prerequisites#
Work in this repo. Ordered roughly by dependency. Tracked in the same milestone, gated on design acceptance in DEP-346; DEP-207 is the umbrella.
| Item | Why it is needed | Blocking? |
|---|---|---|
labels.yaml and its schema under packages/mzmon-lib/schemas/, with the aliases and their deprecation versions (DEP-421) | The single source every other row generates from | ✅ Done |
The cluster move and the honeycomb drop on the clusterd monitor (DEP-347) | A live defect with no consumer to migrate. Can land by hand ahead of the generator | Blocking, and first |
Metric-alias rendering into metricRelabelings on every Materialize monitor, in every flavor (DEP-421) | The aliases themselves. clusterd’s ids reach cluster_id and replica_id from the long-form target labels, which works in every flavor | ✅ Done |
| Target-alias rendering in the transpiler, per flavor (DEP-349) | podTargetLabels cannot rename, so once the long-form keys leave it, clusterd’s ids reach cluster_id only through a target alias | Blocking for retiring the long-form ids |
Reserved-move and drop rendering from labels.yaml (DEP-349) | Generates the reserved moves and drops once there is more than DEP-347’s one of each | Not blocking |
| GMP render refuses what it cannot express — dropped target relabelings and rules on protected labels (DEP-350) | A silently dropped rule is a flavor-specific empty dashboard | Blocking for the GMP flavor |
Environment target alias from materialize.cloud/environment-id to environment_id on every Materialize monitor (DEP-351) | Puts the key on every series once the operator sets the pod label; a no-op before | Blocking for the environment key |
Environment switch: %%{mzEnvironmentFilter}, the environment and namespace pickers on mz_environment_info, and mzEnvironmentName through the info join (DEP-354) | Step 4 of the environment transition | Blocking for retiring materialize_cloud_organization_*; waits on the operator floor |
| Compatibility declaration of the operator version that sets the pod label (DEP-351) | The switch is safe only above it | Blocking for the switch |
kube_mz_environment_info bridge through kube-state-metrics customResourceState, with list and watch on materializes, and kube_mz_environment_active_generation beside it (DEP-361) | Covers operators that predate the upstream info metric, and says which generation each environment serves | ✅ Done |
Agent writes environment_id as log structured metadata from the same pod label (DEP-351) | One environment key across both engines | Should |
Registry migration: queries, selectors in env_top/selector.rs, variables, and the mzClusterName / mzObjectName helpers onto canonical names (DEP-355) | Step 3 of the overlap | Blocking for retiring aliases |
| Alert rules aggregate by both names through the window, with the promtool tests updated (DEP-355) | No alert loses a label inside the window | Blocking for the registry migration |
| Registry check rejecting a reserved label as a matcher on a Materialize metric, and a retired alias anywhere (DEP-356) | Keeps the registry on the vocabulary | Should |
Generated label-contract page from labels.yaml, merged with the alerting design’s owed label contract (DEP-356) | One contract for routing and for queries | Should |
kube_pod_labels adopts the canonical names when DEP-253 lands, with materialize.cloud/environment-id in its allowlist and a metric alias on the kube-state-metrics monitor | Otherwise its join keys are label_cluster_environmentd_materialize_cloud_cluster_id and label_materialize_cloud_organization_name, two more spellings | Should, with DEP-253 |
| E2E assertions below | The failure mode is a rule that renders and does nothing | Should |
Alias retirement after 30 days: labeldrop and the allowlist trimmed (DEP-360) | Step 5 | Not blocking |
Testing#
The render tests prove a rule exists, and the important assertions are about what it does to real series.
- Rule semantics, table-driven. Every generated rule is applied to sample label sets: old name only, canonical only, both equal, neither. The canonical-only and both-equal cases MUST come out unchanged, which is the property that keeps the second pass invisible.
- No cardinality change. Against a live tier, the series count per Materialize job is equal before and after the aliases ship, within scrape noise. A 1:1 copy adds none, and a test that asserts it catches a rule that is accidentally not 1:1.
- Kubernetes-cluster identity everywhere.
count({job=~".*materialize.*", cluster!="<clusterName>"})is zero. This is the assertion theclustercollision fails today. - Every alias has its canonical partner. For each alias,
count({<alias>!=""} unless on (…) {<canonical>!=""})is zero. - Reserved names absent. No Materialize series carries a reserved label, read from
labels.yamlrather than restated in the test. - GMP admission. The rendered
PodMonitoringresources are applied to a GKE cluster with managed collection on, because GMP’s admission webhook is the only authority on which rules it accepts. Theclusterdrop is the case to watch. - Flavor parity. For one Materialize release, the bundled path and a plain prometheus-operator Prometheus fed the same monitors produce the same label names on the same families.
- Alert label sets. The promtool tests’
by (…) (ALERTS)assertions carry both names through the window and only the canonical name after. - Join correctness. A query joining
mz_cluster_infoandmz_replica_infoin one expression returns bothcluster_nameandreplica_name, which fails today. - Environment key everywhere. On an operator that sets the pod label, every series from environmentd, clusterd, balancerd and console carries
environment_id, and its value equals the resource’sspec.environmentId. - One info series per environment. With orchestratord and the bridge both running,
mz_environment_info or kube_mz_environment_infogrouped by(cluster, environment_id, environment_name)has one series for every environment, so the two producers agree on its name. - A rename splits nothing. Changing
spec.environmentNamechangesmz_environment_infoand no other series’ label set. - The name survives an outage. With environmentd scaled to zero, the environment picker still lists the environment by name.
- Upstream lint. A test fixture in
MaterializeInc/materializeregisters a metric with a reserved label and asserts the lint refuses it.
Documentation to update#
- A customer-facing label contract page, generated from
labels.yaml: every canonical label, its meaning and join target, every alias with its retirement date, and the reserved list. It absorbs the alerting design’s owed label contract, and it is the artifact a customer writes queries and routes against. reference/development/dashboard/style-guidelines.md— the label-family section becomes a pointer to the contract page once the aliases ship, and the “query author writes the label” paragraph goes with it.reference/development/pipelines/metrics.md— the three-phase table namesinputMetricProcessoras the home for dashboard-contract normalization. It moves to the monitors, and the table should say why.reference/stability.md— the canonical vocabulary becomes a public interface (“documented metric labels”), while raw upstream labels stay unstable. That is the central claim, written as policy.reference/development/versioning.md— the same surface, in the policy of record.reference/development/queries/alerts.md— its scoping guidance names the long-form label and a query-timelabel_replacefrompod.packages/prometheus-scrapers/README.md— the allowlist rationale gains the aliases and the vocabulary file.metrics/collecting/— the GMP page states which rules the GMP flavor cannot express, once that is measured.reference/development/design-docs/20260916-tenant-query-api.md— its What exists today rows namematerialize_cloud_organization_nameandorganization_nameas the environment labels the proxy enforces on. The key becomesenvironment_id, withclusterwhere a store spans clusters.logs-and-events/— the structured-metadata reference gainsenvironment_id.compatibility.md— the operator version that setsmaterialize.cloud/environment-id.reference/development/roadmap.md— ✅ done. Its Metric label standardization section and its Designs in flight row link here.
Open questions#
- Which repository owns
labels.yaml? The DEP-109 position was that the source-of-truth contract lives with the producer and this repository records what it consumes. The reserved list is this stack’s concern and the aliases are this repository’s mechanism, which argues for here. The canonical names are upstream’s to emit, which argues for there. A split, with upstream owningcanonicaland this repository owningreservedandaliases, is the likely answer. - Is
cluster_idsafe enough? It is not reserved by anything verified, and it sits next toclusterin every store. Multi-cluster cost tooling keys on a configurable cluster label whose documented examples includecluster_id. This design accepts that residual risk in exchange for upstream’s existing spelling, and the contract page should name it. -
collection_idorglobal_id? The catalog usesglobal_idfor the column and the controllers usecollection_idfor the label. This doc picks the controllers’ name; the persist and compute owners may see it differently. - Who owns
spec.environmentName, and is it validated? It is a CRD change upstream. A display name wants a length bound and nothing else, since uniqueness is explicitly not promised. - When does the operator floor move? The environment switch waits on every supported operator setting the pod label, and the support window is set in Compatibility, not here. If that is long, the render-time switch is the way to ship both keys meanwhile.
- Does the tenant claim become
environment_id? The tenant-scoped read path left the claim’s shape open pending control-plane identity. The label it enforces on is now settled; whether the claim carries the same UUID, or something the proxy maps to it, is still that design’s question. - Should SQL expose the bare UUID?
mz_environment_id()returns the composite, so joining a metric to SQL means parsing it. A function or column returningspec.environmentIdalone would make the join direct. - Should
server_nameexist at all on the two compute families? The value is the constantcomputeon families only compute registers, so the label carries no information. Moving it is safe and dropping it is simpler; dropping is safe only while no storage-side family registers the same name. - Does the vocabulary extend to label values?
status="ok",status="success"andoutcome="success"coexist. A value vocabulary for the handful of dimensions alerts read would make thresholds portable across families, and it is a larger change than this one. - Do the other log structured-metadata names follow? The environment key is settled for both engines. Whether
cluster_idandreplica_idreplace the log pipeline’s own spellings is the one-picker-across-two-engines problem with a vocabulary behind it, and Loki stream labels move only under the deprecation cycle. - Should queries select Materialize components by
apprather than byjobvalue?jobis reserved and its value encodes the monitor’s namespace and name, which a customer’s own install will not match. Several shipped queries and alerts match on it. - Does Cloud keep
mz_cluster? Cloud’s alerting reads it, and this stack stampscluster. Adoption is the natural moment to converge. - Does GMP accept a
labeldropofcluster? If not, GMP consumers wait for the upstream rename, and the GMP flavor’s documentation says so.