VictoriaMetrics/lib/promrelabel/if_expression.go
Roman Khavronenko 5f46f8a11d
lib/promrelabel: speedup label match by __name__ (#6432)
The change adds a fastpath for `equalValue` comparisons against
`__name__` label by avoiding calls to `toCanonicalLabelName` func. This
speedups matches by metric name like `'foo'`. See bench stats below:
```
benchcmp old.txt new.txt

benchmark                                           old ns/op     new ns/op     delta
BenchmarkIfExpression/equal_label:_last-10          35.6          35.1          -1.18%
BenchmarkIfExpression/equal_label:_middle-10        18.3          17.3          -5.41%
BenchmarkIfExpression/equal_label:_first-10         1.20          1.24          +2.74%
BenchmarkIfExpression/equal___name__:_last-10       10.1          4.96          -50.75%
BenchmarkIfExpression/equal___name__:_middle-10     5.79          3.16          -45.41%
BenchmarkIfExpression/equal___name__:_first-10      1.17          1.05          -9.76%
```

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-06-07 15:44:48 +02:00

356 lines
7.9 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 interface{}
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(interface{}) error) error {
var v interface{}
if err := f(&v); err != nil {
return fmt.Errorf("cannot unmarshal `match` option: %w", err)
}
return ie.unmarshalFromInterface(v)
}
func (ie *IfExpression) unmarshalFromInterface(v interface{}) 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 []interface{}:
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() (interface{}, 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()
}
return fmt.Sprintf("%s", ie.ies)
}
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(interface{}) 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() (interface{}, 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 "="
}