mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-23 08:56:31 +01:00
5168d0f754
While at it, simplify this function a bit after the commit 861852f262
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6462
363 lines
8.0 KiB
Go
363 lines
8.0 KiB
Go
package promrelabel
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/regexutil"
|
|
"github.com/VictoriaMetrics/metricsql"
|
|
)
|
|
|
|
// IfExpression represents PromQL-like label filters such as `metric_name{filters...}`.
|
|
//
|
|
// It may contain either a single filter or multiple filters, which are executed with `or` operator.
|
|
//
|
|
// Examples:
|
|
//
|
|
// if: 'foo{bar="baz"}'
|
|
//
|
|
// if:
|
|
// - 'foo{bar="baz"}'
|
|
// - '{x=~"y"}'
|
|
type IfExpression struct {
|
|
ies []*ifExpression
|
|
}
|
|
|
|
// Match returns true if labels match at least a single label filter inside ie.
|
|
//
|
|
// Match returns true for empty ie.
|
|
func (ie *IfExpression) Match(labels []prompbmarshal.Label) bool {
|
|
if ie == nil || len(ie.ies) == 0 {
|
|
return true
|
|
}
|
|
for _, ie := range ie.ies {
|
|
if ie.Match(labels) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Parse parses ie from s.
|
|
func (ie *IfExpression) Parse(s string) error {
|
|
ieLocal, err := newIfExpression(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ie.ies = []*ifExpression{ieLocal}
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals ie from JSON data.
|
|
func (ie *IfExpression) UnmarshalJSON(data []byte) error {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return ie.unmarshalFromInterface(v)
|
|
}
|
|
|
|
// MarshalJSON marshals ie to JSON.
|
|
func (ie *IfExpression) MarshalJSON() ([]byte, error) {
|
|
if ie == nil || len(ie.ies) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(ie.ies) == 1 {
|
|
return json.Marshal(ie.ies[0])
|
|
}
|
|
return json.Marshal(ie.ies)
|
|
}
|
|
|
|
// UnmarshalYAML unmarshals ie from YAML passed to f.
|
|
func (ie *IfExpression) UnmarshalYAML(f func(any) error) error {
|
|
var v any
|
|
if err := f(&v); err != nil {
|
|
return fmt.Errorf("cannot unmarshal `match` option: %w", err)
|
|
}
|
|
return ie.unmarshalFromInterface(v)
|
|
}
|
|
|
|
func (ie *IfExpression) unmarshalFromInterface(v any) error {
|
|
ies := ie.ies[:0]
|
|
switch t := v.(type) {
|
|
case string:
|
|
ieLocal, err := newIfExpression(t)
|
|
if err != nil {
|
|
return fmt.Errorf("unexpected `match` option: %w", err)
|
|
}
|
|
ies = append(ies, ieLocal)
|
|
case []any:
|
|
for _, x := range t {
|
|
s, ok := x.(string)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected `match` item type; got %#v; want string", x)
|
|
}
|
|
ieLocal, err := newIfExpression(s)
|
|
if err != nil {
|
|
return fmt.Errorf("unexpected `match` item: %w", err)
|
|
}
|
|
ies = append(ies, ieLocal)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unexpected `match` type; got %#v; want string or an array of strings", t)
|
|
}
|
|
ie.ies = ies
|
|
return nil
|
|
}
|
|
|
|
// MarshalYAML marshals ie to YAML
|
|
func (ie *IfExpression) MarshalYAML() (any, error) {
|
|
if ie == nil || len(ie.ies) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(ie.ies) == 1 {
|
|
return ie.ies[0].MarshalYAML()
|
|
}
|
|
a := make([]string, 0, len(ie.ies))
|
|
for _, ieLocal := range ie.ies {
|
|
v, err := ieLocal.MarshalYAML()
|
|
if err != nil {
|
|
logger.Panicf("BUG: unexpected error: %s", err)
|
|
}
|
|
s := v.(string)
|
|
a = append(a, s)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func newIfExpression(s string) (*ifExpression, error) {
|
|
var ie ifExpression
|
|
if err := ie.Parse(s); err != nil {
|
|
return nil, err
|
|
}
|
|
return &ie, nil
|
|
}
|
|
|
|
// String returns string representation of ie.
|
|
func (ie *IfExpression) String() string {
|
|
if ie == nil {
|
|
return "{}"
|
|
}
|
|
if len(ie.ies) == 1 {
|
|
return ie.ies[0].String()
|
|
}
|
|
|
|
b := append([]byte{}, ie.ies[0].String()...)
|
|
for _, e := range ie.ies[1:] {
|
|
b = append(b, ',')
|
|
b = append(b, e.String()...)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
type ifExpression struct {
|
|
s string
|
|
lfss [][]*labelFilter
|
|
}
|
|
|
|
func (ie *ifExpression) String() string {
|
|
if ie == nil {
|
|
return ""
|
|
}
|
|
return ie.s
|
|
}
|
|
|
|
func (ie *ifExpression) Parse(s string) error {
|
|
expr, err := metricsql.Parse(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
me, ok := expr.(*metricsql.MetricExpr)
|
|
if !ok {
|
|
return fmt.Errorf("expecting series selector; got %q", expr.AppendString(nil))
|
|
}
|
|
lfss, err := metricExprToLabelFilterss(me)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse series selector: %w", err)
|
|
}
|
|
ie.s = s
|
|
ie.lfss = lfss
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals ie from JSON data.
|
|
func (ie *ifExpression) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
return ie.Parse(s)
|
|
}
|
|
|
|
// MarshalJSON marshals ie to JSON.
|
|
func (ie *ifExpression) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(ie.s)
|
|
}
|
|
|
|
// UnmarshalYAML unmarshals ie from YAML passed to f.
|
|
func (ie *ifExpression) UnmarshalYAML(f func(any) error) error {
|
|
var s string
|
|
if err := f(&s); err != nil {
|
|
return fmt.Errorf("cannot unmarshal `if` option: %w", err)
|
|
}
|
|
if err := ie.Parse(s); err != nil {
|
|
return fmt.Errorf("cannot parse `if` series selector: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalYAML marshals ie to YAML.
|
|
func (ie *ifExpression) MarshalYAML() (any, error) {
|
|
return ie.s, nil
|
|
}
|
|
|
|
// Match returns true if ie matches the given labels.
|
|
func (ie *ifExpression) Match(labels []prompbmarshal.Label) bool {
|
|
if ie == nil {
|
|
return true
|
|
}
|
|
for _, lfs := range ie.lfss {
|
|
if matchLabelFilters(lfs, labels) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchLabelFilters(lfs []*labelFilter, labels []prompbmarshal.Label) bool {
|
|
for _, lf := range lfs {
|
|
if !lf.match(labels) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func metricExprToLabelFilterss(me *metricsql.MetricExpr) ([][]*labelFilter, error) {
|
|
lfssNew := make([][]*labelFilter, len(me.LabelFilterss))
|
|
for i, lfs := range me.LabelFilterss {
|
|
lfsNew := make([]*labelFilter, len(lfs))
|
|
for j := range lfs {
|
|
lf, err := newLabelFilter(&lfs[j])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse %s: %w", me.AppendString(nil), err)
|
|
}
|
|
lfsNew[j] = lf
|
|
}
|
|
lfssNew[i] = lfsNew
|
|
}
|
|
return lfssNew, nil
|
|
}
|
|
|
|
// labelFilter contains PromQL filter for `{label op "value"}`
|
|
type labelFilter struct {
|
|
label string
|
|
op string
|
|
value string
|
|
|
|
// re contains compiled regexp for `=~` and `!~` op.
|
|
re *regexutil.PromRegex
|
|
}
|
|
|
|
func newLabelFilter(mlf *metricsql.LabelFilter) (*labelFilter, error) {
|
|
lf := &labelFilter{
|
|
label: toCanonicalLabelName(mlf.Label),
|
|
op: getFilterOp(mlf),
|
|
value: mlf.Value,
|
|
}
|
|
if lf.op == "=~" || lf.op == "!~" {
|
|
re, err := regexutil.NewPromRegex(lf.value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse regexp for %s: %w", mlf.AppendString(nil), err)
|
|
}
|
|
lf.re = re
|
|
}
|
|
return lf, nil
|
|
}
|
|
|
|
func (lf *labelFilter) match(labels []prompbmarshal.Label) bool {
|
|
switch lf.op {
|
|
case "=":
|
|
return lf.equalValue(labels)
|
|
case "!=":
|
|
return !lf.equalValue(labels)
|
|
case "=~":
|
|
return lf.matchRegexp(labels)
|
|
case "!~":
|
|
return !lf.matchRegexp(labels)
|
|
default:
|
|
logger.Panicf("BUG: unexpected operation for label filter: %s", lf.op)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (lf *labelFilter) equalNameValue(labels []prompbmarshal.Label) bool {
|
|
for _, label := range labels {
|
|
if label.Name == "__name__" {
|
|
return label.Value == lf.value
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (lf *labelFilter) equalValue(labels []prompbmarshal.Label) bool {
|
|
if lf.label == "" {
|
|
return lf.equalNameValue(labels)
|
|
}
|
|
labelNameMatches := 0
|
|
for _, label := range labels {
|
|
if label.Name != lf.label {
|
|
continue
|
|
}
|
|
labelNameMatches++
|
|
if label.Value == lf.value {
|
|
return true
|
|
}
|
|
}
|
|
if labelNameMatches == 0 {
|
|
// Special case for {non_existing_label=""}, which matches anything except of non-empty non_existing_label
|
|
return lf.value == ""
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (lf *labelFilter) matchRegexp(labels []prompbmarshal.Label) bool {
|
|
labelNameMatches := 0
|
|
for _, label := range labels {
|
|
if toCanonicalLabelName(label.Name) != lf.label {
|
|
continue
|
|
}
|
|
labelNameMatches++
|
|
if lf.re.MatchString(label.Value) {
|
|
return true
|
|
}
|
|
}
|
|
if labelNameMatches == 0 {
|
|
// Special case for {non_existing_label=~"something|"}, which matches empty non_existing_label
|
|
return lf.re.MatchString("")
|
|
}
|
|
return false
|
|
}
|
|
|
|
func toCanonicalLabelName(labelName string) string {
|
|
if labelName == "__name__" {
|
|
return ""
|
|
}
|
|
return labelName
|
|
}
|
|
|
|
func getFilterOp(mlf *metricsql.LabelFilter) string {
|
|
if mlf.IsNegative {
|
|
if mlf.IsRegexp {
|
|
return "!~"
|
|
}
|
|
return "!="
|
|
}
|
|
if mlf.IsRegexp {
|
|
return "=~"
|
|
}
|
|
return "="
|
|
}
|