Repository navigation
Complete the metric surface described in the telemetry design #476
Description
design/telemetry.md describes a surface of 50 metrics. #470 composes the collector and the renames that produce 23 of them: the engine, GPU and gateway-latency core, everything reachable by renaming one metric to one name and converting its unit.
The other 27 each need something that doesn't exist yet. Grouped by what actually blocks them, because the groups are independent and not equally sized.
Per-object state, 11 metrics
Needs resource-state-metrics on the control plane reading Modelplane's own objects, plus kube-state-metrics for container restarts and gang status. None of these come from scraping a workload; they're the state of a ModelReplica, a ModelDeployment or an InferenceCluster.
modelplane_replica_gpus,modelplane_replica_gpu{gpu_uuid},modelplane_replica_allocated_time_secondsmodelplane_replicas_desired,modelplane_replicas_ready,modelplane_replica_ready_duration_secondsmodelplane_replica_cache_hit,modelplane_cluster_gpus_allocatable,modelplane_cluster_connectedmodelplane_gang_incomplete(LWS/Grove status),modelplane_engine_restarts_total(kube-state-metrics)
This is the largest group, and modelplane_gpu_seconds_total — the metric a cost question actually needs — is derived from modelplane_replica_allocated_time_seconds, so it's here too.
Folding several series under one label, 8 metrics
Needs MetricMapping to grow the two fields left out of #470: a labels form that folds two metrics into one distinguished by a fixed value, or renames a label and remaps its value vocabulary; and a part form that takes a histogram's count as a counter.
modelplane_requests_total{status},modelplane_tokens_total{direction}— fleet gatewaymodelplane_responses_total{reason},modelplane_tool_call_parses_total{outcome}— enginemodelplane_route_requests_total{decision},modelplane_route_pd_pairings_total{status}— pickermodelplane_gpu_ecc_errors_total{type},modelplane_gpu_interconnect_errors_total{link}— DCGM, and also gated on fields that are off by default
The API shape is sketched in #470 (comment). It was left out of #470 because six of these eight depend on source metric names at the gateway, the picker and DCGM that nobody has read off a running deployment yet, and the seventh and eighth need DCGM reconfigured. modelplane_responses_total{reason} is the one implementable today, from vllm:request_success_total{finished_reason}.
First step: capture the gateway's and the picker's actual metric names from a live cluster. The API follows from those, not the other way round.
Merging replicas twice, 2 metrics
A saturation gauge needs a mean and a maximum, because capacity planning reads the mean and an alert fires on the hot replica. The collector merges replicas once today.
modelplane_kv_cache_utilization_ratio_max,modelplane_requests_waiting_max
A pipeline change in compose-serving-stack, not an API change.
Instrumentation that doesn't emit yet, 6 metrics
modelplane_replica_staging_seconds,modelplane_replica_warmup_seconds— ModelExpressmodelplane_requests_throttled_total— Envoy rate limitsmodelplane_dra_allocation_errors_total— DRA drivermodelplane_gpu_thermal_throttle_seconds_total— GPU exportermodelplane_request_kv_transfer_seconds— engine, disaggregated only
Order
Per-object state first: it's eleven metrics, it's the only group blocking cost questions, and it needs no live measurement to design. The label folding wants a cluster to read names off first. The double merge is small and can go any time.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Activity
dennis-upbound commentedon Oct 2, 2026
One thing to fold in from the #470 review, raised by @lsviben: the per-object-state group above assumes
resource-state-metricsruns on the control plane, but #470 composes a collector onto each inference cluster and none onto the control plane. So there is nothing there to scrape it, and that needs deciding before those 11 metrics can be built — either a collector on the control plane, or RSM exporting directly, or those metrics come from somewhere else entirely.Also worth recording against the engine group: SGLang publishes no per-request queue time and no preemption counters at v0.4.6, v0.4.9 or v0.5.0, so
modelplane_request_queue_secondsandmodelplane_requests_preempted_totalare vLLM-only until it does. Checked against a running engine.Say what model delivery should measure
dennis-upbound commentedon Oct 5, 2026
Research pass on this, prompted by the model delivery design (
design/modelcache-sources.md, ondennis/oci-source-poc) wanting four cache metrics that land in the per-object-state group. Three things have changed since this was written, and the approach is now a decision rather than an open question.The folding group is unblocked
Both fields shipped in v0.5.
labelsandpartare inapis/metricmappings/definition.yaml, and both are exercised on a live cluster: a mapping folding SGLang's two token counters gavemodelplane_tokens_total{direction="input"}398 and{direction="output"}607, andpart: Countover its latency histogram gavemodelplane_engine_requests_total12.The first step this issue names is also done. The gateway and the picker have been read off a running GKE cluster:
gen_ai_server_request_duration_seconds,gen_ai_server_time_to_first_token_seconds,gen_ai_server_time_per_output_token_seconds. These are on the ext-proc sidecar's port, not the Envoy admin port — 55gen_ai_*lines on :1064, zero on :19001.llm_d_epp_scheduler_e2e_duration_seconds,llm_d_epp_scheduler_attempts_total. Theinference_extension_*names are deprecated twins of the same series.So for this group what remains is DCGM's two (still gated on fields that are off by default) and writing the mappings.
The per-object-state group has a blocker this issue does not name
Every metric here reads an XR's status, and those XRs live on the control plane. Modelplane composes no collector onto the control plane.
compose_collectoris called only from the ServingStack path, and composes onto each inference cluster.So the group needs two things, not one: something to turn XR status into metrics, and somewhere for it to be scraped and exported from.
design/telemetry.md:166-177already specifies both, and neither exists.There is no control-plane-scoped XR to compose it from.
ServingStackis namespaced and per-cluster, and every function composes through a provider that targets a workload cluster. So a control-plane collector is either a new XR and function, or something the install guide tells a user to run.One piece is already reusable:
TelemetryDestinationis cluster-scoped on the control plane, its own function composes nothing, and its schema is plain exporter configuration. A control-plane collector could read the same list thatcompose_collectorreads. The only friction iscollector.py:540, which stamps a per-clusterclusterattribute that a control-plane collector would set differently or omit.Two ways to get XR status into metrics
kube-state-metrics CustomResourceStateMetrics (CRS). The decisive property:
metricNamePrefixreplaces the whole default prefix, sometricNamePrefix: modelplanewithname: replicas_readyemitsmodelplane_replicas_readydirectly. That passesfilter/modelplanewith no mapping at all. It reads nested paths, emits one series per array element or map key, selects conditions by[type=Ready], coerces RFC3339 timestamps to epoch seconds and quantities like512Gi, and has aStateSettype that fans an enum out to one 0/1 series per value — which fitsModelCache.status.clusters[].phaseexactly.Limits: Gauge, StateSet and Info only, no expressions, and no count-items-matching-a-predicate. You emit per-item 0/1 gauges and sum in PromQL. It needs explicit RBAC per custom resource, and a second ksm on the control plane, since the one in the serving stack runs on workload clusters.
resource-state-metrics (RSM), which is what
design/telemetry.mdalready assumes. crossplane-contrib/resource-state-metrics packages it as a Crossplane Configuration (v0.7.0, pre-GA, 13 stars). NamespacedResourceMetricsMonitorresources with CEL or Starlark resolvers, so predicate counts, timestamp arithmetic and gang completeness are expressible where CRS cannot reach them.Its catch is the mirror image of CRS's advantage: the prefix
kube_customresource_is hardcoded ininternal/family.go:44with no override. So RSM output needs a rename beforefilter/modelplane. That is cheap here, becausetransform/modelplaneruns beforefilter/modelplaneand aMetricMappingper metric is exactly the mechanism #470 built.Crossplane core gives nothing usable. Its metrics are function-run and controller counters;
crossplane_managed_resource_{exists,ready,synced}covers managed resources, not composites.Suggested shape
modelplane_*natively and is upstream-stable. Keep RSM for the ones needing an expression, and pay oneMetricMappingrename for those.modelplane_engine_restarts_totalis plain kube-state-metrics (kube_pod_container_status_restarts_total) on the workload cluster, not CRS, and it is the metric #498 wants for a cause that outlives the pod.Cannot be settled without a cluster: whether LWS and Grove expose enough status for
gang_incompletewithout CEL, and whether the DRA driver publishes a parent GPU UUID formodelplane_replica_gpu.Cache metrics to add to this issue
That design, in section 6.2, adds four, all in this group and blocked by it:
modelplane_cache_cluster_ready— a gauge for each cache and cluster, with the phase as a labelmodelplane_cache_ready_seconds— created until readablemodelplane_cache_staged_bytesandmodelplane_cache_size_bytes— progress#494 spent 40 minutes in one phase with no series to show it, and #495 had to read staging progress from a file listing. The
StateSettype covers the first of these directly.