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":
parser.ExperimentalDurationExpr = true
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":
c.tsdb.EnableNativeHistograms = 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) {
f, err := strconv.ParseFloat(v, 64)
if err == nil {
return formatOpenMetricsFloat(f)
return FormatOpenMetricsFloat(f)
}
}
return v

View File

@ -34,7 +34,7 @@ import (
"github.com/prometheus/prometheus/schema"
)
// floatFormatBufPool is exclusively used in formatOpenMetricsFloat.
// floatFormatBufPool is exclusively used in FormatOpenMetricsFloat.
var floatFormatBufPool = sync.Pool{
New: func() any {
// 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()
q := qq[p.fieldPos]
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:
bb := p.dec.GetHistogram().GetBucket()
if p.fieldPos >= len(bb) {
@ -641,15 +641,15 @@ func (p *ProtobufParser) getMagicLabel() (bool, string, string) {
}
b := bb[p.fieldPos]
p.fieldsDone = math.IsInf(b.GetUpperBound(), +1)
return true, model.BucketLabel, formatOpenMetricsFloat(b.GetUpperBound())
return true, model.BucketLabel, FormatOpenMetricsFloat(b.GetUpperBound())
}
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
// "." nor an "e".
func formatOpenMetricsFloat(f float64) string {
func FormatOpenMetricsFloat(f float64) string {
// A few common cases hardcoded.
switch {
case f == 1:

View File

@ -28,6 +28,7 @@ import (
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/textparse"
"github.com/prometheus/prometheus/model/timestamp"
"github.com/prometheus/prometheus/promql/parser/posrange"
"github.com/prometheus/prometheus/util/strutil"
@ -42,6 +43,11 @@ var parserPool = sync.Pool{
// ExperimentalDurationExpr is a flag to enable experimental duration expression parsing.
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 {
ParseExpr() (Expr, error)
Close()
@ -839,6 +845,31 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
p.checkAST(n.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 != "" {
// 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