Skip to content

Commit 24a051a

Browse files
uplift: Add combined metrics package from evm repositories (#4135)
Signed-off-by: Jonathan Oppenheimer <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent d9b512e commit 24a051a

File tree

5 files changed

+424
-0
lines changed

5 files changed

+424
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package metricstest
5+
6+
import (
7+
"sync"
8+
"testing"
9+
10+
"github.com/ava-labs/libevm/metrics"
11+
)
12+
13+
var metricsLock sync.Mutex
14+
15+
// WithMetrics enables [metrics.Enabled] for the test and prevents any other
16+
// tests with metrics from running concurrently.
17+
//
18+
// [metrics.Enabled] is restored to its original value during testing cleanup.
19+
func WithMetrics(t testing.TB) {
20+
metricsLock.Lock()
21+
initialValue := metrics.Enabled
22+
metrics.Enabled = true
23+
t.Cleanup(func() {
24+
metrics.Enabled = initialValue
25+
metricsLock.Unlock()
26+
})
27+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package prometheus_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/ava-labs/libevm/metrics"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// This test assumes that there are no imported packages that might change the
14+
// default value of [metrics.Enabled]. It is therefore in package
15+
// `prometheus_test` in case any other tests modify the variable. If any imports
16+
// here or in the implementation do actually do so then this test may have false
17+
// negatives.
18+
func TestMetricsEnabledByDefault(t *testing.T) {
19+
require.True(t, metrics.Enabled, "libevm/metrics.Enabled")
20+
require.IsType(t, (*metrics.StandardCounter)(nil), metrics.NewCounter(), "metrics.NewCounter() returned wrong type")
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package prometheus
5+
6+
import "github.com/ava-labs/libevm/metrics"
7+
8+
var _ Registry = metrics.Registry(nil)
9+
10+
type Registry interface {
11+
// Call the given function for each registered metric.
12+
Each(func(name string, metric any))
13+
// Get the metric by the given name or nil if none is registered.
14+
Get(name string) any
15+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package prometheus
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"slices"
10+
"strings"
11+
12+
"github.com/ava-labs/libevm/metrics"
13+
"github.com/prometheus/client_golang/prometheus"
14+
15+
dto "github.com/prometheus/client_model/go"
16+
)
17+
18+
var (
19+
_ prometheus.Gatherer = (*Gatherer)(nil)
20+
21+
errMetricSkip = errors.New("metric skipped")
22+
errMetricTypeNotSupported = errors.New("metric type is not supported")
23+
quantiles = []float64{.5, .75, .95, .99, .999, .9999}
24+
pvShortPercent = []float64{50, 95, 99}
25+
)
26+
27+
// Gatherer implements the [prometheus.Gatherer] interface by gathering all
28+
// metrics from a [Registry].
29+
type Gatherer struct {
30+
registry Registry
31+
}
32+
33+
// Gather gathers metrics from the registry and converts them to
34+
// a slice of metric families.
35+
func (g *Gatherer) Gather() ([]*dto.MetricFamily, error) {
36+
// Gather and pre-sort the metrics to avoid random listings
37+
var names []string
38+
g.registry.Each(func(name string, _ any) {
39+
names = append(names, name)
40+
})
41+
slices.Sort(names)
42+
43+
var (
44+
mfs = make([]*dto.MetricFamily, 0, len(names))
45+
errs []error
46+
)
47+
for _, name := range names {
48+
mf, err := metricFamily(g.registry, name)
49+
switch {
50+
case err == nil:
51+
mfs = append(mfs, mf)
52+
case !errors.Is(err, errMetricSkip):
53+
errs = append(errs, err)
54+
}
55+
}
56+
57+
return mfs, errors.Join(errs...)
58+
}
59+
60+
// NewGatherer returns a [Gatherer] using the given registry.
61+
func NewGatherer(registry Registry) *Gatherer {
62+
return &Gatherer{
63+
registry: registry,
64+
}
65+
}
66+
67+
func metricFamily(registry Registry, name string) (mf *dto.MetricFamily, err error) {
68+
metric := registry.Get(name)
69+
name = strings.ReplaceAll(name, "/", "_")
70+
71+
switch m := metric.(type) {
72+
case metrics.NilCounter, metrics.NilCounterFloat64, metrics.NilEWMA,
73+
metrics.NilGauge, metrics.NilGaugeFloat64, metrics.NilGaugeInfo,
74+
metrics.NilHealthcheck, metrics.NilHistogram, metrics.NilMeter,
75+
metrics.NilResettingTimer, metrics.NilSample, metrics.NilTimer:
76+
return nil, fmt.Errorf("%w: %q metric is nil", errMetricSkip, name)
77+
case metrics.Counter:
78+
return &dto.MetricFamily{
79+
Name: &name,
80+
Type: dto.MetricType_COUNTER.Enum(),
81+
Metric: []*dto.Metric{{
82+
Counter: &dto.Counter{
83+
Value: ptrTo(float64(m.Snapshot().Count())),
84+
},
85+
}},
86+
}, nil
87+
case metrics.CounterFloat64:
88+
return &dto.MetricFamily{
89+
Name: &name,
90+
Type: dto.MetricType_COUNTER.Enum(),
91+
Metric: []*dto.Metric{{
92+
Counter: &dto.Counter{
93+
Value: ptrTo(m.Snapshot().Count()),
94+
},
95+
}},
96+
}, nil
97+
case metrics.Gauge:
98+
return &dto.MetricFamily{
99+
Name: &name,
100+
Type: dto.MetricType_GAUGE.Enum(),
101+
Metric: []*dto.Metric{{
102+
Gauge: &dto.Gauge{
103+
Value: ptrTo(float64(m.Snapshot().Value())),
104+
},
105+
}},
106+
}, nil
107+
case metrics.GaugeFloat64:
108+
return &dto.MetricFamily{
109+
Name: &name,
110+
Type: dto.MetricType_GAUGE.Enum(),
111+
Metric: []*dto.Metric{{
112+
Gauge: &dto.Gauge{
113+
Value: ptrTo(m.Snapshot().Value()),
114+
},
115+
}},
116+
}, nil
117+
case metrics.GaugeInfo:
118+
return nil, fmt.Errorf("%w: %q is a %T", errMetricSkip, name, m)
119+
case metrics.Histogram:
120+
snapshot := m.Snapshot()
121+
thresholds := snapshot.Percentiles(quantiles)
122+
dtoQuantiles := make([]*dto.Quantile, len(quantiles))
123+
for i := range thresholds {
124+
dtoQuantiles[i] = &dto.Quantile{
125+
Quantile: ptrTo(quantiles[i]),
126+
Value: ptrTo(thresholds[i]),
127+
}
128+
}
129+
return &dto.MetricFamily{
130+
Name: &name,
131+
Type: dto.MetricType_SUMMARY.Enum(),
132+
Metric: []*dto.Metric{{
133+
Summary: &dto.Summary{
134+
SampleCount: ptrTo(uint64(snapshot.Count())),
135+
SampleSum: ptrTo(float64(snapshot.Sum())),
136+
Quantile: dtoQuantiles,
137+
},
138+
}},
139+
}, nil
140+
case metrics.Meter:
141+
return &dto.MetricFamily{
142+
Name: &name,
143+
Type: dto.MetricType_GAUGE.Enum(),
144+
Metric: []*dto.Metric{{
145+
Gauge: &dto.Gauge{
146+
Value: ptrTo(float64(m.Snapshot().Count())),
147+
},
148+
}},
149+
}, nil
150+
case metrics.Timer:
151+
snapshot := m.Snapshot()
152+
thresholds := snapshot.Percentiles(quantiles)
153+
dtoQuantiles := make([]*dto.Quantile, len(quantiles))
154+
for i := range thresholds {
155+
dtoQuantiles[i] = &dto.Quantile{
156+
Quantile: ptrTo(quantiles[i]),
157+
Value: ptrTo(thresholds[i]),
158+
}
159+
}
160+
return &dto.MetricFamily{
161+
Name: &name,
162+
Type: dto.MetricType_SUMMARY.Enum(),
163+
Metric: []*dto.Metric{{
164+
Summary: &dto.Summary{
165+
SampleCount: ptrTo(uint64(snapshot.Count())),
166+
SampleSum: ptrTo(float64(snapshot.Sum())),
167+
Quantile: dtoQuantiles,
168+
},
169+
}},
170+
}, nil
171+
case metrics.ResettingTimer:
172+
snapshot := m.Snapshot()
173+
thresholds := snapshot.Percentiles(pvShortPercent)
174+
dtoQuantiles := make([]*dto.Quantile, len(pvShortPercent))
175+
for i := range pvShortPercent {
176+
dtoQuantiles[i] = &dto.Quantile{
177+
Quantile: ptrTo(pvShortPercent[i]),
178+
Value: ptrTo(thresholds[i]),
179+
}
180+
}
181+
count := snapshot.Count()
182+
return &dto.MetricFamily{
183+
Name: &name,
184+
Type: dto.MetricType_SUMMARY.Enum(),
185+
Metric: []*dto.Metric{{
186+
Summary: &dto.Summary{
187+
SampleCount: ptrTo(uint64(count)),
188+
SampleSum: ptrTo(float64(count) * snapshot.Mean()),
189+
Quantile: dtoQuantiles,
190+
},
191+
}},
192+
}, nil
193+
default:
194+
return nil, fmt.Errorf("%w: metric %q type %T", errMetricTypeNotSupported, name, metric)
195+
}
196+
}
197+
198+
func ptrTo[T any](x T) *T { return &x }

0 commit comments

Comments
 (0)