diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 1a3de04bd4..4e6caf9a4a 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -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 } diff --git a/core/blockchain.go b/core/blockchain.go index c579123c0e..6790cdb28c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -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. diff --git a/metrics/exp/exp.go b/metrics/exp/exp.go index 2b04eeab27..93150fd4cf 100644 --- a/metrics/exp/exp.go +++ b/metrics/exp/exp.go @@ -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: diff --git a/metrics/gauge_info.go b/metrics/gauge_info.go new file mode 100644 index 0000000000..18d3cbae9f --- /dev/null +++ b/metrics/gauge_info.go @@ -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 +} diff --git a/metrics/gauge_info_test.go b/metrics/gauge_info_test.go new file mode 100644 index 0000000000..c02bcd50e9 --- /dev/null +++ b/metrics/gauge_info_test.go @@ -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}] +} diff --git a/metrics/graphite.go b/metrics/graphite.go index 29f72b0c41..f22538adf3 100644 --- a/metrics/graphite.go +++ b/metrics/graphite.go @@ -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) diff --git a/metrics/influxdb/influxdb.go b/metrics/influxdb/influxdb.go index 5dfbbab3ed..267ed4d61d 100644 --- a/metrics/influxdb/influxdb.go +++ b/metrics/influxdb/influxdb.go @@ -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 { diff --git a/metrics/librato/librato.go b/metrics/librato/librato.go index 3d45f4c7be..fa98595991 100644 --- a/metrics/librato/librato.go +++ b/metrics/librato/librato.go @@ -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) diff --git a/metrics/log.go b/metrics/log.go index d1ce627a83..d71a1c3d9c 100644 --- a/metrics/log.go +++ b/metrics/log.go @@ -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) diff --git a/metrics/opentsdb.go b/metrics/opentsdb.go index c9fd2e75d5..5140928373 100644 --- a/metrics/opentsdb.go +++ b/metrics/opentsdb.go @@ -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}) diff --git a/metrics/prometheus/collector.go b/metrics/prometheus/collector.go index 2bd9bf22cc..5a91bdb27a 100644 --- a/metrics/prometheus/collector.go +++ b/metrics/prometheus/collector.go @@ -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)) diff --git a/metrics/prometheus/collector_test.go b/metrics/prometheus/collector_test.go index ff87c8e765..ad1da15d43 100644 --- a/metrics/prometheus/collector_test.go +++ b/metrics/prometheus/collector_test.go @@ -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 diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go index d966fa9a86..81a717ccbf 100644 --- a/metrics/prometheus/prometheus.go +++ b/metrics/prometheus/prometheus.go @@ -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: diff --git a/metrics/registry.go b/metrics/registry.go index ec6e37c54f..b5c80ec2cd 100644 --- a/metrics/registry.go +++ b/metrics/registry.go @@ -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 } diff --git a/metrics/syslog.go b/metrics/syslog.go index f23b07e199..76c8490567 100644 --- a/metrics/syslog.go +++ b/metrics/syslog.go @@ -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())) diff --git a/metrics/writer.go b/metrics/writer.go index 82434e9d1d..b70de0bc81 100644 --- a/metrics/writer.go +++ b/metrics/writer.go @@ -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)