Skip to content

Commit

Permalink
Merge pull request #8 from c98tristan/feat/add-prometheus
Browse files Browse the repository at this point in the history
metrics: add prometheus collector
  • Loading branch information
c98tristan authored Dec 23, 2024
2 parents 4290b28 + d1a05e4 commit 5fff440
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 7 deletions.
6 changes: 3 additions & 3 deletions internal/debug/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ var Flags = []cli.Flag{
//vmoduleFlag,
//backtraceAtFlag,
//debugFlag,
//pprofFlag,
//pprofAddrFlag,
//pprofPortFlag,
pprofFlag,
pprofAddrFlag,
pprofPortFlag,
//memprofilerateFlag,
//blockprofilerateFlag,
//cpuprofileFlag,
Expand Down
16 changes: 16 additions & 0 deletions metrics/exp/exp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/http"
"sync"

"github.com/tomochain/tomochain/metrics/prometheus"

"github.com/tomochain/tomochain/metrics"
)

Expand Down Expand Up @@ -42,6 +44,7 @@ func Exp(r metrics.Registry) {
// http.HandleFunc("/debug/vars", e.expHandler)
// haven't found an elegant way, so just use a different endpoint
http.Handle("/debug/metrics", h)
http.Handle("/debug/metrics/prometheus", prometheus.Handler(r))
}

// ExpHandler will return an expvar powered metrics handler.
Expand Down Expand Up @@ -134,6 +137,17 @@ func (exp *exp) publishTimer(name string, metric metrics.Timer) {
exp.getFloat(name + ".mean-rate").Set(t.RateMean())
}

func (exp *exp) publishResettingTimer(name string, metric metrics.ResettingTimer) {
t := metric.Snapshot()
ps := t.Percentiles([]float64{50, 75, 95, 99})
exp.getInt(name + ".count").Set(int64(len(t.Values())))
exp.getFloat(name + ".mean").Set(t.Mean())
exp.getInt(name + ".50-percentile").Set(ps[0])
exp.getInt(name + ".75-percentile").Set(ps[1])
exp.getInt(name + ".95-percentile").Set(ps[2])
exp.getInt(name + ".99-percentile").Set(ps[3])
}

func (exp *exp) syncToExpvar() {
exp.registry.Each(func(name string, i interface{}) {
switch i.(type) {
Expand All @@ -149,6 +163,8 @@ func (exp *exp) syncToExpvar() {
exp.publishMeter(name, i.(metrics.Meter))
case metrics.Timer:
exp.publishTimer(name, i.(metrics.Timer))
case metrics.ResettingTimer:
exp.publishResettingTimer(name, i.(metrics.ResettingTimer))
default:
panic(fmt.Sprintf("unsupported type for '%s': %T", name, i))
}
Expand Down
115 changes: 115 additions & 0 deletions metrics/prometheus/collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package prometheus

import (
"bytes"
"fmt"
"strconv"
"strings"

"github.com/tomochain/tomochain/metrics"
)

var (
typeGaugeTpl = "# TYPE %s gauge\n"
typeCounterTpl = "# TYPE %s counter\n"
typeSummaryTpl = "# TYPE %s summary\n"
keyValueTpl = "%s %v\n\n"
keyQuantileTagValueTpl = "%s {quantile=\"%s\"} %v\n\n"
)

// collector is a collection of byte buffers that aggregate Prometheus reports
// for different metric types.
type collector struct {
buff *bytes.Buffer
}

// newCollector createa a new Prometheus metric aggregator.
func newCollector() *collector {
return &collector{
buff: &bytes.Buffer{},
}
}

func (c *collector) addCounter(name string, m metrics.Counter) {
c.writeGaugeCounter(name, m.Count())
}

func (c *collector) addGauge(name string, m metrics.Gauge) {
c.writeGaugeCounter(name, m.Value())
}

func (c *collector) addGaugeFloat64(name string, m metrics.GaugeFloat64) {
c.writeGaugeCounter(name, m.Value())
}

func (c *collector) addHistogram(name string, m metrics.Histogram) {
pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}
ps := m.Percentiles(pv)
c.writeSummaryCounter(name, m.Count())
for i := range pv {
c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i])
}
}

func (c *collector) addMeter(name string, m metrics.Meter) {
c.writeGaugeCounter(name, m.Count())
}

func (c *collector) addTimer(name string, m metrics.Timer) {
pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}
ps := m.Percentiles(pv)
c.writeSummaryCounter(name, m.Count())
for i := range pv {
c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i])
}
}

