github.com/MetalBlockchain/metalgo@v1.11.9/api/metrics/prefix_gatherer.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package metrics
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  
    10  	"github.com/prometheus/client_golang/prometheus"
    11  	"google.golang.org/protobuf/proto"
    12  
    13  	"github.com/MetalBlockchain/metalgo/utils/metric"
    14  
    15  	dto "github.com/prometheus/client_model/go"
    16  )
    17  
    18  var (
    19  	_ MultiGatherer = (*prefixGatherer)(nil)
    20  
    21  	errOverlappingNamespaces = errors.New("prefix could create overlapping namespaces")
    22  )
    23  
    24  // NewPrefixGatherer returns a new MultiGatherer that merges metrics by adding a
    25  // prefix to their names.
    26  func NewPrefixGatherer() MultiGatherer {
    27  	return &prefixGatherer{}
    28  }
    29  
    30  type prefixGatherer struct {
    31  	multiGatherer
    32  }
    33  
    34  func (g *prefixGatherer) Register(prefix string, gatherer prometheus.Gatherer) error {
    35  	g.lock.Lock()
    36  	defer g.lock.Unlock()
    37  
    38  	for _, existingPrefix := range g.names {
    39  		if eitherIsPrefix(prefix, existingPrefix) {
    40  			return fmt.Errorf("%w: %q conflicts with %q",
    41  				errOverlappingNamespaces,
    42  				prefix,
    43  				existingPrefix,
    44  			)
    45  		}
    46  	}
    47  
    48  	g.names = append(g.names, prefix)
    49  	g.gatherers = append(g.gatherers, &prefixedGatherer{
    50  		prefix:   prefix,
    51  		gatherer: gatherer,
    52  	})
    53  	return nil
    54  }
    55  
    56  type prefixedGatherer struct {
    57  	prefix   string
    58  	gatherer prometheus.Gatherer
    59  }
    60  
    61  func (g *prefixedGatherer) Gather() ([]*dto.MetricFamily, error) {
    62  	// Gather returns partially filled metrics in the case of an error. So, it
    63  	// is expected to still return the metrics in the case an error is returned.
    64  	metricFamilies, err := g.gatherer.Gather()
    65  	for _, metricFamily := range metricFamilies {
    66  		metricFamily.Name = proto.String(metric.AppendNamespace(
    67  			g.prefix,
    68  			metricFamily.GetName(),
    69  		))
    70  	}
    71  	return metricFamilies, err
    72  }
    73  
    74  // eitherIsPrefix returns true if either [a] is a prefix of [b] or [b] is a
    75  // prefix of [a].
    76  //
    77  // This function accounts for the usage of the namespace boundary, so "hello" is
    78  // not considered a prefix of "helloworld". However, "hello" is considered a
    79  // prefix of "hello_world".
    80  func eitherIsPrefix(a, b string) bool {
    81  	if len(a) > len(b) {
    82  		a, b = b, a
    83  	}
    84  	return a == b[:len(a)] && // a is a prefix of b
    85  		(len(a) == 0 || // a is empty
    86  			len(a) == len(b) || // a is equal to b
    87  			b[len(a)] == metric.NamespaceSeparatorByte) // a ends at a namespace boundary of b
    88  }