Files
Max Kotliar be3eddf425 lib/prompb: Merge prompbmarshal logic into prompb
The prompb and prompbmarshal share exactly the same models and provide
marshal and unmarshale capabilities for them. This creates duplication
(changes in one model has to be made in another, case with metadata) and
confusion where for example you compare same looking models but golang
says they are not the same (because of the type).

This commit merge prompbmarshal logic into prompb so the rest of the
code is aligned on prompb models.

Moves samplesPool and labelsPool to WriteRequestUnmarshaller.
Make WriteRequest struct clean from unmarshal logic.

The benchmark shows no significant changes:

$benchstat prompbmarshal.bench prompb2.bench
goos: darwin
goarch: arm64
pkg: github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb
cpu: Apple M1 Pro
                                 │ prompbmarshal.bench │           prompb2.bench            │
                                 │       sec/op        │   sec/op     vs base               │
WriteRequestUnmarshalProtobuf-10           189.2µ ± 5%   190.8µ ± 8%       ~ (p=0.579 n=10)
WriteRequestMarshalProtobuf-10             145.3µ ± 7%   143.6µ ± 2%       ~ (p=0.143 n=10)
geomean                                    165.8µ        165.5µ       -0.14%

                                 │ prompbmarshal.bench │            prompb2.bench            │
                                 │         B/s         │     B/s       vs base               │
WriteRequestUnmarshalProtobuf-10          50.42Mi ± 5%   49.99Mi ± 8%       ~ (p=0.593 n=10)
WriteRequestMarshalProtobuf-10            65.64Mi ± 7%   66.39Mi ± 2%       ~ (p=0.143 n=10)
geomean                                   57.53Mi        57.61Mi       +0.14%

                                 │ prompbmarshal.bench │            prompb2.bench             │
                                 │        B/op         │     B/op       vs base               │
WriteRequestUnmarshalProtobuf-10         27.70Ki ±  4%   26.90Ki ±  7%       ~ (p=0.190 n=10)
WriteRequestMarshalProtobuf-10           3.267Ki ± 12%   3.273Ki ± 12%       ~ (p=0.971 n=10)
geomean                                  9.514Ki         9.383Ki        -1.38%

                                 │ prompbmarshal.bench │            prompb2.bench            │
                                 │      allocs/op      │ allocs/op   vs base                 │
WriteRequestUnmarshalProtobuf-10          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
WriteRequestMarshalProtobuf-10            0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                              ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
2025-07-31 01:04:11 +03:00

363 lines
7.9 KiB
Go

package promrelabel
import (
"encoding/json"
"fmt"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"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 []prompb.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 []prompb.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 []prompb.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 []prompb.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 []prompb.Label) bool {
for _, label := range labels {
if label.Name == "__name__" {
return label.Value == lf.value
}
}
return false
}
func (lf *labelFilter) equalValue(labels []prompb.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 []prompb.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 "="
}