func (c *collector) addResettingTimer(name string, m metrics.ResettingTimer) {
if len(m.Values()) <= 0 {
return
}
ps := m.Percentiles([]float64{50, 95, 99})
val := m.Values()
c.writeSummaryCounter(name, len(val))
c.writeSummaryPercentile(name, "0.50", ps[0])
c.writeSummaryPercentile(name, "0.95", ps[1])
c.writeSummaryPercentile(name, "0.99", ps[2])
}

func (c *collector) writeGaugeCounter(name string, value interface{}) {
name = mutateKey(name)
c.buff.WriteString(fmt.Sprintf(typeGaugeTpl, name))
c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value))
}

func (c *collector) writeSummaryCounter(name string, value interface{}) {
name = mutateKey(name + "_count")
c.buff.WriteString(fmt.Sprintf(typeCounterTpl, name))
c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value))
}

func (c *collector) writeSummaryPercentile(name, p string, value interface{}) {
name = mutateKey(name)
c.buff.WriteString(fmt.Sprintf(typeSummaryTpl, name))
c.buff.WriteString(fmt.Sprintf(keyQuantileTagValueTpl, name, p, value))
}

func mutateKey(key string) string {
return strings.Replace(key, "/", "_", -1)
}
68 changes: 68 additions & 0 deletions metrics/prometheus/prometheus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// Package prometheus exposes go-metrics into a Prometheus format.
package prometheus

import (
"fmt"
"net/http"
"sort"

"github.com/tomochain/tomochain/log"
"github.com/tomochain/tomochain/metrics"
)

