feature: type-and-unit-labels (PROM-39 implementation) (#16228)
* feature: type-and-unit-labels (extended MetricIdentity) Experimental implementation of https://github.com/prometheus/proposals/pull/39 Previous (unmerged) experiments: * https://github.com/prometheus/prometheus/compare/main...dashpole:prometheus:type_and_unit_labels * https://github.com/prometheus/prometheus/pull/16025 Signed-off-by: bwplotka <bwplotka@gmail.com> feature: type-and-unit-labels (extended MetricIdentity) Experimental implementation of https://github.com/prometheus/proposals/pull/39 Previous (unmerged) experiments: * https://github.com/prometheus/prometheus/compare/main...dashpole:prometheus:type_and_unit_labels * https://github.com/prometheus/prometheus/pull/16025 Signed-off-by: bwplotka <bwplotka@gmail.com> * Fix compilation errors Signed-off-by: Arthur Silva Sens <arthursens2005@gmail.com> Lint Signed-off-by: Arthur Silva Sens <arthursens2005@gmail.com> Revert change made to protobuf 'Accept' header Signed-off-by: Arthur Silva Sens <arthursens2005@gmail.com> Fix compilation errors for 'dedupelabels' tag Signed-off-by: Arthur Silva Sens <arthursens2005@gmail.com> * Rectored into schema.Metadata Signed-off-by: bwplotka <bwplotka@gmail.com> * texparse: Added tests for PromParse Signed-off-by: bwplotka <bwplotka@gmail.com> * add OM tests. Signed-off-by: bwplotka <bwplotka@gmail.com> * add proto tests Signed-off-by: bwplotka <bwplotka@gmail.com> * Addressed comments. Signed-off-by: bwplotka <bwplotka@gmail.com> * add schema label tests. Signed-off-by: bwplotka <bwplotka@gmail.com> * addressed comments. Signed-off-by: bwplotka <bwplotka@gmail.com> * fix tests. Signed-off-by: bwplotka <bwplotka@gmail.com> * add promql tests. Signed-off-by: bwplotka <bwplotka@gmail.com> * lint Signed-off-by: bwplotka <bwplotka@gmail.com> * Addressed comments. Signed-off-by: bwplotka <bwplotka@gmail.com> --------- Signed-off-by: bwplotka <bwplotka@gmail.com> Signed-off-by: Arthur Silva Sens <arthursens2005@gmail.com> Co-authored-by: Arthur Silva Sens <arthursens2005@gmail.com>pull/15365/head^2
parent
5a98246f50
commit
8e6b008608
@ -0,0 +1,280 @@ |
||||
# Test PROM-39 type and unit labels with operators. |
||||
|
||||
# A. Healthy case |
||||
# NOTE: __unit__"request" is not a best practice unit, but keeping that to test the unit handling. |
||||
load 5m |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server", instance="0", group="production"} 0+10x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server", instance="1", group="production"} 0+20x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server", instance="0", group="canary"} 0+30x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server", instance="1", group="canary"} 0+40x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="0", group="production"} 0+50x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="1", group="production"} 0+60x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="0", group="canary"} 0+70x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="1", group="canary"} 0+80x10 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM(http_requests_total{__type__="counter", __unit__="request"}) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM({__type__="counter"}) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM({__unit__="request"}) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM({__type__="counter", __unit__="request"}) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) - COUNT(http_requests_total) BY (job) |
||||
{job="api-server"} 996 |
||||
{job="app-server"} 2596 |
||||
|
||||
eval instant at 50m -http_requests_total{job="api-server",instance="0",group="production"} |
||||
{job="api-server",instance="0",group="production"} -100 |
||||
|
||||
eval instant at 50m +http_requests_total{job="api-server",instance="0",group="production"} |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server",instance="0",group="production"} 100 |
||||
|
||||
eval instant at 50m -10^3 * - SUM(http_requests_total) BY (job) ^ -1 |
||||
{job="api-server"} 1 |
||||
{job="app-server"} 0.38461538461538464 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) / 0 |
||||
{job="api-server"} +Inf |
||||
{job="app-server"} +Inf |
||||
|
||||
eval instant at 50m http_requests_total{group="canary", instance="0", job="api-server"} / 0 |
||||
{group="canary", instance="0", job="api-server"} +Inf |
||||
|
||||
eval instant at 50m 0 * http_requests_total{group="canary", instance="0", job="api-server"} % 0 |
||||
{group="canary", instance="0", job="api-server"} NaN |
||||
|
||||
eval instant at 50m http_requests_total{job="api-server", group="canary"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="api-server"} 400 |
||||
|
||||
eval instant at 50m rate(http_requests_total[25m]) * 25 * 60 |
||||
{group="canary", instance="0", job="api-server"} 150 |
||||
{group="canary", instance="0", job="app-server"} 350 |
||||
{group="canary", instance="1", job="api-server"} 200 |
||||
{group="canary", instance="1", job="app-server"} 400 |
||||
{group="production", instance="0", job="api-server"} 50 |
||||
{group="production", instance="0", job="app-server"} 249.99999999999997 |
||||
{group="production", instance="1", job="api-server"} 100 |
||||
{group="production", instance="1", job="app-server"} 300 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} and http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="0", job="app-server"} 700 |
||||
|
||||
eval instant at 50m (http_requests_total{group="canary"} + 1) and http_requests_total{instance="0"} |
||||
{group="canary", instance="0", job="api-server"} 301 |
||||
{group="canary", instance="0", job="app-server"} 701 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} or http_requests_total{group="production"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="0", job="app-server"} 700 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="app-server"} 800 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="0", job="api-server"} 100 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="0", job="app-server"} 500 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="api-server"} 200 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="app-server"} 600 |
||||
|
||||
# On overlap the rhs samples must be dropped. |
||||
eval instant at 50m (http_requests_total{group="canary"} + 1) or http_requests_total{instance="1"} |
||||
{group="canary", instance="0", job="api-server"} 301 |
||||
{group="canary", instance="0", job="app-server"} 701 |
||||
{group="canary", instance="1", job="api-server"} 401 |
||||
{group="canary", instance="1", job="app-server"} 801 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="api-server"} 200 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="app-server"} 600 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless on(job) http_requests_total{instance="0"} |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless on(job, instance) http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} / on(instance,job) http_requests_total{group="production"} |
||||
{instance="0", job="api-server"} 3 |
||||
{instance="0", job="app-server"} 1.4 |
||||
{instance="1", job="api-server"} 2 |
||||
{instance="1", job="app-server"} 1.3333333333333333 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless ignoring(group, instance) http_requests_total{instance="0"} |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless ignoring(group) http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{__type__="counter", __unit__="request", group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} / ignoring(group) http_requests_total{group="production"} |
||||
{instance="0", job="api-server"} 3 |
||||
{instance="0", job="app-server"} 1.4 |
||||
{instance="1", job="api-server"} 2 |
||||
{instance="1", job="app-server"} 1.3333333333333333 |
||||
|
||||
# Comparisons. |
||||
eval instant at 50m SUM(http_requests_total) BY (job) > 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) == bool SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 1 |
||||
{job="app-server"} 1 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) != bool SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 0 |
||||
{job="app-server"} 0 |
||||
|
||||
eval instant at 50m http_requests_total{job="api-server", instance="0", group="production"} == bool 100 |
||||
{job="api-server", instance="0", group="production"} 1 |
||||
|
||||
clear |
||||
|
||||
# A. Inconsistent type and unit cases for unique series. |
||||
# NOTE: __unit__"request" is not a best practice unit, but keeping that to test the unit handling. |
||||
load 5m |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server", instance="0", group="production"} 0+10x10 |
||||
http_requests_total{__type__="gauge", __unit__="request", job="api-server", instance="1", group="production"} 0+20x10 |
||||
http_requests_total{__type__="gauge", __unit__="not-request", job="api-server", instance="0", group="canary"} 0+30x10 |
||||
http_requests_total{__type__="counter", __unit__="not-request", job="api-server", instance="1", group="canary"} 0+40x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="0", group="production"} 0+50x10 |
||||
http_requests_total{__type__="counter", __unit__="request", job="app-server", instance="1", group="production"} 0+60x10 |
||||
http_requests_total{__type__="counter", __unit__="", job="app-server", instance="0", group="canary"} 0+70x10 |
||||
http_requests_total{job="app-server", instance="1", group="canary"} 0+80x10 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM(http_requests_total{__type__="counter", __unit__="request"}) BY (job) |
||||
{job="api-server"} 100 |
||||
{job="app-server"} 1100 |
||||
|
||||
eval instant at 50m SUM({__type__="counter"}) BY (job) |
||||
{job="api-server"} 500 |
||||
{job="app-server"} 1800 |
||||
|
||||
eval instant at 50m SUM({__unit__="request"}) BY (job) |
||||
{job="api-server"} 300 |
||||
{job="app-server"} 1100 |
||||
|
||||
eval instant at 50m SUM({__type__="counter", __unit__="request"}) BY (job) |
||||
{job="api-server"} 100 |
||||
{job="app-server"} 1100 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) - COUNT(http_requests_total) BY (job) |
||||
{job="api-server"} 996 |
||||
{job="app-server"} 2596 |
||||
|
||||
eval instant at 50m -http_requests_total{job="api-server",instance="0",group="production"} |
||||
{job="api-server",instance="0",group="production"} -100 |
||||
|
||||
eval instant at 50m +http_requests_total{job="api-server",instance="0",group="production"} |
||||
http_requests_total{__type__="counter", __unit__="request", job="api-server",instance="0",group="production"} 100 |
||||
|
||||
eval instant at 50m -10^3 * - SUM(http_requests_total) BY (job) ^ -1 |
||||
{job="api-server"} 1 |
||||
{job="app-server"} 0.38461538461538464 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) / 0 |
||||
{job="api-server"} +Inf |
||||
{job="app-server"} +Inf |
||||
|
||||
eval instant at 50m http_requests_total{group="canary", instance="0", job="api-server"} / 0 |
||||
{group="canary", instance="0", job="api-server"} +Inf |
||||
|
||||
eval instant at 50m 0 * http_requests_total{group="canary", instance="0", job="api-server"} % 0 |
||||
{group="canary", instance="0", job="api-server"} NaN |
||||
|
||||
eval instant at 50m http_requests_total{job="api-server", group="canary"} |
||||
http_requests_total{__type__="gauge", __unit__="not-request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
|
||||
eval instant at 50m http_requests_total{__type__="counter", job="api-server", group="canary"} |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
|
||||
eval instant at 50m rate(http_requests_total[25m]) * 25 * 60 |
||||
{group="canary", instance="0", job="api-server"} 150 |
||||
{group="canary", instance="0", job="app-server"} 350 |
||||
{group="canary", instance="1", job="api-server"} 200 |
||||
{group="canary", instance="1", job="app-server"} 400 |
||||
{group="production", instance="0", job="api-server"} 50 |
||||
{group="production", instance="0", job="app-server"} 249.99999999999997 |
||||
{group="production", instance="1", job="api-server"} 100 |
||||
{group="production", instance="1", job="app-server"} 300 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} and http_requests_total{instance="0"} |
||||
http_requests_total{__type__="gauge", __unit__="not-request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="", group="canary", instance="0", job="app-server"} 700 |
||||
|
||||
eval instant at 50m (http_requests_total{group="canary"} + 1) and http_requests_total{instance="0"} |
||||
{group="canary", instance="0", job="api-server"} 301 |
||||
{group="canary", instance="0", job="app-server"} 701 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} or http_requests_total{group="production"} |
||||
http_requests_total{__type__="gauge", __unit__="not-request", group="canary", instance="0", job="api-server"} 300 |
||||
http_requests_total{__type__="counter", __unit__="", group="canary", instance="0", job="app-server"} 700 |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{group="canary", instance="1", job="app-server"} 800 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="0", job="api-server"} 100 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="0", job="app-server"} 500 |
||||
http_requests_total{__type__="gauge", __unit__="request", group="production", instance="1", job="api-server"} 200 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="app-server"} 600 |
||||
|
||||
# On overlap the rhs samples must be dropped. |
||||
eval instant at 50m (http_requests_total{group="canary"} + 1) or http_requests_total{instance="1"} |
||||
{group="canary", instance="0", job="api-server"} 301 |
||||
{group="canary", instance="0", job="app-server"} 701 |
||||
{group="canary", instance="1", job="api-server"} 401 |
||||
{group="canary", instance="1", job="app-server"} 801 |
||||
http_requests_total{__type__="gauge", __unit__="request", group="production", instance="1", job="api-server"} 200 |
||||
http_requests_total{__type__="counter", __unit__="request", group="production", instance="1", job="app-server"} 600 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless on(job) http_requests_total{instance="0"} |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless on(job, instance) http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} / on(instance,job) http_requests_total{group="production"} |
||||
{instance="0", job="api-server"} 3 |
||||
{instance="0", job="app-server"} 1.4 |
||||
{instance="1", job="api-server"} 2 |
||||
{instance="1", job="app-server"} 1.3333333333333333 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} unless ignoring(group) http_requests_total{instance="0"} |
||||
http_requests_total{__type__="counter", __unit__="not-request", group="canary", instance="1", job="api-server"} 400 |
||||
http_requests_total{group="canary", instance="1", job="app-server"} 800 |
||||
|
||||
eval instant at 50m http_requests_total{group="canary"} / ignoring(group) http_requests_total{group="production"} |
||||
|
||||
# Comparisons. |
||||
eval instant at 50m SUM(http_requests_total) BY (job) > 1000 |
||||
{job="app-server"} 2600 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) == bool SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 1 |
||||
{job="app-server"} 1 |
||||
|
||||
eval instant at 50m SUM(http_requests_total) BY (job) != bool SUM(http_requests_total) BY (job) |
||||
{job="api-server"} 0 |
||||
{job="app-server"} 0 |
||||
|
||||
eval instant at 50m http_requests_total{job="api-server", instance="0", group="production"} == bool 100 |
||||
{job="api-server", instance="0", group="production"} 1 |
@ -0,0 +1,157 @@ |
||||
// Copyright 2025 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package schema |
||||
|
||||
import ( |
||||
"github.com/prometheus/common/model" |
||||
|
||||
"github.com/prometheus/prometheus/model/labels" |
||||
) |
||||
|
||||
const ( |
||||
// Special label names and selectors for schema.Metadata fields.
|
||||
// They are currently private to ensure __name__, __type__ and __unit__ are used
|
||||
// together and remain extensible in Prometheus. See NewMetadataFromLabels and Metadata
|
||||
// methods for the interactions with the labels package structs.
|
||||
metricName = "__name__" |
||||
metricType = "__type__" |
||||
metricUnit = "__unit__" |
||||
) |
||||
|
||||
// IsMetadataLabel returns true if the given label name is a special
|
||||
// schema Metadata label.
|
||||
func IsMetadataLabel(name string) bool { |
||||
return name == metricName || name == metricType || name == metricUnit |
||||
} |
||||
|
||||
// Metadata represents the core metric schema/metadata elements that:
|
||||
// * are describing and identifying the metric schema/shape (e.g. name, type and unit).
|
||||
// * are contributing to the general metric/series identity.
|
||||
// * with the type-and-unit feature, are stored as Prometheus labels.
|
||||
//
|
||||
// Historically, similar information was encoded in the labels.MetricName (suffixes)
|
||||
// and in the separate metadata.Metadata structures. However, with the
|
||||
// type-and-unit-label feature (PROM-39), this information can be now stored directly
|
||||
// in the special schema metadata labels, which offers better reliability (e.g. atomicity),
|
||||
// compatibility and, in many cases, efficiency.
|
||||
//
|
||||
// NOTE: Metadata in the current form is generally similar (yet different) to:
|
||||
// - The MetricFamily definition in OpenMetrics (https://prometheus.io/docs/specs/om/open_metrics_spec/#metricfamily).
|
||||
// However, there is a small and important distinction around the metric name semantics
|
||||
// for the "classic" representation of complex metrics like histograms. The
|
||||
// Metadata.Name follows the __name__ semantics. See Name for details.
|
||||
// - Original metadata.Metadata entries. However, not all fields in that metadata
|
||||
// are "identifiable", notably the help field, plus metadata does not contain Name.
|
||||
type Metadata struct { |
||||
// Name represents the final metric name for a Prometheus series.
|
||||
// NOTE(bwplotka): Prometheus scrape formats (e.g. OpenMetrics) define
|
||||
// the "metric family name". The Metadata.Name (so __name__ label) is not
|
||||
// always the same as the MetricFamily.Name e.g.:
|
||||
// * OpenMetrics metric family name on scrape: "acme_http_router_request_seconds"
|
||||
// * Resulting Prometheus metric name: "acme_http_router_request_seconds_sum"
|
||||
//
|
||||
// Empty string means nameless metric (e.g. result of the PromQL function).
|
||||
Name string |
||||
// Type represents the metric type. Empty value ("") is equivalent to
|
||||
// model.UnknownMetricType.
|
||||
Type model.MetricType |
||||
// Unit represents the metric unit. Empty string means an unitless metric (e.g.
|
||||
// result of the PromQL function).
|
||||
//
|
||||
// NOTE: Currently unit value is not strictly defined other than OpenMetrics
|
||||
// recommendations: https://prometheus.io/docs/specs/om/open_metrics_spec/#units-and-base-units
|
||||
// TODO(bwplotka): Consider a stricter validation and rules e.g. lowercase only or UCUM standard.
|
||||
// Read more in https://github.com/prometheus/proposals/blob/main/proposals/2024-09-25_metadata-labels.md#more-strict-unit-and-type-value-definition
|
||||
Unit string |
||||
} |
||||
|
||||
// NewMetadataFromLabels returns the schema metadata from the labels.
|
||||
func NewMetadataFromLabels(ls labels.Labels) Metadata { |
||||
typ := model.MetricTypeUnknown |
||||
if got := ls.Get(metricType); got != "" { |
||||
typ = model.MetricType(got) |
||||
} |
||||
return Metadata{ |
||||
Name: ls.Get(metricName), |
||||
Type: typ, |
||||
Unit: ls.Get(metricUnit), |
||||
} |
||||
} |
||||
|
||||
// IsTypeEmpty returns true if the metric type is empty (not set).
|
||||
func (m Metadata) IsTypeEmpty() bool { |
||||
return m.Type == "" || m.Type == model.MetricTypeUnknown |
||||
} |
||||
|
||||
// IsEmptyFor returns true if the Metadata field, represented by the given labelName
|
||||
// is empty (not set). If the labelName in not representing any Metadata field,
|
||||
// IsEmptyFor returns true.
|
||||
func (m Metadata) IsEmptyFor(labelName string) bool { |
||||
switch labelName { |
||||
case metricName: |
||||
return m.Name == "" |
||||
case metricType: |
||||
return m.IsTypeEmpty() |
||||
case metricUnit: |
||||
return m.Unit == "" |
||||
default: |
||||
return true |
||||
} |
||||
} |
||||
|
||||
// AddToLabels adds metric schema metadata as labels into the labels.ScratchBuilder.
|
||||
// Empty Metadata fields will be ignored (not added).
|
||||
func (m Metadata) AddToLabels(b *labels.ScratchBuilder) { |
||||
if m.Name != "" { |
||||
b.Add(metricName, m.Name) |
||||
} |
||||
if !m.IsTypeEmpty() { |
||||
b.Add(metricType, string(m.Type)) |
||||
} |
||||
if m.Unit != "" { |
||||
b.Add(metricUnit, m.Unit) |
||||
} |
||||
} |
||||
|
||||
// SetToLabels injects metric schema metadata as labels into the labels.Builder.
|
||||
// It follows the labels.Builder.Set semantics, so empty Metadata fields will
|
||||
// remove the corresponding existing labels if they were previously set.
|
||||
func (m Metadata) SetToLabels(b *labels.Builder) { |
||||
b.Set(metricName, m.Name) |
||||
if m.Type == model.MetricTypeUnknown { |
||||
// Unknown equals empty semantically, so remove the label on unknown too as per
|
||||
// method signature comment.
|
||||
b.Set(metricType, "") |
||||
} else { |
||||
b.Set(metricType, string(m.Type)) |
||||
} |
||||
b.Set(metricUnit, m.Unit) |
||||
} |
||||
|
||||
// IgnoreOverriddenMetadataLabelsScratchBuilder is a wrapper over labels scratch builder
|
||||
// that ignores label additions that would collide with non-empty Overwrite Metadata fields.
|
||||
type IgnoreOverriddenMetadataLabelsScratchBuilder struct { |
||||
*labels.ScratchBuilder |
||||
|
||||
Overwrite Metadata |
||||
} |
||||
|
||||
// Add a name/value pair, unless it would collide with the non-empty Overwrite Metadata
|
||||
// field. Note if you Add the same name twice you will get a duplicate label, which is invalid.
|
||||
func (b IgnoreOverriddenMetadataLabelsScratchBuilder) Add(name, value string) { |
||||
if !b.Overwrite.IsEmptyFor(name) { |
||||
return |
||||
} |
||||
b.ScratchBuilder.Add(name, value) |
||||
} |
@ -0,0 +1,153 @@ |
||||
// Copyright 2025 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package schema |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/prometheus/common/model" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/prometheus/prometheus/model/labels" |
||||
"github.com/prometheus/prometheus/util/testutil" |
||||
) |
||||
|
||||
func TestMetadata(t *testing.T) { |
||||
testMeta := Metadata{ |
||||
Name: "metric_total", |
||||
Type: model.MetricTypeCounter, |
||||
Unit: "seconds", |
||||
} |
||||
|
||||
for _, tcase := range []struct { |
||||
emptyName, emptyType, emptyUnit bool |
||||
}{ |
||||
{}, |
||||
{emptyName: true}, |
||||
{emptyType: true}, |
||||
{emptyUnit: true}, |
||||
{emptyName: true, emptyType: true, emptyUnit: true}, |
||||
} { |
||||
var ( |
||||
expectedMeta Metadata |
||||
expectedLabels labels.Labels |
||||
) |
||||
{ |
||||
// Setup expectations.
|
||||
lb := labels.NewScratchBuilder(0) |
||||
lb.Add("foo", "bar") |
||||
|
||||
if !tcase.emptyName { |
||||
lb.Add(metricName, testMeta.Name) |
||||
expectedMeta.Name = testMeta.Name |
||||
} |
||||
if !tcase.emptyType { |
||||
lb.Add(metricType, string(testMeta.Type)) |
||||
expectedMeta.Type = testMeta.Type |
||||
} else { |
||||
expectedMeta.Type = model.MetricTypeUnknown |
||||
} |
||||
if !tcase.emptyUnit { |
||||
lb.Add(metricUnit, testMeta.Unit) |
||||
expectedMeta.Unit = testMeta.Unit |
||||
} |
||||
lb.Sort() |
||||
expectedLabels = lb.Labels() |
||||
} |
||||
|
||||
t.Run(fmt.Sprintf("meta=%#v", expectedMeta), func(t *testing.T) { |
||||
{ |
||||
// From labels to Metadata.
|
||||
got := NewMetadataFromLabels(expectedLabels) |
||||
require.Equal(t, expectedMeta, got) |
||||
} |
||||
{ |
||||
// Empty methods.
|
||||
require.Equal(t, tcase.emptyName, expectedMeta.IsEmptyFor(metricName)) |
||||
require.Equal(t, tcase.emptyType, expectedMeta.IsEmptyFor(metricType)) |
||||
require.Equal(t, tcase.emptyType, expectedMeta.IsTypeEmpty()) |
||||
require.Equal(t, tcase.emptyUnit, expectedMeta.IsEmptyFor(metricUnit)) |
||||
} |
||||
{ |
||||
// From Metadata to labels for various builders.
|
||||
slb := labels.NewScratchBuilder(0) |
||||
slb.Add("foo", "bar") |
||||
expectedMeta.AddToLabels(&slb) |
||||
slb.Sort() |
||||
testutil.RequireEqual(t, expectedLabels, slb.Labels()) |
||||
|
||||
lb := labels.NewBuilder(labels.FromStrings("foo", "bar")) |
||||
expectedMeta.SetToLabels(lb) |
||||
testutil.RequireEqual(t, expectedLabels, lb.Labels()) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestIgnoreOverriddenMetadataLabelsScratchBuilder(t *testing.T) { |
||||
// PROM-39 specifies that metadata labels should be sourced primarily from the metadata structures.
|
||||
// However, the original labels should be preserved IF the metadata structure does not set or support certain information.
|
||||
// Test those cases with common label interactions.
|
||||
incomingLabels := labels.FromStrings(metricName, "different_name", metricType, string(model.MetricTypeSummary), metricUnit, "MB", "foo", "bar") |
||||
for _, tcase := range []struct { |
||||
highPrioMeta Metadata |
||||
expectedLabels labels.Labels |
||||
}{ |
||||
{ |
||||
expectedLabels: incomingLabels, |
||||
}, |
||||
{ |
||||
highPrioMeta: Metadata{ |
||||
Name: "metric_total", |
||||
Type: model.MetricTypeCounter, |
||||
Unit: "seconds", |
||||
}, |
||||
expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeCounter), metricUnit, "seconds", "foo", "bar"), |
||||
}, |
||||
{ |
||||
highPrioMeta: Metadata{ |
||||
Name: "metric_total", |
||||
Type: model.MetricTypeCounter, |
||||
}, |
||||
expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeCounter), metricUnit, "MB", "foo", "bar"), |
||||
}, |
||||
{ |
||||
highPrioMeta: Metadata{ |
||||
Type: model.MetricTypeCounter, |
||||
Unit: "seconds", |
||||
}, |
||||
expectedLabels: labels.FromStrings(metricName, "different_name", metricType, string(model.MetricTypeCounter), metricUnit, "seconds", "foo", "bar"), |
||||
}, |
||||
{ |
||||
highPrioMeta: Metadata{ |
||||
Name: "metric_total", |
||||
Type: model.MetricTypeUnknown, |
||||
Unit: "seconds", |
||||
}, |
||||
expectedLabels: labels.FromStrings(metricName, "metric_total", metricType, string(model.MetricTypeSummary), metricUnit, "seconds", "foo", "bar"), |
||||
}, |
||||
} { |
||||
t.Run(fmt.Sprintf("meta=%#v", tcase.highPrioMeta), func(t *testing.T) { |
||||
lb := labels.NewScratchBuilder(0) |
||||
tcase.highPrioMeta.AddToLabels(&lb) |
||||
wrapped := &IgnoreOverriddenMetadataLabelsScratchBuilder{ScratchBuilder: &lb, Overwrite: tcase.highPrioMeta} |
||||
incomingLabels.Range(func(l labels.Label) { |
||||
wrapped.Add(l.Name, l.Value) |
||||
}) |
||||
lb.Sort() |
||||
require.Equal(t, tcase.expectedLabels, lb.Labels()) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue