Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5dca9dc
Add the MetricMapping and TelemetryDestination kinds
dennis-upbound Sep 28, 2026
f13387d
Report whether a mapping and a destination actually work
dennis-upbound Sep 28, 2026
7cea2bd
Regenerate schemas for the telemetry kinds
dennis-upbound Sep 28, 2026
6ed1904
Add the telemetry guide
dennis-upbound Sep 28, 2026
0946764
Compose the OpenTelemetry collector
dennis-upbound Sep 28, 2026
6eaf26f
Narrow the telemetry API to what's in use
dennis-upbound Sep 30, 2026
8ada9b6
Run a value rewrite in the context that can reach the value
dennis-upbound Sep 30, 2026
8c75825
Convert framebuffer memory to the unit its name claims
dennis-upbound Sep 30, 2026
a4cc790
Describe the metrics instead of the collector's transform language
dennis-upbound Sep 30, 2026
82a582e
Hold a metric name to the characters a metric name can have
dennis-upbound Sep 30, 2026
de140e3
Regenerate the schema lock after rebasing
dennis-upbound Sep 30, 2026
9912a90
Type the parts of a sink that belong to Modelplane
dennis-upbound Sep 30, 2026
8fc7fca
Let a sink address its destination the way its exporter does
dennis-upbound Sep 30, 2026
d878697
Scrape the endpoints these components actually publish
dennis-upbound Sep 30, 2026
aa50066
Label serving pods with what their metrics belong to
dennis-upbound Sep 30, 2026
2350058
Name the cluster a series belongs to after the one an operator named
dennis-upbound Sep 30, 2026
4f01123
Assert the fleet's telemetry in the local end-to-end test
dennis-upbound Sep 30, 2026
2d7265c
Scrape the GPU exporter the cluster came with
dennis-upbound Sep 30, 2026
9c006ec
Keep a series' identity when the backend flattens it
dennis-upbound Sep 30, 2026
dced0f8
Carry only the identity a series is attributed to
dennis-upbound Oct 1, 2026
1481878
Tell a deployment's replicas apart
dennis-upbound Oct 1, 2026
5400e3d
Fold several metrics into one, and take a histogram's count
dennis-upbound Oct 1, 2026
0075504
Resolve a cluster gateway's name where kube-dns is the resolver
dennis-upbound Oct 1, 2026
96b9c80
Set the required aggregation in the metric-mapping fixture
dennis-upbound Oct 1, 2026
4a35660
Regenerate the schema lock after rebasing
dennis-upbound Oct 1, 2026
2397158
Name the processor that lifts the identity for what it does
dennis-upbound Oct 1, 2026
505835a
Keep every series unique to whatever produced it
dennis-upbound Oct 1, 2026
47bf569
Say what a series actually carries
dennis-upbound Oct 1, 2026
313006e
Let a component say it counts in percent
dennis-upbound Oct 1, 2026
f93cbcd
Compile a mapping's unit conversion to scale_metric
dennis-upbound Oct 2, 2026
ef3313e
Scrape the decode engine and the endpoint picker
dennis-upbound Oct 2, 2026
9a368aa
Stop the collector gating the serving stack's readiness
dennis-upbound Oct 2, 2026
ea13a08
Copy a sink's credential to the clusters that mount it
dennis-upbound Oct 2, 2026
a30b21a
Build the collector's config from every TelemetryDestination
dennis-upbound Oct 2, 2026
9e6c9c7
Stop the composed Endpoints being mirrored into a second slice
dennis-upbound Oct 2, 2026
8d00dc8
Correct what the telemetry APIs and guide claim
dennis-upbound Oct 2, 2026
96b22dc
Map only the SGLang metrics SGLang actually publishes
dennis-upbound Oct 2, 2026
0fcb57d
Skip a telemetry object the current schema rejects
dennis-upbound Oct 2, 2026
2b7ce70
Leans down guide, clarifying some sections, adds examples
tr0njavolta Oct 2, 2026
f0355d0
Merge branch 'dennis/metrics-impl' into metrics-edits-docs
tr0njavolta Oct 2, 2026
313bc9c
Merge pull request #1 from tr0njavolta/metrics-edits-docs
dennis-upbound Oct 2, 2026
56c8040
Correct what the telemetry guide claims after the edit pass
dennis-upbound Oct 2, 2026
cc09442
Answer the rest of the guide's review comments
dennis-upbound Oct 2, 2026
191b0bf
Drop acrossReplicas from MetricMapping
dennis-upbound Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apis/metricmappings/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: metricmappings.modelplane.ai
spec:
compositeTypeRef:
apiVersion: modelplane.ai/v1alpha1
kind: MetricMapping
mode: Pipeline
pipeline:
- functionRef:
name: modelplane-modelplanecompose-metric-mapping
Comment thread
haarchri marked this conversation as resolved.
step: compose-metric-mapping
179 changes: 179 additions & 0 deletions apis/metricmappings/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: metricmappings.modelplane.ai
spec:
group: modelplane.ai
names:
categories: [crossplane, modelplane, platform]
kind: MetricMapping
plural: metricmappings
shortNames: [mm]
scope: Cluster
versions:
- name: v1alpha1
served: true
referenceable: true
additionalPrinterColumns:
- name: CLUSTERS
type: integer
jsonPath: .status.clusters
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [metrics]
description: >-
How one component's metrics become part of the modelplane_*
surface. Modelplane renders every MetricMapping into each
inference cluster's collector, so a mapping is written once on
the control plane and reaches the whole fleet.

