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