mirror of https://github.com/prometheus/prometheus
Browse Source
Return annotations (warnings and infos) from PromQL queries This generalizes the warnings we have already used before (but only for problems with remote read) as "annotations". Annotations can be warnings or infos (the latter could be false positives). We do not treat them different in the API for now and return them all as "warnings". It would be easy to distinguish them and return infos separately, should that appear useful in the future. The new annotations are then used to create a lot of warnings or infos during PromQL evaluations. Partially these are things we have wanted for a long time (e.g. inform the user that they have applied `rate` to a metric that doesn't look like a counter), but the new native histograms have created even more needs for those annotations (e.g. if a query tries to aggregate float numbers with histograms). The annotations added here are not yet complete. A prominent example would be a warning about a range too short for a rate calculation. But such a warnings is more tricky to create with good fidelity and we will tackle it later. Another TODO is to take annotations into account when evaluating recording rules. --------- Signed-off-by: Jeanette Tan <jeanette.tan@grafana.com>pull/12852/head
zenador
1 year ago
committed by
GitHub
34 changed files with 1204 additions and 959 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@ |
|||||||
|
// Copyright 2023 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.
|
||||||
|
|
||||||
|
// posrange is used to report a position in query strings for error
|
||||||
|
// and warning messages.
|
||||||
|
package posrange |
||||||
|
|
||||||
|
import "fmt" |
||||||
|
|
||||||
|
// Pos is the position in a string.
|
||||||
|
// Negative numbers indicate undefined positions.
|
||||||
|
type Pos int |
||||||
|
|
||||||
|
// PositionRange describes a position in the input string of the parser.
|
||||||
|
type PositionRange struct { |
||||||
|
Start Pos |
||||||
|
End Pos |
||||||
|
} |
||||||
|
|
||||||
|
// StartPosInput uses the query string to convert the PositionRange into a
|
||||||
|
// line:col string, indicating when this is not possible if the query is empty
|
||||||
|
// or the position is invalid. When this is used to convert ParseErr to a string,
|
||||||
|
// lineOffset is an additional line offset to be added, and is only used inside
|
||||||
|
// unit tests.
|
||||||
|
func (p PositionRange) StartPosInput(query string, lineOffset int) string { |
||||||
|
if query == "" { |
||||||
|
return "unknown position" |
||||||
|
} |
||||||
|
pos := int(p.Start) |
||||||
|
if pos < 0 || pos > len(query) { |
||||||
|
return "invalid position" |
||||||
|
} |
||||||
|
|
||||||
|
lastLineBreak := -1 |
||||||
|
line := lineOffset + 1 |
||||||
|
for i, c := range query[:pos] { |
||||||
|
if c == '\n' { |
||||||
|
lastLineBreak = i |
||||||
|
line++ |
||||||
|
} |
||||||
|
} |
||||||
|
col := pos - lastLineBreak |
||||||
|
return fmt.Sprintf("%d:%d", line, col) |
||||||
|
} |
@ -0,0 +1,165 @@ |
|||||||
|
// Copyright 2023 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 annotations |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/prometheus/common/model" |
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/promql/parser/posrange" |
||||||
|
) |
||||||
|
|
||||||
|
// Annotations is a general wrapper for warnings and other information
|
||||||
|
// that is returned by the query API along with the results.
|
||||||
|
// Each individual annotation is modeled by a Go error.
|
||||||
|
// They are deduplicated based on the string returned by error.Error().
|
||||||
|
// The zero value is usable without further initialization, see New().
|
||||||
|
type Annotations map[string]error |
||||||
|
|
||||||
|
// New returns new Annotations ready to use. Note that the zero value of
|
||||||
|
// Annotations is also fully usable, but using this method is often more
|
||||||
|
// readable.
|
||||||
|
func New() *Annotations { |
||||||
|
return &Annotations{} |
||||||
|
} |
||||||
|
|
||||||
|
// Add adds an annotation (modeled as a Go error) in-place and returns the
|
||||||
|
// modified Annotations for convenience.
|
||||||
|
func (a *Annotations) Add(err error) Annotations { |
||||||
|
if *a == nil { |
||||||
|
*a = Annotations{} |
||||||
|
} |
||||||
|
(*a)[err.Error()] = err |
||||||
|
return *a |
||||||
|
} |
||||||
|
|
||||||
|
// Merge adds the contents of the second annotation to the first, modifying
|
||||||
|
// the first in-place, and returns the merged first Annotation for convenience.
|
||||||
|
func (a *Annotations) Merge(aa Annotations) Annotations { |
||||||
|
if *a == nil { |
||||||
|
*a = Annotations{} |
||||||
|
} |
||||||
|
for key, val := range aa { |
||||||
|
(*a)[key] = val |
||||||
|
} |
||||||
|
return *a |
||||||
|
} |
||||||
|
|
||||||
|
// AsErrors is a convenience function to return the annotations map as a slice
|
||||||
|
// of errors.
|
||||||
|
func (a Annotations) AsErrors() []error { |
||||||
|
arr := make([]error, 0, len(a)) |
||||||
|
for _, err := range a { |
||||||
|
arr = append(arr, err) |
||||||
|
} |
||||||
|
return arr |
||||||
|
} |
||||||
|
|
||||||
|
// AsStrings is a convenience function to return the annotations map as a slice
|
||||||
|
// of strings. The query string is used to get the line number and character offset
|
||||||
|
// positioning info of the elements which trigger an annotation. We limit the number
|
||||||
|
// of annotations returned here with maxAnnos (0 for no limit).
|
||||||
|
func (a Annotations) AsStrings(query string, maxAnnos int) []string { |
||||||
|
arr := make([]string, 0, len(a)) |
||||||
|
for _, err := range a { |
||||||
|
if maxAnnos > 0 && len(arr) >= maxAnnos { |
||||||
|
break |
||||||
|
} |
||||||
|
anErr, ok := err.(annoErr) |
||||||
|
if ok { |
||||||
|
anErr.Query = query |
||||||
|
err = anErr |
||||||
|
} |
||||||
|
arr = append(arr, err.Error()) |
||||||
|
} |
||||||
|
if maxAnnos > 0 && len(a) > maxAnnos { |
||||||
|
arr = append(arr, fmt.Sprintf("%d more annotations omitted", len(a)-maxAnnos)) |
||||||
|
} |
||||||
|
return arr |
||||||
|
} |
||||||
|
|
||||||
|
//nolint:revive // Ignore ST1012
|
||||||
|
var ( |
||||||
|
// Currently there are only 2 types, warnings and info.
|
||||||
|
// For now, info are visually identical with warnings as we have not updated
|
||||||
|
// the API spec or the frontend to show a different kind of warning. But we
|
||||||
|
// make the distinction here to prepare for adding them in future.
|
||||||
|
PromQLInfo = errors.New("PromQL info") |
||||||
|
PromQLWarning = errors.New("PromQL warning") |
||||||
|
|
||||||
|
InvalidQuantileWarning = fmt.Errorf("%w: quantile value should be between 0 and 1", PromQLWarning) |
||||||
|
BadBucketLabelWarning = fmt.Errorf("%w: bucket label %q is missing or has a malformed value", PromQLWarning, model.BucketLabel) |
||||||
|
MixedFloatsHistogramsWarning = fmt.Errorf("%w: encountered a mix of histograms and floats for metric name", PromQLWarning) |
||||||
|
MixedClassicNativeHistogramsWarning = fmt.Errorf("%w: vector contains a mix of classic and native histograms for metric name", PromQLWarning) |
||||||
|
|
||||||
|
PossibleNonCounterInfo = fmt.Errorf("%w: metric might not be a counter, name does not end in _total/_sum/_count:", PromQLInfo) |
||||||
|
) |
||||||
|
|
||||||
|
type annoErr struct { |
||||||
|
PositionRange posrange.PositionRange |
||||||
|
Err error |
||||||
|
Query string |
||||||
|
} |
||||||
|
|
||||||
|
func (e annoErr) Error() string { |
||||||
|
return fmt.Sprintf("%s (%s)", e.Err, e.PositionRange.StartPosInput(e.Query, 0)) |
||||||
|
} |
||||||
|
|
||||||
|
// NewInvalidQuantileWarning is used when the user specifies an invalid quantile
|
||||||
|
// value, i.e. a float that is outside the range [0, 1] or NaN.
|
||||||
|
func NewInvalidQuantileWarning(q float64, pos posrange.PositionRange) annoErr { |
||||||
|
return annoErr{ |
||||||
|
PositionRange: pos, |
||||||
|
Err: fmt.Errorf("%w, got %g", InvalidQuantileWarning, q), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewBadBucketLabelWarning is used when there is an error parsing the bucket label
|
||||||
|
// of a classic histogram.
|
||||||
|
func NewBadBucketLabelWarning(metricName, label string, pos posrange.PositionRange) annoErr { |
||||||
|
return annoErr{ |
||||||
|
PositionRange: pos, |
||||||
|
Err: fmt.Errorf("%w of %q for metric name %q", BadBucketLabelWarning, label, metricName), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewMixedFloatsHistogramsWarning is used when the queried series includes both
|
||||||
|
// float samples and histogram samples for functions that do not support mixed
|
||||||
|
// samples.
|
||||||
|
func NewMixedFloatsHistogramsWarning(metricName string, pos posrange.PositionRange) annoErr { |
||||||
|
return annoErr{ |
||||||
|
PositionRange: pos, |
||||||
|
Err: fmt.Errorf("%w %q", MixedFloatsHistogramsWarning, metricName), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewMixedClassicNativeHistogramsWarning is used when the queried series includes
|
||||||
|
// both classic and native histograms.
|
||||||
|
func NewMixedClassicNativeHistogramsWarning(metricName string, pos posrange.PositionRange) annoErr { |
||||||
|
return annoErr{ |
||||||
|
PositionRange: pos, |
||||||
|
Err: fmt.Errorf("%w %q", MixedClassicNativeHistogramsWarning, metricName), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewPossibleNonCounterInfo is used when a counter metric does not have the suffixes
|
||||||
|
// _total, _sum or _count.
|
||||||
|
func NewPossibleNonCounterInfo(metricName string, pos posrange.PositionRange) annoErr { |
||||||
|
return annoErr{ |
||||||
|
PositionRange: pos, |
||||||
|
Err: fmt.Errorf("%w %q", PossibleNonCounterInfo, metricName), |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue