Files
VictoriaMetrics/lib/promutil/labels.go
Max Kotliar b98e592752 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

348 lines
8.1 KiB
Go

package promutil
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
)
// Labels contains Prometheus labels.
type Labels struct {
Labels []prompb.Label
}
// NewLabels returns Labels with the given capacity.
func NewLabels(capacity int) *Labels {
return &Labels{
Labels: make([]prompb.Label, 0, capacity),
}
}
// NewLabelsFromMap returns Labels generated from m.
func NewLabelsFromMap(m map[string]string) *Labels {
var x Labels
x.InitFromMap(m)
return &x
}
// MarshalYAML implements yaml.Marshaler interface.
func (x *Labels) MarshalYAML() (any, error) {
m := x.ToMap()
return m, nil
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (x *Labels) UnmarshalYAML(unmarshal func(any) error) error {
var m map[string]string
if err := unmarshal(&m); err != nil {
return err
}
x.InitFromMap(m)
return nil
}
// MarshalJSON returns JSON representation for x.
func (x *Labels) MarshalJSON() ([]byte, error) {
m := x.ToMap()
return json.Marshal(m)
}
// UnmarshalJSON unmarshals JSON from data.
func (x *Labels) UnmarshalJSON(data []byte) error {
var m map[string]string
if err := json.Unmarshal(data, &m); err != nil {
return err
}
x.InitFromMap(m)
return nil
}
// InitFromMap initializes x from m.
func (x *Labels) InitFromMap(m map[string]string) {
x.Reset()
for name, value := range m {
x.Add(name, value)
}
x.Sort()
}
// ToMap returns a map for the given labels x.
func (x *Labels) ToMap() map[string]string {
labels := x.GetLabels()
m := make(map[string]string, len(labels))
for _, label := range labels {
m[label.Name] = label.Value
}
return m
}
// GetLabels returns the list of labels from x.
func (x *Labels) GetLabels() []prompb.Label {
if x == nil {
return nil
}
return x.Labels
}
// String returns string representation of x.
func (x *Labels) String() string {
labels := x.GetLabels()
// Calculate the required memory for storing serialized labels.
n := 2 // for `{...}`
for _, label := range labels {
n += len(label.Name) + len(label.Value)
n += 4 // for `="...",`
}
b := make([]byte, 0, n)
b = append(b, '{')
for i, label := range labels {
b = append(b, label.Name...)
b = append(b, '=')
b = strconv.AppendQuote(b, label.Value)
if i+1 < len(labels) {
b = append(b, ',')
}
}
b = append(b, '}')
return bytesutil.ToUnsafeString(b)
}
// Reset resets x.
func (x *Labels) Reset() {
clear(x.Labels)
x.Labels = x.Labels[:0]
}
// Clone returns a clone of x.
func (x *Labels) Clone() *Labels {
srcLabels := x.GetLabels()
labels := append([]prompb.Label{}, srcLabels...)
return &Labels{
Labels: labels,
}
}
// Sort sorts x labels in alphabetical order of their names.
func (x *Labels) Sort() {
if !sort.IsSorted(x) {
sort.Sort(x)
}
}
// SortStable sorts x labels in alphabetical order of their name using stable sort.
func (x *Labels) SortStable() {
if !sort.IsSorted(x) {
sort.Stable(x)
}
}
// Len returns the number of labels in x.
func (x *Labels) Len() int {
labels := x.GetLabels()
return len(labels)
}
// Less compares label names at i and j index.
func (x *Labels) Less(i, j int) bool {
labels := x.Labels
return labels[i].Name < labels[j].Name
}
// Swap swaps labels at i and j index.
func (x *Labels) Swap(i, j int) {
labels := x.Labels
labels[i], labels[j] = labels[j], labels[i]
}
// Add adds name=value label to x.
func (x *Labels) Add(name, value string) {
x.Labels = append(x.Labels, prompb.Label{
Name: name,
Value: value,
})
}
// AddFrom adds src labels to x.
func (x *Labels) AddFrom(src *Labels) {
for _, label := range src.GetLabels() {
x.Add(label.Name, label.Value)
}
}
// Get returns value for label with the given name.
func (x *Labels) Get(name string) string {
labels := x.GetLabels()
for _, label := range labels {
if label.Name == name {
return label.Value
}
}
return ""
}
// Set label value for label with given name
// If the label with the given name doesn't exist, it adds as the new label
func (x *Labels) Set(name, value string) {
if name == "" || value == "" {
return
}
labels := x.GetLabels()
for i, label := range labels {
if label.Name == name {
labels[i].Value = value
return
}
}
x.Add(name, value)
}
// InternStrings interns all the strings used in x labels.
func (x *Labels) InternStrings() {
labels := x.GetLabels()
for _, label := range labels {
label.Name = bytesutil.InternString(label.Name)
label.Value = bytesutil.InternString(label.Value)
}
}
// RemoveDuplicates removes labels with duplicate names.
func (x *Labels) RemoveDuplicates() {
if x.Len() < 2 {
return
}
// Remove duplicate labels if any.
// Stable sorting is needed in order to preserve the order for labels with identical names.
// This is needed in order to remove labels with duplicate names other than the last one.
x.SortStable()
labels := x.Labels
prevName := labels[0].Name
hasDuplicateLabels := false
for _, label := range labels[1:] {
if label.Name == prevName {
hasDuplicateLabels = true
break
}
prevName = label.Name
}
if !hasDuplicateLabels {
return
}
prevName = labels[0].Name
tmp := labels[:1]
for _, label := range labels[1:] {
if label.Name == prevName {
tmp[len(tmp)-1] = label
} else {
tmp = append(tmp, label)
prevName = label.Name
}
}
clear(labels[len(tmp):])
x.Labels = tmp
}
// RemoveMetaLabels removes all the `__meta_` labels from x.
//
// See https://www.robustperception.io/life-of-a-label for details.
func (x *Labels) RemoveMetaLabels() {
src := x.Labels
dst := x.Labels[:0]
for _, label := range src {
if strings.HasPrefix(label.Name, "__meta_") {
continue
}
dst = append(dst, label)
}
clear(src[len(dst):])
x.Labels = dst
}
// RemoveLabelsWithDoubleUnderscorePrefix removes labels with "__" prefix from x.
func (x *Labels) RemoveLabelsWithDoubleUnderscorePrefix() {
src := x.Labels
dst := x.Labels[:0]
for _, label := range src {
name := label.Name
if strings.HasPrefix(name, "__") {
continue
}
dst = append(dst, label)
}
clear(src[len(dst):])
x.Labels = dst
}
// GetLabels returns and empty Labels instance from the pool.
//
// The returned Labels instance must be returned to pool via PutLabels() when no longer needed.
func GetLabels() *Labels {
v := labelsPool.Get()
if v == nil {
return &Labels{}
}
return v.(*Labels)
}
// PutLabels returns x, which has been obtained via GetLabels(), to the pool.
//
// The x mustn't be used after returning to the pool.
func PutLabels(x *Labels) {
x.Reset()
labelsPool.Put(x)
}
var labelsPool sync.Pool
// MustNewLabelsFromString creates labels from s, which can have the form `metric{labels}`.
//
// This function must be used only in tests. Use NewLabelsFromString in production code.
func MustNewLabelsFromString(metricWithLabels string) *Labels {
labels, err := NewLabelsFromString(metricWithLabels)
if err != nil {
logger.Panicf("BUG: cannot parse %q: %s", metricWithLabels, err)
}
return labels
}
// NewLabelsFromString creates labels from s, which can have the form `metric{labels}`.
//
// This function must be used only in non performance-critical code, since it allocates too much
func NewLabelsFromString(metricWithLabels string) (*Labels, error) {
stripDummyMetric := false
if strings.HasPrefix(metricWithLabels, "{") {
// Add a dummy metric name, since the parser needs it
metricWithLabels = "dummy_metric" + metricWithLabels
stripDummyMetric = true
}
// add a value to metricWithLabels, so it could be parsed by prometheus protocol parser.
s := metricWithLabels + " 123"
var rows prometheus.Rows
var err error
rows.UnmarshalWithErrLogger(s, func(s string) {
err = fmt.Errorf("error during metric parse: %s", s)
})
if err != nil {
return nil, err
}
if len(rows.Rows) != 1 {
return nil, fmt.Errorf("unexpected number of rows parsed; got %d; want 1", len(rows.Rows))
}
r := rows.Rows[0]
var x Labels
if !stripDummyMetric {
x.Add("__name__", r.Metric)
}
for _, tag := range r.Tags {
x.Add(tag.Key, tag.Value)
}
return &x, nil
}