A mapping naming a component Modelplane already provides
renames for is additive: its renames run after the built-in
ones, on whatever those left behind. A metric a built-in
already renamed no longer answers to the name it was emitted
under, so a second mapping selecting on that name matches
nothing and the built-in stands. Select on the `modelplane_`
name instead to rename one of Modelplane's own.
properties:
metrics:
type: array
minItems: 1
maxItems: 128
description: >-
The metrics to rename. Give two components' metrics the same
name only if they measure the same thing, and histograms only
if their buckets match too. A quantile across mismatched
buckets is wrong.
items:
type: object
required: [from, to]
properties:
from:
type: string
maxLength: 255
pattern: '^[a-zA-Z_:][a-zA-Z0-9_:]*$'
description: >-
The metric's name as the component exposes it, matched
exactly wherever it appears in the fleet.

A histogram is named by its base name, without the
_count, _sum or _bucket a Prometheus query would use:
the collector holds it as one metric, and `part` is
what reaches into it.
to:
type: string
maxLength: 255
pattern: '^modelplane_[a-z0-9_]*[a-z0-9]$'
description: >-
The name to export the metric under. Only modelplane_*
metrics leave a cluster, so a metric no mapping renames
never leaves its cluster.

Name it in base units - seconds, bytes, joules, a ratio
from nought to one - because that is what `fromUnit`
converts to.
part:
type: string
enum: [Count, Sum]
description: >-
Take a part of a histogram as a counter of its own,
rather than the histogram itself. Count is how many
observations it holds, which is a request count where
the histogram measures request duration. Sum is their
total.

The extraction leaves the histogram alone, but the
collector exports only what a mapping renames, so the
histogram itself is dropped unless another mapping
gives it a `modelplane_` name of its own. Write that
second mapping to keep both.
labels:
type: array
maxItems: 16
description: >-
Labels to set on this metric's series. To fold several
metrics into one name, give each its own entry with
the same `to` and a different fixed value, such as
direction: input and direction: output on
modelplane_tokens_total.
items:
type: object
required: [name]
x-kubernetes-validations:
- rule: "has(self.value) != has(self.from)"
message: set either value, for a fixed label, or from, to carry one the component already emits.
- rule: "!has(self.values) || has(self.from)"
message: values remaps what from carries, so it needs from. A fixed value has nothing to remap.
properties:
name:
type: string
maxLength: 63
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$'
description: The label to set.
value:
type: string
maxLength: 253
description: >-
A fixed value, the same on every series this
mapping produces. This is what tells two folded
metrics apart.
from:
type: string
maxLength: 63
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$'
description: >-
A label the component already emits. Modelplane
copies its value into this label and removes the
original.

Naming the label itself keeps it: that is how
`values` rewrites what a component writes without
renaming the label.
values:
type: object
maxProperties: 32
additionalProperties:
type: string
maxLength: 253
description: >-
What each of that label's values becomes, for
putting an engine's own vocabulary into
Modelplane's. A value with no entry here is left
as the component wrote it.
fromUnit:
type: string
enum: [Millijoules, Mebibytes, Milliseconds, Nanoseconds, Percent]
description: >-
What the component measures this in, when that isn't
the unit the name claims. Modelplane converts to the
base unit: millijoules and milliseconds are divided by
a thousand, nanoseconds by a billion, percent by a
hundred, and mebibytes multiplied out to bytes. A
histogram is converted whole - its sum, its bounds and
its bucket boundaries - so its quantiles come out in
the target unit too.

