Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b13eebf
add combined metrics package
JonathanOppenheimer Jul 29, 2025
ff47252
support GaugeInfo from subnet-evm
JonathanOppenheimer Jul 29, 2025
45f25b0
lint update
JonathanOppenheimer Jul 29, 2025
b1d230a
Merge branch 'master' into uplift-evm-metrics
JonathanOppenheimer Jul 29, 2025
846aa13
Merge branch 'master' into uplift-evm-metrics
JonathanOppenheimer Jul 29, 2025
9a08a58
Update vms/evm/metrics/metricstest/metrics.go
JonathanOppenheimer Jul 30, 2025
d5e51fe
Update vms/evm/metrics/metricstest/metrics.go
JonathanOppenheimer Jul 30, 2025
c162667
Update vms/evm/metrics/prometheus/enabled_test.go
JonathanOppenheimer Jul 30, 2025
1ce92b9
Update vms/evm/metrics/prometheus/prometheus_test.go
JonathanOppenheimer Jul 30, 2025
b82fcce
Update vms/evm/metrics/prometheus/interfaces.go
JonathanOppenheimer Jul 30, 2025
32038aa
Update vms/evm/metrics/prometheus/interfaces.go
JonathanOppenheimer Jul 30, 2025
6e7fced
Update vms/evm/metrics/prometheus/prometheus.go
JonathanOppenheimer Jul 30, 2025
7179722
Update vms/evm/metrics/prometheus/prometheus_test.go
JonathanOppenheimer Jul 30, 2025
6af4cd1
Update vms/evm/metrics/prometheus/prometheus_test.go
JonathanOppenheimer Jul 30, 2025
739310c
Maintain initial metrics.Enabled value
JonathanOppenheimer Jul 30, 2025
c90b0d6
Prometheus variable organization
JonathanOppenheimer Jul 30, 2025
c6b8a18
Refactor vms/evm/metrics/prometheus/prometheus_test.go
JonathanOppenheimer Jul 30, 2025
0999f21
Accumulate errors in Gather()
JonathanOppenheimer Jul 30, 2025
8af978f
Proper import order
JonathanOppenheimer Jul 31, 2025
83290c2
Add continue to swtich case
JonathanOppenheimer Jul 31, 2025
cff5859
Merge branch 'master' into uplift-evm-metrics
JonathanOppenheimer Jul 31, 2025
4ccc7ec
Merge branch 'master' into uplift-evm-metrics
JonathanOppenheimer Jul 31, 2025
19d4194
Update vms/evm/metrics/metricstest/metrics.go
JonathanOppenheimer Jul 31, 2025
8980eb1
Update vms/evm/metrics/prometheus/prometheus.go
JonathanOppenheimer Jul 31, 2025
2e5d18a
Update vms/evm/metrics/prometheus/prometheus.go
JonathanOppenheimer Jul 31, 2025
149e8c2
Update vms/evm/metrics/prometheus/prometheus.go
JonathanOppenheimer Jul 31, 2025
36b98e9
Fix TestGatherer_Gather test case
JonathanOppenheimer Jul 31, 2025
0a4e60a
lint error
JonathanOppenheimer Jul 31, 2025
2ce2d42
Address final nits (#4148)
StephenButtolph Jul 31, 2025
22ea450
Remove weird edge case
StephenButtolph Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions vms/evm/metrics/metricstest/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package metricstest

import (
"sync"
"testing"

"github.com/ava-labs/libevm/metrics"
)

var metricsLock sync.Mutex

// WithMetrics enables [metrics.Enabled] for the test and prevents any other
// tests with metrics from running concurrently.
//
// [metrics.Enabled] is restored to its original value during testing cleanup.
func WithMetrics(t testing.TB) {
metricsLock.Lock()
initialValue := metrics.Enabled
metrics.Enabled = true
t.Cleanup(func() {
metrics.Enabled = initialValue
metricsLock.Unlock()
})
}
21 changes: 21 additions & 0 deletions vms/evm/metrics/prometheus/enabled_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package prometheus_test

import (
"testing"

"github.com/ava-labs/libevm/metrics"
"github.com/stretchr/testify/require"
)

// This test assumes that there are no imported packages that might change the
// default value of [metrics.Enabled]. It is therefore in package
// `prometheus_test` in case any other tests modify the variable. If any imports
// here or in the implementation do actually do so then this test may have false
// negatives.
func TestMetricsEnabledByDefault(t *testing.T) {
require.True(t, metrics.Enabled, "libevm/metrics.Enabled")
require.IsType(t, (*metrics.StandardCounter)(nil), metrics.NewCounter(), "metrics.NewCounter() returned wrong type")
}
15 changes: 15 additions & 0 deletions vms/evm/metrics/prometheus/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package prometheus

import "github.com/ava-labs/libevm/metrics"

var _ Registry = metrics.Registry(nil)

type Registry interface {
// Call the given function for each registered metric.
Each(func(name string, metric any))
// Get the metric by the given name or nil if none is registered.
Get(name string) any
}
198 changes: 198 additions & 0 deletions vms/evm/metrics/prometheus/prometheus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package prometheus

import (
"errors"
"fmt"
"slices"
"strings"

"github.com/ava-labs/libevm/metrics"
"github.com/prometheus/client_golang/prometheus"

dto "github.com/prometheus/client_model/go"
)

var (
_ prometheus.Gatherer = (*Gatherer)(nil)

errMetricSkip = errors.New("metric skipped")
errMetricTypeNotSupported = errors.New("metric type is not supported")
quantiles = []float64{.5, .75, .95, .99, .999, .9999}
pvShortPercent = []float64{50, 95, 99}
)

// Gatherer implements the [prometheus.Gatherer] interface by gathering all
// metrics from a [Registry].
type Gatherer struct {
registry Registry
}

// Gather gathers metrics from the registry and converts them to
// a slice of metric families.
func (g *Gatherer) Gather() ([]*dto.MetricFamily, error) {
// Gather and pre-sort the metrics to avoid random listings
var names []string
g.registry.Each(func(name string, _ any) {
names = append(names, name)
})
slices.Sort(names)

var (
mfs = make([]*dto.MetricFamily, 0, len(names))
errs []error
)
for _, name := range names {
mf, err := metricFamily(g.registry, name)
switch {
case err == nil:
mfs = append(mfs, mf)
case !errors.Is(err, errMetricSkip):
errs = append(errs, err)
}
}

return mfs, errors.Join(errs...)
}

// NewGatherer returns a [Gatherer] using the given registry.
func NewGatherer(registry Registry) *Gatherer {
return &Gatherer{
registry: registry,
}
}

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.NilCounter, metrics.NilCounterFloat64, metrics.NilEWMA,
metrics.NilGauge, metrics.NilGaugeFloat64, metrics.NilGaugeInfo,
metrics.NilHealthcheck, metrics.NilHistogram, metrics.NilMeter,
metrics.NilResettingTimer, metrics.NilSample, metrics.NilTimer:
return nil, fmt.Errorf("%w: %q metric is nil", errMetricSkip, name)
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.GaugeInfo:
return nil, fmt.Errorf("%w: %q is a %T", errMetricSkip, name, m)
case metrics.Histogram:
snapshot := m.Snapshot()
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())),
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()
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())),
SampleSum: ptrTo(float64(snapshot.Sum())),
Quantile: dtoQuantiles,
},
}},
}, nil
case metrics.ResettingTimer:
snapshot := m.Snapshot()
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]),
}
}
count := snapshot.Count()
return &dto.MetricFamily{
Name: &name,
Type: dto.MetricType_SUMMARY.Enum(),
Metric: []*dto.Metric{{
Summary: &dto.Summary{
SampleCount: ptrTo(uint64(count)),
SampleSum: ptrTo(float64(count) * snapshot.Mean()),
Quantile: dtoQuantiles,
},
}},
}, nil
default:
return nil, fmt.Errorf("%w: metric %q type %T", errMetricTypeNotSupported, name, metric)
}
}

func ptrTo[T any](x T) *T { return &x }
Loading