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 }