From e6c2e5b28cd9d9cb839c750738703541382a4d62 Mon Sep 17 00:00:00 2001 From: Quentin Mc Gaw Date: Mon, 13 Jan 2025 15:44:25 +0100 Subject: [PATCH] Migrate subnet-evm specific files back to metrics/prometheus - Bring over refactoring and fixes done in https://github.com/ava-labs/libevm/pull/103 - Bring over test refactoring done in https://github.com/ava-labs/libevm/pull/103 --- metrics/prometheus/interfaces.go | 10 ++ metrics/prometheus/prometheus.go | 193 ++++++++++++++++++++++++++ metrics/prometheus/prometheus_test.go | 83 +++++++++++ plugin/evm/vm.go | 4 +- 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 metrics/prometheus/interfaces.go create mode 100644 metrics/prometheus/prometheus.go create mode 100644 metrics/prometheus/prometheus_test.go diff --git a/metrics/prometheus/interfaces.go b/metrics/prometheus/interfaces.go new file mode 100644 index 0000000000..234627d862 --- /dev/null +++ b/metrics/prometheus/interfaces.go @@ -0,0 +1,10 @@ +// (c) 2025 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +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.go b/metrics/prometheus/prometheus.go new file mode 100644 index 0000000000..1061921da7 --- /dev/null +++ b/metrics/prometheus/prometheus.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 any) { + 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_test.go b/metrics/prometheus/prometheus_test.go new file mode 100644 index 0000000000..1b9a4a18b7 --- /dev/null +++ b/metrics/prometheus/prometheus_test.go @@ -0,0 +1,83 @@ +// (c) 2025 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package prometheus + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/metrics" +) + +func TestGatherer_Gather(t *testing.T) { + registry := metrics.NewRegistry() + register := func(t *testing.T, name string, collector any) { + t.Helper() + err := registry.Register(name, collector) + require.NoError(t, err) + } + + counter := metrics.NewCounter() + counter.Inc(12345) + register(t, "test/counter", counter) + + gauge := metrics.NewGauge() + gauge.Update(23456) + register(t, "test/gauge", gauge) + + gaugeFloat64 := metrics.NewGaugeFloat64() + gaugeFloat64.Update(34567.89) + register(t, "test/gauge_float64", gaugeFloat64) + + sample := metrics.NewUniformSample(1028) + histogram := metrics.NewHistogram(sample) + register(t, "test/histogram", histogram) + + meter := metrics.NewMeter() + t.Cleanup(meter.Stop) + meter.Mark(9999999) + register(t, "test/meter", meter) + + timer := metrics.NewTimer() + t.Cleanup(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) + register(t, "test/timer", timer) + + resettingTimer := metrics.NewResettingTimer() + register(t, "test/resetting_timer", resettingTimer) + resettingTimer.Update(time.Second) // must be after register call + + emptyResettingTimer := metrics.NewResettingTimer() + register(t, "test/empty_resetting_timer", emptyResettingTimer) + + emptyResettingTimer.Update(time.Second) // no effect because of snapshot below + register(t, "test/empty_resetting_timer_snapshot", emptyResettingTimer.Snapshot()) + + g := NewGatherer(registry) + + families, err := g.Gather() + require.NoError(t, err) + familyStrings := make([]string, len(families)) + for i := range families { + familyStrings[i] = families[i].String() + } + want := []string{ + `name:"test_counter" type:COUNTER metric: > `, + `name:"test_gauge" type:GAUGE metric: > `, + `name:"test_gauge_float64" type:GAUGE metric: > `, + `name:"test_histogram" type:SUMMARY metric: quantile: quantile: quantile: quantile: quantile: > > `, + `name:"test_meter" type:GAUGE metric: > `, + `name:"test_resetting_timer" type:SUMMARY metric: quantile: quantile: > > `, + `name:"test_timer" type:SUMMARY metric: quantile: quantile: quantile: quantile: quantile: > > `, + } + assert.Equal(t, want, familyStrings) +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index dbac17eb73..f367989838 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -34,6 +34,7 @@ import ( "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/coreth/eth" "github.com/ava-labs/coreth/eth/ethconfig" + corethprometheus "github.com/ava-labs/coreth/metrics/prometheus" "github.com/ava-labs/coreth/miner" "github.com/ava-labs/coreth/node" "github.com/ava-labs/coreth/params" @@ -45,7 +46,6 @@ import ( "github.com/ava-labs/coreth/triedb/hashdb" "github.com/ava-labs/coreth/utils" "github.com/ava-labs/libevm/metrics" - libevmPrometheus "github.com/ava-labs/libevm/metrics/prometheus" warpcontract "github.com/ava-labs/coreth/precompile/contracts/warp" "github.com/ava-labs/coreth/rpc" @@ -636,7 +636,7 @@ func (vm *VM) initializeMetrics() error { return nil } - gatherer := libevmPrometheus.NewGatherer(metrics.DefaultRegistry) + gatherer := corethprometheus.NewGatherer(metrics.DefaultRegistry) if err := vm.ctx.Metrics.Register(ethMetricsPrefix, gatherer); err != nil { return err }