-
Notifications
You must be signed in to change notification settings - Fork 28
Collect metrics from every inference cluster #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dennis-upbound
merged 44 commits into
modelplaneai:main
from
dennis-upbound:dennis/metrics-impl
Oct 2, 2026
Merged
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 f13387d
Report whether a mapping and a destination actually work
dennis-upbound 7cea2bd
Regenerate schemas for the telemetry kinds
dennis-upbound 6ed1904
Add the telemetry guide
dennis-upbound 0946764
Compose the OpenTelemetry collector
dennis-upbound 6eaf26f
Narrow the telemetry API to what's in use
dennis-upbound 8ada9b6
Run a value rewrite in the context that can reach the value
dennis-upbound 8c75825
Convert framebuffer memory to the unit its name claims
dennis-upbound a4cc790
Describe the metrics instead of the collector's transform language
dennis-upbound 82a582e
Hold a metric name to the characters a metric name can have
dennis-upbound de140e3
Regenerate the schema lock after rebasing
dennis-upbound 9912a90
Type the parts of a sink that belong to Modelplane
dennis-upbound 8fc7fca
Let a sink address its destination the way its exporter does
dennis-upbound d878697
Scrape the endpoints these components actually publish
dennis-upbound aa50066
Label serving pods with what their metrics belong to
dennis-upbound 2350058
Name the cluster a series belongs to after the one an operator named
dennis-upbound 4f01123
Assert the fleet's telemetry in the local end-to-end test
dennis-upbound 2d7265c
Scrape the GPU exporter the cluster came with
dennis-upbound 9c006ec
Keep a series' identity when the backend flattens it
dennis-upbound dced0f8
Carry only the identity a series is attributed to
dennis-upbound 1481878
Tell a deployment's replicas apart
dennis-upbound 5400e3d
Fold several metrics into one, and take a histogram's count
dennis-upbound 0075504
Resolve a cluster gateway's name where kube-dns is the resolver
dennis-upbound 96b9c80
Set the required aggregation in the metric-mapping fixture
dennis-upbound 4a35660
Regenerate the schema lock after rebasing
dennis-upbound 2397158
Name the processor that lifts the identity for what it does
dennis-upbound 505835a
Keep every series unique to whatever produced it
dennis-upbound 47bf569
Say what a series actually carries
dennis-upbound 313006e
Let a component say it counts in percent
dennis-upbound f93cbcd
Compile a mapping's unit conversion to scale_metric
dennis-upbound ef3313e
Scrape the decode engine and the endpoint picker
dennis-upbound 9a368aa
Stop the collector gating the serving stack's readiness
dennis-upbound ea13a08
Copy a sink's credential to the clusters that mount it
dennis-upbound a30b21a
Build the collector's config from every TelemetryDestination
dennis-upbound 9e6c9c7
Stop the composed Endpoints being mirrored into a second slice
dennis-upbound 8d00dc8
Correct what the telemetry APIs and guide claim
dennis-upbound 96b22dc
Map only the SGLang metrics SGLang actually publishes
dennis-upbound 0fcb57d
Skip a telemetry object the current schema rejects
dennis-upbound 2b7ce70
Leans down guide, clarifying some sections, adds examples
tr0njavolta f0355d0
Merge branch 'dennis/metrics-impl' into metrics-edits-docs
tr0njavolta 313bc9c
Merge pull request #1 from tr0njavolta/metrics-edits-docs
dennis-upbound 56c8040
Correct what the telemetry guide claims after the edit pass
dennis-upbound cc09442
Answer the rest of the guide's review comments
dennis-upbound 191b0bf
Drop acrossReplicas from MetricMapping
dennis-upbound File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| step: compose-metric-mapping | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
haarchri marked this conversation as resolved.
|
||
| step: compose-telemetry-destination | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.