From c0b8fd421f2c7f546e59b58c1c73ea8e81c1e285 Mon Sep 17 00:00:00 2001
From: Quentin Mc Gaw <quentin.mcgaw@avalabs.org>
Date: Mon, 13 Jan 2025 15:42:19 +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

---
 go.mod                                |   4 +-
 go.sum                                |   4 +-
 metrics/prometheus/interfaces.go      |  10 ++
 metrics/prometheus/prometheus.go      | 193 ++++++++++++++++++++++++++
 metrics/prometheus/prometheus_test.go |  91 ++++++++++++
 plugin/evm/vm.go                      |   4 +-
 6 files changed, 300 insertions(+), 6 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/go.mod b/go.mod
index c214e6a1c5..4ffb2e25ea 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/VictoriaMetrics/fastcache v1.12.1
 	github.com/antithesishq/antithesis-sdk-go v0.3.8
 	github.com/ava-labs/avalanchego v1.12.1
-	github.com/ava-labs/libevm v0.0.0-20250113110843-18c93de8be7f
+	github.com/ava-labs/libevm v1.13.14-0.1.0-rc.1
 	github.com/cespare/cp v0.1.0
 	github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233
 	github.com/davecgh/go-spew v1.1.1
@@ -31,6 +31,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/onsi/ginkgo/v2 v2.13.1
 	github.com/prometheus/client_golang v1.16.0
+	github.com/prometheus/client_model v0.3.0
 	github.com/spf13/cast v1.5.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.12.0
@@ -128,7 +129,6 @@ require (
 	github.com/pires/go-proxyproto v0.6.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index 2e61401a83..469ca3d9b6 100644
--- a/go.sum
+++ b/go.sum
@@ -64,8 +64,8 @@ github.com/ava-labs/avalanchego v1.12.1 h1:NL04K5+gciC2XqGZbDcIu0nuVApEddzc6Yyuj
 github.com/ava-labs/avalanchego v1.12.1/go.mod h1:xnVvN86jhxndxfS8e0U7v/0woyfx9BhX/feld7XDjDE=
 github.com/ava-labs/coreth v0.13.9-rc.2-encapsulate-signer h1:mRB03tLPUvgNko4nP4VwWQdiHeHaLHtdwsnqwxrsGec=
 github.com/ava-labs/coreth v0.13.9-rc.2-encapsulate-signer/go.mod h1:tqRAe+7bGLo2Rq/Ph4iYMSch72ag/Jn0DiDMDz1Xa9E=
-github.com/ava-labs/libevm v0.0.0-20250113110843-18c93de8be7f h1:KeKggoIyyF+o/GeGofo2+UO93WN7ulqMcVlP2K3iUzM=
-github.com/ava-labs/libevm v0.0.0-20250113110843-18c93de8be7f/go.mod h1:M8TCw2g1D5GBB7hu7g1F4aot5bRHGSxnBawNVmHE9Z0=
+github.com/ava-labs/libevm v1.13.14-0.1.0-rc.1 h1:ughW0E2DUNRnvwJYNU8zUSCUzIWdcOwyXSBpy7oauZE=
+github.com/ava-labs/libevm v1.13.14-0.1.0-rc.1/go.mod h1:yBctIV/wnxXTF38h95943jvpuk4aj07TrjbpoGor6LQ=
 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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..4eb7eb994a
--- /dev/null
+++ b/metrics/prometheus/prometheus_test.go
@@ -0,0 +1,91 @@
+// (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) {
+	metricsEnabled := metrics.Enabled
+	if !metricsEnabled {
+		metrics.Enabled = true
+		t.Cleanup(func() {
+			metrics.Enabled = false
+		})
+	}
+
+	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:<counter:<value:12345 > > `,
+		`name:"test_gauge" type:GAUGE metric:<gauge:<value:23456 > > `,
+		`name:"test_gauge_float64" type:GAUGE metric:<gauge:<value:34567.89 > > `,
+		`name:"test_histogram" type:SUMMARY metric:<summary:<sample_count:0 sample_sum:0 quantile:<quantile:0.5 value:0 > quantile:<quantile:0.75 value:0 > quantile:<quantile:0.95 value:0 > quantile:<quantile:0.99 value:0 > quantile:<quantile:0.999 value:0 > quantile:<quantile:0.9999 value:0 > > > `,
+		`name:"test_meter" type:GAUGE metric:<gauge:<value:9.999999e+06 > > `,
+		`name:"test_resetting_timer" type:SUMMARY metric:<summary:<sample_count:1 quantile:<quantile:50 value:1e+09 > quantile:<quantile:95 value:1e+09 > quantile:<quantile:99 value:1e+09 > > > `,
+		`name:"test_timer" type:SUMMARY metric:<summary:<sample_count:6 sample_sum:2.3e+08 quantile:<quantile:0.5 value:2.25e+07 > quantile:<quantile:0.75 value:4.8e+07 > quantile:<quantile:0.95 value:1.2e+08 > quantile:<quantile:0.99 value:1.2e+08 > quantile:<quantile:0.999 value:1.2e+08 > quantile:<quantile:0.9999 value:1.2e+08 > > > `,
+	}
+	assert.Equal(t, want, familyStrings)
+}
diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go
index 7c419edb6d..1edd6d305e 100644
--- a/plugin/evm/vm.go
+++ b/plugin/evm/vm.go
@@ -24,7 +24,6 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 
 	"github.com/ava-labs/libevm/metrics"
-	libevmprometheus "github.com/ava-labs/libevm/metrics/prometheus"
 	"github.com/ava-labs/subnet-evm/commontype"
 	"github.com/ava-labs/subnet-evm/consensus/dummy"
 	"github.com/ava-labs/subnet-evm/constants"
@@ -34,6 +33,7 @@ import (
 	"github.com/ava-labs/subnet-evm/core/types"
 	"github.com/ava-labs/subnet-evm/eth"
 	"github.com/ava-labs/subnet-evm/eth/ethconfig"
+	subnetevmprometheus "github.com/ava-labs/subnet-evm/metrics/prometheus"
 	"github.com/ava-labs/subnet-evm/miner"
 	"github.com/ava-labs/subnet-evm/node"
 	"github.com/ava-labs/subnet-evm/params"
@@ -555,7 +555,7 @@ func (vm *VM) initializeMetrics() error {
 		return nil
 	}
 
-	gatherer := libevmprometheus.NewGatherer(metrics.DefaultRegistry)
+	gatherer := subnetevmprometheus.NewGatherer(metrics.DefaultRegistry)
 	if err := vm.ctx.Metrics.Register(ethMetricsPrefix, gatherer); err != nil {
 		return err
 	}