Percent is for a component that counts a saturation
from nought to a hundred where the name says a ratio.
Check rather than assume: vLLM publishes
kv_cache_usage_perc and the value is a fraction, so a
name is no guide.
status:
type: object
properties:
clusters:
type: integer
description: >-
How many inference clusters apply this mapping.
conditions:
type: array
items:
type: object
13 changes: 13 additions & 0 deletions apis/telemetrydestinations/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: telemetrydestinations.modelplane.ai
spec:
compositeTypeRef:
apiVersion: modelplane.ai/v1alpha1
kind: TelemetryDestination
mode: Pipeline
pipeline:
- functionRef:
name: modelplane-modelplanecompose-telemetry-destination
Comment thread
haarchri marked this conversation as resolved.
step: compose-telemetry-destination
160 changes: 160 additions & 0 deletions apis/telemetrydestinations/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: telemetrydestinations.modelplane.ai
spec:
group: modelplane.ai
names:
categories: [crossplane, modelplane, platform]
kind: TelemetryDestination
plural: telemetrydestinations
shortNames: [td]
scope: Cluster
versions:
- name: v1alpha1
served: true
referenceable: true
additionalPrinterColumns:
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [sinks]
description: >-
Where the fleet's metrics go. Modelplane runs no collectors
until a TelemetryDestination exists, and creating one turns on
collection on every inference cluster.

Several can exist. Their sinks are concatenated into one
collector configuration, so adding a backend is a new object
rather than an edit to one somebody else owns. A sink's name is
the collector's name for its exporter, so it has to be unique
across destinations.
properties:
sinks:
type: array
minItems: 1
maxItems: 16
description: >-
Where to send it. Every sink gets the whole stream, so two
sinks is two copies of the fleet's metrics, billed twice.
x-kubernetes-list-type: map
x-kubernetes-list-map-keys: [name]
items:
type: object
required: [name, type]
x-kubernetes-validations:
- rule: "!has(self.auth) || has(self.secretRef)"
message: secretRef is required when auth is set, because the credential lives in it.
properties:
name:
type: string
maxLength: 63
pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
description: >-
This sink's name, unique within the destination. It
names the collector's exporter instance, the
authenticator Modelplane composes for it, and the
directory its credential mounts at, so renaming one
restarts the collector.
type:
type: string
maxLength: 63
description: >-
The collector exporter to send with, by the name
OpenTelemetry gives it: otlphttp, otlp,
prometheus_remote_write, kafka, and every other one the
collector provides.

Not an enum: the collector already refuses to start
on a name it doesn't have, so repeating the list here
would only add a second place for it to go stale.
endpoint:
type: string
maxLength: 2048
description: >-
Where this sink writes. Leave it unset for an exporter
that doesn't take an endpoint, such as kafka or debug,
and configure it in `config` instead.

Set here, it wins: Modelplane applies it over
`config`, so a sink cannot be quietly redirected by
the configuration passed through beside it.
auth:
type: object
description: >-
Authentication Modelplane sets up for this sink, using
a credential from `secretRef`. For another scheme,
define an authenticator under spec.extensions and
reference it from `config`.

Set here, it wins: Modelplane applies it over an
`auth` block in `config`, so a sink's credential
cannot be quietly unpicked.
properties:
bearerTokenKey:
type: string
maxLength: 253
description: >-
The key in this sink's Secret holding the bearer
token. Modelplane mounts it as a file and points
the authenticator at it, so a rotated token is
picked up without restarting the collector.
config:
type: object
x-kubernetes-preserve-unknown-fields: true
description: >-
The rest of the exporter's configuration, passed
through as written: TLS, retries, queueing,
compression, headers.

Modelplane sets one default, for the exporters that
flatten a series into labels: prometheus and
prometheus_remote_write get
resource_to_telemetry_conversion, or they would
receive every series stripped of the cluster,
deployment, engine and role it belongs to. Setting it
here overrides that.
secretRef:
type: object
required: [name]
description: >-
A Secret holding this sink's credentials. Modelplane
mounts each key as a file under
/etc/modelplane/telemetry/<sink name>/ and sets it as
an environment variable for ${env:KEY} references in
`config`. All sinks share one environment, so where
two Secrets have the same key, refer to the file.
properties:
name:
type: string
maxLength: 253
description: >-
Name of the Secret, in modelplane-system on the
control plane. Modelplane copies it to every
cluster running a collector, so it doesn't have
to exist on each of them already.
extensions:
type: object
x-kubernetes-preserve-unknown-fields: true
description: >-
Collector extensions, passed through as written. Use it to
define an authenticator that a sink's `config` references.

An exporter needs a client authenticator - basicauth,
oauth2client, sigv4auth, headers_setter. The oidc extension
authenticates callers of a receiver, so it is not one of
these.
status:
type: object
properties:
conditions:
type: array
items:
type: object
8 changes: 8 additions & 0 deletions crossplane-project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ spec:
tarball:
name: compose-model-service
pathPrefix: _output/functions/compose-model-service
- source: Tarball
tarball:
name: compose-metric-mapping
Comment thread
haarchri marked this conversation as resolved.
pathPrefix: _output/functions/compose-metric-mapping
- source: Tarball
tarball:
name: compose-telemetry-destination
Comment thread
haarchri marked this conversation as resolved.
pathPrefix: _output/functions/compose-telemetry-destination
- source: Tarball
tarball:
name: compose-usages
Expand Down
Loading
Loading