mirror of
https://github.com/hanzoai/metrics.git
synced 2026-10-30 20:03:39 +00:00
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
363 lines
7.9 KiB
Go
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 "="
|
|
}
|