Normalise 'le' and 'quantile' labels in queries

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>
This commit is contained in:
Ganesh Vernekar 2025-09-02 15:50:48 -07:00
parent 11c49151b7
commit 8815b789b8
4 changed files with 40 additions and 6 deletions

View File

@ -253,6 +253,9 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
case "promql-duration-expr": case "promql-duration-expr":
parser.ExperimentalDurationExpr = true parser.ExperimentalDurationExpr = true
logger.Info("Experimental duration expression parsing enabled.") logger.Info("Experimental duration expression parsing enabled.")
case "promql-normalise-le-quantile-labels":
parser.NormaliseLeAndQuantileMatchers = true
logger.Info("Experimental normalising of 'le' and 'quantile' labels for PromQL enabled.")
case "native-histograms": case "native-histograms":
c.tsdb.EnableNativeHistograms = true c.tsdb.EnableNativeHistograms = true
c.scrape.EnableNativeHistogramsIngestion = true c.scrape.EnableNativeHistogramsIngestion = true

View File

@ -773,7 +773,7 @@ func normalizeFloatsInLabelValues(t model.MetricType, l, v string) string {
if (t == model.MetricTypeSummary && l == model.QuantileLabel) || (t == model.MetricTypeHistogram && l == model.BucketLabel) { if (t == model.MetricTypeSummary && l == model.QuantileLabel) || (t == model.MetricTypeHistogram && l == model.BucketLabel) {
f, err := strconv.ParseFloat(v, 64) f, err := strconv.ParseFloat(v, 64)
if err == nil { if err == nil {
return formatOpenMetricsFloat(f) return FormatOpenMetricsFloat(f)
} }
} }
return v return v

View File

@ -34,7 +34,7 @@ import (
"github.com/prometheus/prometheus/schema" "github.com/prometheus/prometheus/schema"
) )
// floatFormatBufPool is exclusively used in formatOpenMetricsFloat. // floatFormatBufPool is exclusively used in FormatOpenMetricsFloat.
var floatFormatBufPool = sync.Pool{ var floatFormatBufPool = sync.Pool{
New: func() any { New: func() any {
// To contain at most 17 digits and additional syntax for a float64. // To contain at most 17 digits and additional syntax for a float64.
@ -632,7 +632,7 @@ func (p *ProtobufParser) getMagicLabel() (bool, string, string) {
qq := p.dec.GetSummary().GetQuantile() qq := p.dec.GetSummary().GetQuantile()
q := qq[p.fieldPos] q := qq[p.fieldPos]
p.fieldsDone = p.fieldPos == len(qq)-1 p.fieldsDone = p.fieldPos == len(qq)-1
return true, model.QuantileLabel, formatOpenMetricsFloat(q.GetQuantile()) return true, model.QuantileLabel, FormatOpenMetricsFloat(q.GetQuantile())
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
bb := p.dec.GetHistogram().GetBucket() bb := p.dec.GetHistogram().GetBucket()
if p.fieldPos >= len(bb) { if p.fieldPos >= len(bb) {
@ -641,15 +641,15 @@ func (p *ProtobufParser) getMagicLabel() (bool, string, string) {
} }
b := bb[p.fieldPos] b := bb[p.fieldPos]
p.fieldsDone = math.IsInf(b.GetUpperBound(), +1) p.fieldsDone = math.IsInf(b.GetUpperBound(), +1)
return true, model.BucketLabel, formatOpenMetricsFloat(b.GetUpperBound()) return true, model.BucketLabel, FormatOpenMetricsFloat(b.GetUpperBound())
} }
return false, "", "" return false, "", ""
} }
// formatOpenMetricsFloat works like the usual Go string formatting of a float // FormatOpenMetricsFloat works like the usual Go string formatting of a float
// but appends ".0" if the resulting number would otherwise contain neither a // but appends ".0" if the resulting number would otherwise contain neither a
// "." nor an "e". // "." nor an "e".
func formatOpenMetricsFloat(f float64) string { func FormatOpenMetricsFloat(f float64) string {
// A few common cases hardcoded. // A few common cases hardcoded.
switch { switch {
case f == 1: case f == 1:

View File

@ -28,6 +28,7 @@ import (
"github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"github.com/prometheus/prometheus/model/timestamp" "github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/promql/parser/posrange" "github.com/prometheus/prometheus/promql/parser/posrange"
"github.com/prometheus/prometheus/util/strutil" "github.com/prometheus/prometheus/util/strutil"
@ -42,6 +43,11 @@ var parserPool = sync.Pool{
// ExperimentalDurationExpr is a flag to enable experimental duration expression parsing. // ExperimentalDurationExpr is a flag to enable experimental duration expression parsing.
var ExperimentalDurationExpr bool var ExperimentalDurationExpr bool
// NormaliseLeAndQuantileMatchers is a flag to enable experimental conversion of matchers like
// le="2" and quantile="2" to le=~"2|2.0" and quantile=~"2|2.0" to match the normalisation
// of these labels during scrape time.
var NormaliseLeAndQuantileMatchers bool
type Parser interface { type Parser interface {
ParseExpr() (Expr, error) ParseExpr() (Expr, error)
Close() Close()
@ -839,6 +845,31 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
p.checkAST(n.VectorSelector) p.checkAST(n.VectorSelector)
case *VectorSelector: case *VectorSelector:
if NormaliseLeAndQuantileMatchers {
for i, m := range n.LabelMatchers {
// Rewrite the matchers of type le="2" and quantile="2".
if m == nil || m.Type != labels.MatchEqual || (m.Name != model.QuantileLabel && m.Name != model.BucketLabel) {
continue
}
f, err := strconv.ParseFloat(m.Value, 64)
if err != nil {
continue
}
omFloat := textparse.FormatOpenMetricsFloat(f)
if omFloat == m.Value {
// The label value is already in the OpenMetric format, example le="2.0".
continue
}
// Example: changes the matcher from le="2" to le=~"2|2.0".
mat, err := labels.NewMatcher(labels.MatchRegexp, m.Name, fmt.Sprintf("%s|%s", m.Value, omFloat))
if err == nil {
n.LabelMatchers[i] = mat
} else {
p.addParseErrf(n.PositionRange(), "rewrite of matcher failed: %s", err.Error())
}
}
}
if n.Name != "" { if n.Name != "" {
// In this case the last LabelMatcher is checking for the metric name // In this case the last LabelMatcher is checking for the metric name
// set outside the braces. This checks if the name has already been set // set outside the braces. This checks if the name has already been set