Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(metrics/prometheus): add files needed for coreth and subnet-evm #103

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions metrics/prometheus/interfaces.libevm.go
Original file line number Diff line number Diff line change
@@ -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
}
193 changes: 193 additions & 0 deletions metrics/prometheus/prometheus.libevm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// (c) 2025, Ava Labs, Inc. All rights reserved.

Check failure on line 1 in metrics/prometheus/prometheus.libevm.go

View workflow job for this annotation

GitHub Actions / lint

Actual: (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

Check failure on line 122 in metrics/prometheus/prometheus.libevm.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint:gosec` is unused for linter "gosec" (nolintlint)
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

Check failure on line 156 in metrics/prometheus/prometheus.libevm.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint:gosec` is unused for linter "gosec" (nolintlint)
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

Check failure on line 183 in metrics/prometheus/prometheus.libevm.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint:gosec` is unused for linter "gosec" (nolintlint)
// 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)
}
}
87 changes: 87 additions & 0 deletions metrics/prometheus/prometheus.libevm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// (c) 2025, Ava Labs, Inc. All rights reserved.

Check failure on line 1 in metrics/prometheus/prometheus.libevm_test.go

View workflow job for this annotation

GitHub Actions / lint

Actual: (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)
}
Loading