// Handler returns an HTTP handler which dump metrics in Prometheus format.
func Handler(reg metrics.Registry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Gather and pre-sort the metrics to avoid random listings
var names []string
reg.Each(func(name string, i interface{}) {
names = append(names, name)
})
sort.Strings(names)

// Aggregate all the metris into a Prometheus collector
c := newCollector()

for _, name := range names {
i := reg.Get(name)

switch m := i.(type) {
case metrics.Counter:
c.addCounter(name, m.Snapshot())
case metrics.Gauge:
c.addGauge(name, m.Snapshot())
case metrics.GaugeFloat64:
c.addGaugeFloat64(name, m.Snapshot())
case metrics.Histogram:
c.addHistogram(name, m.Snapshot())
case metrics.Meter:
c.addMeter(name, m.Snapshot())
case metrics.Timer:
c.addTimer(name, m.Snapshot())
case metrics.ResettingTimer:
c.addResettingTimer(name, m.Snapshot())
default:
log.Warn("Unknown Prometheus metric type", "type", fmt.Sprintf("%T", i))
}
}
w.Header().Add("Content-Type", "text/plain")
w.Header().Add("Content-Length", fmt.Sprint(c.buff.Len()))
w.Write(c.buff.Bytes())
})
}
8 changes: 6 additions & 2 deletions metrics/resetting_timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ type NilResettingTimer struct {
func (NilResettingTimer) Values() []int64 { return nil }

// Snapshot is a no-op.
func (NilResettingTimer) Snapshot() ResettingTimer { return NilResettingTimer{} }
func (NilResettingTimer) Snapshot() ResettingTimer {
return &ResettingTimerSnapshot{
values: []int64{},
}
}

// Time is a no-op.
func (NilResettingTimer) Time(func()) {}
Expand Down Expand Up @@ -210,7 +214,7 @@ func (t *ResettingTimerSnapshot) calc(percentiles []float64) {
// poor man's math.Round(x):
// math.Floor(x + 0.5)
indexOfPerc := int(math.Floor(((abs / 100.0) * float64(count)) + 0.5))
if pct >= 0 {
if pct >= 0 && indexOfPerc > 0 {
indexOfPerc -= 1 // index offset=0
}
thresholdBoundary = t.values[indexOfPerc]
Expand Down
110 changes: 110 additions & 0 deletions metrics/resetting_timer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,113 @@ func TestResettingTimer(t *testing.T) {
}
}
}

func TestResettingTimerWithFivePercentiles(t *testing.T) {
tests := []struct {
values []int64
start int
end int
wantP05 int64
wantP20 int64
wantP50 int64
wantP95 int64
wantP99 int64
wantMean float64
wantMin int64
wantMax int64
}{
{
values: []int64{},
start: 1,
end: 11,
wantP05: 1, wantP20: 2, wantP50: 5, wantP95: 10, wantP99: 10,
wantMin: 1, wantMax: 10, wantMean: 5.5,
},
{
values: []int64{},
start: 1,
end: 101,
wantP05: 5, wantP20: 20, wantP50: 50, wantP95: 95, wantP99: 99,
wantMin: 1, wantMax: 100, wantMean: 50.5,
},
{
values: []int64{1},
start: 0,
end: 0,
wantP05: 1, wantP20: 1, wantP50: 1, wantP95: 1, wantP99: 1,
wantMin: 1, wantMax: 1, wantMean: 1,
},
{
values: []int64{0},
start: 0,
end: 0,
wantP05: 0, wantP20: 0, wantP50: 0, wantP95: 0, wantP99: 0,
wantMin: 0, wantMax: 0, wantMean: 0,
},
{
values: []int64{},
start: 0,
end: 0,
wantP05: 0, wantP20: 0, wantP50: 0, wantP95: 0, wantP99: 0,
wantMin: 0, wantMax: 0, wantMean: 0,
},
{
values: []int64{1, 10},
start: 0,
end: 0,
wantP05: 1, wantP20: 1, wantP50: 1, wantP95: 10, wantP99: 10,
wantMin: 1, wantMax: 10, wantMean: 5.5,
},
}
for ind, tt := range tests {
timer := NewResettingTimer()

for i := tt.start; i < tt.end; i++ {
tt.values = append(tt.values, int64(i))
}

for _, v := range tt.values {
timer.Update(time.Duration(v))
}

snap := timer.Snapshot()

ps := snap.Percentiles([]float64{5, 20, 50, 95, 99})

val := snap.Values()

if len(val) > 0 {
if tt.wantMin != val[0] {
t.Fatalf("%d: min: got %d, want %d", ind, val[0], tt.wantMin)
}

if tt.wantMax != val[len(val)-1] {
t.Fatalf("%d: max: got %d, want %d", ind, val[len(val)-1], tt.wantMax)
}
}

if tt.wantMean != snap.Mean() {
t.Fatalf("%d: mean: got %.2f, want %.2f", ind, snap.Mean(), tt.wantMean)
}

if tt.wantP05 != ps[0] {
t.Fatalf("%d: p05: got %d, want %d", ind, ps[0], tt.wantP05)
}

if tt.wantP20 != ps[1] {
t.Fatalf("%d: p20: got %d, want %d", ind, ps[1], tt.wantP20)
}

if tt.wantP50 != ps[2] {
t.Fatalf("%d: p50: got %d, want %d", ind, ps[2], tt.wantP50)
}

if tt.wantP95 != ps[3] {
t.Fatalf("%d: p95: got %d, want %d", ind, ps[3], tt.wantP95)
}

if tt.wantP99 != ps[4] {
t.Fatalf("%d: p99: got %d, want %d", ind, ps[4], tt.wantP99)
}
}
}
Loading

0 comments on commit 5fff440

Please sign in to comment.