diff --git a/metrics/prometheus/interfaces.libevm.go b/metrics/prometheus/interfaces.libevm.go new file mode 100644 index 00000000000..5bf57253ba9 --- /dev/null +++ b/metrics/prometheus/interfaces.libevm.go @@ -0,0 +1,8 @@ +package prometheus + +type Registry interface { + // Call the given function for each registered metric. + Each(func(string, any)) + // Get the metric by the given name or nil if none is registered. + Get(string) any +} diff --git a/metrics/prometheus/prometheus.libevm.go b/metrics/prometheus/prometheus.libevm.go new file mode 100644 index 00000000000..91625df4ff2 --- /dev/null +++ b/metrics/prometheus/prometheus.libevm.go @@ -0,0 +1,193 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package prometheus + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/ava-labs/libevm/metrics" + + dto "github.com/prometheus/client_model/go" +) + +type Gatherer struct { + registry Registry +} + +var _ prometheus.Gatherer = (*Gatherer)(nil) + +// NewGatherer returns a gatherer using the given registry. +// Note this gatherer implements the [prometheus.Gatherer] interface. +func NewGatherer(registry Registry) *Gatherer { + return &Gatherer{ + registry: registry, + } +} + +func (g *Gatherer) Gather() (mfs []*dto.MetricFamily, err error) { + // Gather and pre-sort the metrics to avoid random listings + var names []string + g.registry.Each(func(name string, i interface{}) { + names = append(names, name) + }) + sort.Strings(names) + + mfs = make([]*dto.MetricFamily, 0, len(names)) + for _, name := range names { + mf, err := metricFamily(g.registry, name) + if errors.Is(err, errMetricSkip) { + continue + } + mfs = append(mfs, mf) + } + + return mfs, nil +} + +var ( + errMetricSkip = errors.New("metric skipped") +) + +func ptrTo[T any](x T) *T { return &x } + +func metricFamily(registry Registry, name string) (mf *dto.MetricFamily, err error) { + metric := registry.Get(name) + name = strings.ReplaceAll(name, "/", "_") + + switch m := metric.(type) { + case metrics.Counter: + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{{ + Counter: &dto.Counter{ + Value: ptrTo(float64(m.Snapshot().Count())), + }, + }}, + }, nil + case metrics.CounterFloat64: + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{{ + Counter: &dto.Counter{ + Value: ptrTo(m.Snapshot().Count()), + }, + }}, + }, nil + case metrics.Gauge: + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_GAUGE.Enum(), + Metric: []*dto.Metric{{ + Gauge: &dto.Gauge{ + Value: ptrTo(float64(m.Snapshot().Value())), + }, + }}, + }, nil + case metrics.GaugeFloat64: + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_GAUGE.Enum(), + Metric: []*dto.Metric{{ + Gauge: &dto.Gauge{ + Value: ptrTo(m.Snapshot().Value()), + }, + }}, + }, nil + case metrics.Histogram: + snapshot := m.Snapshot() + + quantiles := []float64{.5, .75, .95, .99, .999, .9999} + thresholds := snapshot.Percentiles(quantiles) + dtoQuantiles := make([]*dto.Quantile, len(quantiles)) + for i := range thresholds { + dtoQuantiles[i] = &dto.Quantile{ + Quantile: ptrTo(quantiles[i]), + Value: ptrTo(thresholds[i]), + } + } + + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{{ + Summary: &dto.Summary{ + SampleCount: ptrTo(uint64(snapshot.Count())), //nolint:gosec + SampleSum: ptrTo(float64(snapshot.Sum())), + Quantile: dtoQuantiles, + }, + }}, + }, nil + case metrics.Meter: + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_GAUGE.Enum(), + Metric: []*dto.Metric{{ + Gauge: &dto.Gauge{ + Value: ptrTo(float64(m.Snapshot().Count())), + }, + }}, + }, nil + case metrics.Timer: + snapshot := m.Snapshot() + + quantiles := []float64{.5, .75, .95, .99, .999, .9999} + thresholds := snapshot.Percentiles(quantiles) + dtoQuantiles := make([]*dto.Quantile, len(quantiles)) + for i := range thresholds { + dtoQuantiles[i] = &dto.Quantile{ + Quantile: ptrTo(quantiles[i]), + Value: ptrTo(thresholds[i]), + } + } + + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{{ + Summary: &dto.Summary{ + SampleCount: ptrTo(uint64(snapshot.Count())), //nolint:gosec + SampleSum: ptrTo(float64(snapshot.Sum())), + Quantile: dtoQuantiles, + }, + }}, + }, nil + case metrics.ResettingTimer: + snapshot := m.Snapshot() + if snapshot.Count() == 0 { + return nil, fmt.Errorf("%w: resetting timer metric count is zero", errMetricSkip) + } + + pvShortPercent := []float64{50, 95, 99} + thresholds := snapshot.Percentiles(pvShortPercent) + dtoQuantiles := make([]*dto.Quantile, len(pvShortPercent)) + for i := range pvShortPercent { + dtoQuantiles[i] = &dto.Quantile{ + Quantile: ptrTo(pvShortPercent[i]), + Value: ptrTo(thresholds[i]), + } + } + + return &dto.MetricFamily{ + Name: &name, + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{{ + Summary: &dto.Summary{ + SampleCount: ptrTo(uint64(snapshot.Count())), //nolint:gosec + // TODO: do we need to specify SampleSum here? and if so + // what should that be? + Quantile: dtoQuantiles, + }, + }}, + }, nil + default: + return nil, fmt.Errorf("metric type is not supported: %T", metric) + } +} diff --git a/metrics/prometheus/prometheus.libevm_test.go b/metrics/prometheus/prometheus.libevm_test.go new file mode 100644 index 00000000000..1293e25253c --- /dev/null +++ b/metrics/prometheus/prometheus.libevm_test.go @@ -0,0 +1,87 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package prometheus + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/metrics" +) + +func TestGatherer(t *testing.T) { + registry := metrics.NewRegistry() + + counter := metrics.NewCounter() + counter.Inc(12345) + + err := registry.Register("test/counter", counter) + require.NoError(t, err) + + gauge := metrics.NewGauge() + gauge.Update(23456) + + err = registry.Register("test/gauge", gauge) + require.NoError(t, err) + + gaugeFloat64 := metrics.NewGaugeFloat64() + gaugeFloat64.Update(34567.89) + + err = registry.Register("test/gauge_float64", gaugeFloat64) + require.NoError(t, err) + + sample := metrics.NewUniformSample(1028) + histogram := metrics.NewHistogram(sample) + + err = registry.Register("test/histogram", histogram) + require.NoError(t, err) + + meter := metrics.NewMeter() + defer meter.Stop() + meter.Mark(9999999) + + err = registry.Register("test/meter", meter) + require.NoError(t, err) + + timer := metrics.NewTimer() + defer timer.Stop() + timer.Update(20 * time.Millisecond) + timer.Update(21 * time.Millisecond) + timer.Update(22 * time.Millisecond) + timer.Update(120 * time.Millisecond) + timer.Update(23 * time.Millisecond) + timer.Update(24 * time.Millisecond) + + err = registry.Register("test/timer", timer) + require.NoError(t, err) + + resettingTimer := metrics.NewResettingTimer() + resettingTimer.Update(10 * time.Millisecond) + resettingTimer.Update(11 * time.Millisecond) + resettingTimer.Update(12 * time.Millisecond) + resettingTimer.Update(120 * time.Millisecond) + resettingTimer.Update(13 * time.Millisecond) + resettingTimer.Update(14 * time.Millisecond) + + err = registry.Register("test/resetting_timer", resettingTimer) + require.NoError(t, err) + + err = registry.Register("test/resetting_timer_snapshot", resettingTimer.Snapshot()) + require.NoError(t, err) + + emptyResettingTimer := metrics.NewResettingTimer() + + err = registry.Register("test/empty_resetting_timer", emptyResettingTimer) + require.NoError(t, err) + + err = registry.Register("test/empty_resetting_timer_snapshot", emptyResettingTimer.Snapshot()) + require.NoError(t, err) + + g := NewGatherer(registry) + + _, err = g.Gather() + require.NoError(t, err) +}