metrics/*,cmd/geth,core/blockchain: Implemented support for informational metrics

- Created GaugeInfo metrics type for registering informational metrics.
- Updated all related metrics modules to support the new GaugeInfo type.
- Registered chain/info GaugeInfo with the chain_id value.
- Registered geth/info GaugeInfo with system and build parameters.

Implements #21783
This commit is contained in:
jorgeacortes 2022-05-13 20:05:56 +02:00 committed by Martin Holst Swende
parent d4e345c7d4
commit 44a7ea23f5
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
16 changed files with 409 additions and 1 deletions

View file

@ -22,6 +22,8 @@ import (
"fmt"
"os"
"reflect"
"runtime"
"strconv"
"unicode"
"github.com/urfave/cli/v2"
@ -35,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/version"
@ -161,6 +164,24 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
}
applyMetricConfig(ctx, &cfg)
// Create Info Gauge with geth system and build information
gethInfoGauge := metrics.NewRegisteredGaugeInfo("geth/info", nil)
protocolVersions := ""
for idx, val := range eth.ProtocolVersions {
protocolVersions += strconv.FormatUint(uint64(val), 10)
if idx < len(eth.ProtocolVersions)-1 {
protocolVersions += ","
}
}
gethInfo := metrics.GaugeInfoValue{
metrics.NewGaugeInfoEntry("version", params.VersionWithMeta),
metrics.NewGaugeInfoEntry("arch", runtime.GOARCH),
metrics.NewGaugeInfoEntry("os", runtime.GOOS),
metrics.NewGaugeInfoEntry("commit", gitCommit),
metrics.NewGaugeInfoEntry("protocol_versions", protocolVersions),
}
gethInfoGauge.Update(gethInfo)
return stack, cfg
}

View file

@ -60,6 +60,8 @@ var (
headFinalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil)
headSafeBlockGauge = metrics.NewRegisteredGauge("chain/head/safe", nil)
chainInfoGauge = metrics.NewRegisteredGaugeInfo("chain/info", nil)
accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil)
accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil)
accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil)
@ -322,6 +324,12 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
bc.currentFinalBlock.Store(nil)
bc.currentSafeBlock.Store(nil)
// Update chain info data metrics
chainInfoGauge.Update(
metrics.GaugeInfoValue{
{Key: "chain_id", Val: bc.chainConfig.ChainID.String()},
})
// If Geth is initialized with an external ancient store, re-initialize the
// missing chain indexes and chain flags. This procedure can survive crash
// and can be resumed in next restart since chain flags are updated in last step.

View file

@ -95,6 +95,20 @@ func (exp *exp) getFloat(name string) *expvar.Float {
return v
}
func (exp *exp) getInfo(name string) *expvar.String {
var v *expvar.String
exp.expvarLock.Lock()
p := expvar.Get(name)
if p != nil {
v = p.(*expvar.String)
} else {
v = new(expvar.String)
expvar.Publish(name, v)
}
exp.expvarLock.Unlock()
return v
}
func (exp *exp) publishCounter(name string, metric metrics.Counter) {
v := exp.getInt(name)
v.Set(metric.Count())
@ -113,6 +127,10 @@ func (exp *exp) publishGaugeFloat64(name string, metric metrics.GaugeFloat64) {
exp.getFloat(name).Set(metric.Value())
}
func (exp *exp) publishGaugeInfo(name string, metric metrics.GaugeInfo) {
exp.getInfo(name).Set(fmt.Sprintf("%s", metric.Value()))
}
func (exp *exp) publishHistogram(name string, metric metrics.Histogram) {
h := metric.Snapshot()
ps := h.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})
@ -178,6 +196,8 @@ func (exp *exp) syncToExpvar() {
exp.publishGauge(name, i)
case metrics.GaugeFloat64:
exp.publishGaugeFloat64(name, i)
case metrics.GaugeInfo:
exp.publishGaugeInfo(name, i)
case metrics.Histogram:
exp.publishHistogram(name, i)
case metrics.Meter:

175
metrics/gauge_info.go Normal file
View file

@ -0,0 +1,175 @@
package metrics
import (
"sync"
)
// GaugeInfos hold a GaugeInfoValue value that can be set arbitrarily.
type GaugeInfo interface {
Snapshot() GaugeInfo
Update(GaugeInfoValue)
Value() GaugeInfoValue
ValueJsonString() string
}
type GaugeInfoEntry struct {
Key string
Val string
}
type GaugeInfoValue []GaugeInfoEntry
func NewGaugeInfoEntry(key string, val string) GaugeInfoEntry {
return GaugeInfoEntry{key, val}
}
// GetOrRegisterGaugeInfo returns an existing GaugeInfo or constructs and registers a
// new StandardGaugeInfo.
func GetOrRegisterGaugeInfo(name string, r Registry) GaugeInfo {
if nil == r {
r = DefaultRegistry
}
return r.GetOrRegister(name, NewGaugeInfo()).(GaugeInfo)
}
// NewGaugeInfo constructs a new StandardGaugeInfo.
func NewGaugeInfo() GaugeInfo {
if !Enabled {
return NilGaugeInfo{}
}
return &StandardGaugeInfo{
value: GaugeInfoValue{},
}
}
// NewRegisteredGaugeInfo constructs and registers a new StandardGaugeInfo.
func NewRegisteredGaugeInfo(name string, r Registry) GaugeInfo {
c := NewGaugeInfo()
if nil == r {
r = DefaultRegistry
}
r.Register(name, c)
return c
}
// NewFunctionalGauge constructs a new FunctionalGauge.
func NewFunctionalGaugeInfo(f func() GaugeInfoValue) GaugeInfo {
if !Enabled {
return NilGaugeInfo{}
}
return &FunctionalGaugeInfo{value: f}
}
// NewRegisteredFunctionalGauge constructs and registers a new StandardGauge.
func NewRegisteredFunctionalGaugeInfo(name string, r Registry, f func() GaugeInfoValue) GaugeInfo {
c := NewFunctionalGaugeInfo(f)
if nil == r {
r = DefaultRegistry
}
r.Register(name, c)
return c
}
// GaugeInfoSnapshot is a read-only copy of another GaugeInfo.
type GaugeInfoSnapshot GaugeInfoValue
// Snapshot returns the snapshot.
func (g GaugeInfoSnapshot) Snapshot() GaugeInfo { return g }
// Update panics.
func (GaugeInfoSnapshot) Update(GaugeInfoValue) {
panic("Update called on a GaugeInfoSnapshot")
}
// Value returns the value at the time the snapshot was taken.
func (g GaugeInfoSnapshot) Value() GaugeInfoValue { return GaugeInfoValue(g) }
// Value returns the value at the time the snapshot was taken in JSON string format.
func (g GaugeInfoSnapshot) ValueJsonString() string {
return gaugeInfoValueToJsonString(g.Value())
}
// NilGauge is a no-op Gauge.
type NilGaugeInfo struct{}
// Snapshot is a no-op.
func (NilGaugeInfo) Snapshot() GaugeInfo { return NilGaugeInfo{} }
// Update is a no-op.
func (NilGaugeInfo) Update(v GaugeInfoValue) {}
// Value is a no-op.
func (NilGaugeInfo) Value() GaugeInfoValue { return GaugeInfoValue{} }
// Value is a no-op.
func (NilGaugeInfo) ValueJsonString() string { return gaugeInfoValueToJsonString(GaugeInfoValue{}) }
// StandardGaugeInfo is the standard implementation of a GaugeInfo and uses
// sync.Mutex to manage a single string value.
type StandardGaugeInfo struct {
mutex sync.Mutex
value GaugeInfoValue
}
// Snapshot returns a read-only copy of the gauge.
func (g *StandardGaugeInfo) Snapshot() GaugeInfo {
return GaugeInfoSnapshot(g.Value())
}
// Update updates the gauge's value.
func (g *StandardGaugeInfo) Update(v GaugeInfoValue) {
g.mutex.Lock()
defer g.mutex.Unlock()
g.value = v
}
// Value returns the gauge's current value.
func (g *StandardGaugeInfo) Value() GaugeInfoValue {
g.mutex.Lock()
defer g.mutex.Unlock()
return g.value
}
// Value returns the gauge's current value in JSON string format.
func (g *StandardGaugeInfo) ValueJsonString() string {
g.mutex.Lock()
defer g.mutex.Unlock()
return gaugeInfoValueToJsonString(g.value)
}
// FunctionalGaugeInfo returns value from given function
type FunctionalGaugeInfo struct {
value func() GaugeInfoValue
}
// Value returns the gauge's current value.
func (g FunctionalGaugeInfo) Value() GaugeInfoValue {
return g.value()
}
// Value returns the gauge's current value in JSON string format
func (g FunctionalGaugeInfo) ValueJsonString() string {
return gaugeInfoValueToJsonString(g.value())
}
// Snapshot returns the snapshot.
func (g FunctionalGaugeInfo) Snapshot() GaugeInfo { return GaugeInfoSnapshot(g.Value()) }
// Update panics.
func (FunctionalGaugeInfo) Update(GaugeInfoValue) {
panic("Update called on a FunctionalGaugeInfo")
}
// Custom conversion to Json to avoid printing "Key" and "Val"
func gaugeInfoValueToJsonString(g GaugeInfoValue) string {
lastIdx := len(g) - 1
v := "{"
for idx, entry := range g {
v += "\"" + entry.Key + "\":\"" + entry.Val + "\""
if idx != lastIdx {
v += ","
}
}
v += "}"
return v
}

129
metrics/gauge_info_test.go Normal file
View file

@ -0,0 +1,129 @@
package metrics
import (
"fmt"
"strconv"
"testing"
)
func BenchmarkGuageInfo(b *testing.B) {
g := NewGaugeInfo()
b.ResetTimer()
for i := 0; i < b.N; i++ {
g.Update(GaugeInfoValue{
{"chain_id", string(rune(i))},
})
}
}
func TestGaugeInfo(t *testing.T) {
g := NewGaugeInfo()
g.Update(GaugeInfoValue{
{"chain_id", "5"},
},
)
expected := GaugeInfoValue{
{"chain_id", "5"},
}
for idx, v := range g.Value() {
if v.Key != expected[idx].Key || v.Val != expected[idx].Val {
t.Errorf("g.Value()[%v]: %v != %v\n", idx, v, expected[idx])
}
}
}
func TestGaugeInfoSnapshot(t *testing.T) {
g := NewGaugeInfo()
g.Update(GaugeInfoValue{
{"chain_id", "5"},
})
snapshot := g.Snapshot()
g.Update(GaugeInfoValue{
{"chain_id", "1"},
})
expected := GaugeInfoValue{
{"chain_id", "5"},
}
for idx, v := range snapshot.Value() {
if v.Key != expected[idx].Key || v.Val != expected[idx].Val {
t.Errorf("g.Value()[%v]: %v != %v\n", idx, v, expected[idx])
}
}
}
func TestGetOrRegisterGaugeInfo(t *testing.T) {
r := NewRegistry()
NewRegisteredGaugeInfo("foo", r).Update(GaugeInfoValue{
{"chain_id", "5"},
})
expected := GaugeInfoValue{
{"chain_id", "5"},
}
g := GetOrRegisterGaugeInfo("foo", r)
for idx, v := range g.Value() {
if v.Key != expected[idx].Key || v.Val != expected[idx].Val {
t.Fatal(g)
}
}
}
func TestFunctionalGaugeInfo(t *testing.T) {
info := GaugeInfoValue{
{"chain_id", "0"},
}
counter := 1
fg := NewFunctionalGaugeInfo(func() GaugeInfoValue {
info[0].Val = strconv.Itoa(counter)
counter++
return info
})
fg.Value()
fg.Value()
if info[0].Val != "2" {
t.Error("info[0].Val != \"2\" -> ", info[0].Val)
}
}
func TestGetOrRegisterFunctionalGaugeInfo(t *testing.T) {
r := NewRegistry()
NewRegisteredFunctionalGaugeInfo("foo", r, func() GaugeInfoValue {
return GaugeInfoValue{
{"chain_id", "5"},
}
})
expected := GaugeInfoValue{
{"chain_id", "5"},
}
g := GetOrRegisterGaugeInfo("foo", r)
for idx, v := range g.Value() {
if v.Key != expected[idx].Key || v.Val != expected[idx].Val {
t.Fatal(g)
}
}
}
func TestGaugeInfoValueJsonString(t *testing.T) {
g := NewGaugeInfo()
g.Update(GaugeInfoValue{
{"chain_id", "5"},
{"anotherKey", "any_string_value"},
{"third_key", "anything"},
},
)
expected := `{"chain_id":"5","anotherKey":"any_string_value","third_key":"anything"}`
got := g.ValueJsonString()
if got != expected {
t.Errorf("g.ValueToJsonString(): %s != %s\n", got, expected)
}
}
func ExampleGetOrRegisterGaugeInfo() {
m := "chain/info"
g := GetOrRegisterGaugeInfo(m, nil)
g.Update(GaugeInfoValue{
{"chain_id", "5"},
{"random_value", "10"},
{"chain_data", "356"},
})
fmt.Println(g.Value()) // Output: [{chain_id 5} {random_value 10} {chain_data 356}]
}

View file

@ -73,6 +73,8 @@ func graphite(c *GraphiteConfig) error {
fmt.Fprintf(w, "%s.%s.value %d %d\n", c.Prefix, name, metric.Value(), now)
case GaugeFloat64:
fmt.Fprintf(w, "%s.%s.value %f %d\n", c.Prefix, name, metric.Value(), now)
case GaugeInfo:
fmt.Fprintf(w, "%s.%s.value %s %d\n", c.Prefix, name, metric.ValueJsonString(), now)
case Histogram:
h := metric.Snapshot()
ps := h.Percentiles(c.Percentiles)

View file

@ -32,6 +32,13 @@ func readMeter(namespace, name string, i interface{}) (string, map[string]interf
"value": metric.Snapshot().Value(),
}
return measurement, fields
case metrics.GaugeInfo:
ms := metric.Snapshot()
measurement := fmt.Sprintf("%s%s.gauge", namespace, name)
fields := map[string]interface{}{
"value": ms.ValueJsonString(),
}
return measurement, fields
case metrics.Histogram:
ms := metric.Snapshot()
if ms.Count() <= 0 {

View file

@ -126,6 +126,10 @@ func (rep *Reporter) BuildRequest(now time.Time, r metrics.Registry) (snapshot B
measurement[Name] = name
measurement[Value] = m.Value()
snapshot.Gauges = append(snapshot.Gauges, measurement)
case metrics.GaugeInfo:
measurement[Name] = name
measurement[Value] = m.Value()
snapshot.Gauges = append(snapshot.Gauges, measurement)
case metrics.Histogram:
if m.Count() > 0 {
gauges := make([]Measurement, histogramGaugeCount)

View file

@ -33,6 +33,9 @@ func LogScaled(r Registry, freq time.Duration, scale time.Duration, l Logger) {
case GaugeFloat64:
l.Printf("gauge %s\n", name)
l.Printf(" value: %f\n", metric.Value())
case GaugeInfo:
l.Printf("gauge %s\n", name)
l.Printf(" value: %s\n", metric.Value())
case Healthcheck:
metric.Check()
l.Printf("healthcheck %s\n", name)

View file

@ -77,6 +77,8 @@ func openTSDB(c *OpenTSDBConfig) error {
fmt.Fprintf(w, "put %s.%s.value %d %d host=%s\n", c.Prefix, name, now, metric.Value(), shortHostname)
case GaugeFloat64:
fmt.Fprintf(w, "put %s.%s.value %d %f host=%s\n", c.Prefix, name, now, metric.Value(), shortHostname)
case GaugeInfo:
fmt.Fprintf(w, "put %s.%s.value %d %s host=%s\n", c.Prefix, name, now, metric.ValueJsonString(), shortHostname)
case Histogram:
h := metric.Snapshot()
ps := h.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})

View file

@ -62,6 +62,10 @@ func (c *collector) addGaugeFloat64(name string, m metrics.GaugeFloat64) {
c.writeGaugeCounter(name, m.Value())
}
func (c *collector) addGaugeInfo(name string, m metrics.GaugeInfo) {
c.writeGaugeInfo(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)
@ -102,6 +106,19 @@ func (c *collector) addResettingTimer(name string, m metrics.ResettingTimer) {
c.buff.WriteRune('\n')
}
func (c *collector) writeGaugeInfo(name string, value metrics.GaugeInfoValue) {
name = mutateKey(name)
c.buff.WriteString(fmt.Sprintf(typeGaugeTpl, name))
c.buff.WriteString(fmt.Sprintf("%s {", name))
for idx, entry := range value {
c.buff.WriteString(fmt.Sprintf("%s=\"%s\"", entry.Key, entry.Val))
if idx != len(value)-1 {
c.buff.WriteString(", ")
}
}
c.buff.WriteString("} 1 \n\n")
}
func (c *collector) writeGaugeCounter(name string, value interface{}) {
name = mutateKey(name)
c.buff.WriteString(fmt.Sprintf(typeGaugeTpl, name))

View file

@ -32,6 +32,16 @@ func TestCollector(t *testing.T) {
gaugeFloat64.Update(34567.89)
c.addGaugeFloat64("test/gauge_float64", gaugeFloat64)
gaugeInfo := metrics.NewGaugeInfo()
gaugeInfo.Update(metrics.GaugeInfoValue{
metrics.NewGaugeInfoEntry("version", "1.10.18-unstable"),
metrics.NewGaugeInfoEntry("arch", "amd64"),
metrics.NewGaugeInfoEntry("os", "linux"),
metrics.NewGaugeInfoEntry("commit", "7caa2d8163ae3132c1c2d6978c76610caee2d949"),
metrics.NewGaugeInfoEntry("protocol_versions", "64 65 66"),
})
c.addGaugeInfo("geth/info", gaugeInfo)
histogram := metrics.NewHistogram(&metrics.NilSample{})
c.addHistogram("test/histogram", histogram)
@ -74,6 +84,9 @@ test_gauge 23456
# TYPE test_gauge_float64 gauge
test_gauge_float64 34567.89
# TYPE geth_info gauge
geth_info {version="1.10.18-unstable", arch="amd64", os="linux", commit="7caa2d8163ae3132c1c2d6978c76610caee2d949", protocol_versions="64 65 66"} 1
# TYPE test_histogram_count counter
test_histogram_count 0

View file

@ -51,6 +51,8 @@ func Handler(reg metrics.Registry) http.Handler {
c.addGauge(name, m.Snapshot())
case metrics.GaugeFloat64:
c.addGaugeFloat64(name, m.Snapshot())
case metrics.GaugeInfo:
c.addGaugeInfo(name, m.Snapshot())
case metrics.Histogram:
c.addHistogram(name, m.Snapshot())
case metrics.Meter:

View file

@ -191,7 +191,7 @@ func (r *StandardRegistry) Unregister(name string) {
func (r *StandardRegistry) loadOrRegister(name string, i interface{}) (interface{}, bool, bool) {
switch i.(type) {
case Counter, CounterFloat64, Gauge, GaugeFloat64, Healthcheck, Histogram, Meter, Timer, ResettingTimer:
case Counter, CounterFloat64, Gauge, GaugeFloat64, GaugeInfo, Healthcheck, Histogram, Meter, Timer, ResettingTimer:
default:
return nil, false, false
}

View file

@ -23,6 +23,8 @@ func Syslog(r Registry, d time.Duration, w *syslog.Writer) {
w.Info(fmt.Sprintf("gauge %s: value: %d", name, metric.Value()))
case GaugeFloat64:
w.Info(fmt.Sprintf("gauge %s: value: %f", name, metric.Value()))
case GaugeInfo:
w.Info(fmt.Sprintf("gauge %s: value: %s", name, metric.Value()))
case Healthcheck:
metric.Check()
w.Info(fmt.Sprintf("healthcheck %s: error: %v", name, metric.Error()))

View file

@ -39,6 +39,9 @@ func WriteOnce(r Registry, w io.Writer) {
case GaugeFloat64:
fmt.Fprintf(w, "gauge %s\n", namedMetric.name)
fmt.Fprintf(w, " value: %f\n", metric.Value())
case GaugeInfo:
fmt.Fprintf(w, "gauge %s\n", namedMetric.name)
fmt.Fprintf(w, " value: %s\n", metric.ValueJsonString())
case Healthcheck:
metric.Check()
fmt.Fprintf(w, "healthcheck %s\n", namedMetric.name)