From 8e617977807c7aae27d0a6c4f1560df3a9c14ee3 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Sat, 26 Aug 2023 10:48:55 +0200 Subject: [PATCH] metrics: improve testing, integrate informational metrics --- cmd/geth/config.go | 38 +++--- core/blockchain.go | 5 +- metrics/exp/exp.go | 2 +- metrics/gauge_info.go | 47 ++----- metrics/gauge_info_test.go | 114 +++++----------- metrics/graphite.go | 2 +- metrics/influxdb/influxdb.go | 2 +- metrics/influxdb/influxdb_test.go | 141 ++++++++++++++++++++ metrics/influxdb/influxdbv1.go | 15 ++- metrics/influxdb/influxdbv2.go | 13 +- metrics/influxdb/testdata/influxdbv1.want | 6 + metrics/influxdb/testdata/influxdbv2.want | 6 + metrics/opentsdb.go | 27 ++-- metrics/opentsdb_test.go | 30 +++++ metrics/prometheus/collector.go | 15 ++- metrics/prometheus/collector_test.go | 111 ++++++++------- metrics/prometheus/testdata/prometheus.want | 48 +++++++ metrics/registry.go | 33 ++++- metrics/testdata/opentsb.want | 23 ++++ metrics/writer.go | 2 +- 20 files changed, 439 insertions(+), 241 deletions(-) create mode 100644 metrics/influxdb/influxdb_test.go create mode 100644 metrics/influxdb/testdata/influxdbv1.want create mode 100644 metrics/influxdb/testdata/influxdbv2.want create mode 100644 metrics/prometheus/testdata/prometheus.want create mode 100644 metrics/testdata/opentsb.want diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 4e6caf9a4a..2761ee7453 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -23,11 +23,9 @@ import ( "os" "reflect" "runtime" - "strconv" + "strings" "unicode" - "github.com/urfave/cli/v2" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/external" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -37,7 +35,6 @@ 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" @@ -46,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/naoina/toml" + "github.com/urfave/cli/v2" ) var ( @@ -164,24 +162,6 @@ 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 } @@ -198,6 +178,20 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } backend, eth := utils.RegisterEthService(stack, &cfg.Eth) + // Create gauge with geth system and build information + if eth != nil { // The 'eth' backend may be nil in light mode + var protos []string + for _, p := range eth.Protocols() { + protos = append(protos, fmt.Sprintf("%v/%d", p.Name, p.Version)) + } + metrics.NewRegisteredGaugeInfo("geth/info", nil).Update(metrics.GaugeInfoValue{ + "arch": runtime.GOARCH, + "os": runtime.GOOS, + "version": cfg.Node.Version, + "eth_protocols": strings.Join(protos, ","), + }) + } + // Configure log filter RPC API. filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth) diff --git a/core/blockchain.go b/core/blockchain.go index 6790cdb28c..3e9440fed7 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -325,10 +325,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.currentSafeBlock.Store(nil) // Update chain info data metrics - chainInfoGauge.Update( - metrics.GaugeInfoValue{ - {Key: "chain_id", Val: bc.chainConfig.ChainID.String()}, - }) + chainInfoGauge.Update(metrics.GaugeInfoValue{"chain_id": 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 diff --git a/metrics/exp/exp.go b/metrics/exp/exp.go index 93150fd4cf..9e850f96b2 100644 --- a/metrics/exp/exp.go +++ b/metrics/exp/exp.go @@ -128,7 +128,7 @@ func (exp *exp) publishGaugeFloat64(name string, metric metrics.GaugeFloat64) { } func (exp *exp) publishGaugeInfo(name string, metric metrics.GaugeInfo) { - exp.getInfo(name).Set(fmt.Sprintf("%s", metric.Value())) + exp.getInfo(name).Set(metric.Value().String()) } func (exp *exp) publishHistogram(name string, metric metrics.Histogram) { diff --git a/metrics/gauge_info.go b/metrics/gauge_info.go index 18d3cbae9f..f1b2075939 100644 --- a/metrics/gauge_info.go +++ b/metrics/gauge_info.go @@ -1,6 +1,7 @@ package metrics import ( + "encoding/json" "sync" ) @@ -9,18 +10,14 @@ type GaugeInfo interface { Snapshot() GaugeInfo Update(GaugeInfoValue) Value() GaugeInfoValue - ValueJsonString() string } -type GaugeInfoEntry struct { - Key string - Val string -} +// GaugeInfoValue is a mappng of (string) keys to (string) values +type GaugeInfoValue map[string]string -type GaugeInfoValue []GaugeInfoEntry - -func NewGaugeInfoEntry(key string, val string) GaugeInfoEntry { - return GaugeInfoEntry{key, val} +func (val GaugeInfoValue) String() string { + data, _ := json.Marshal(val) + return string(data) } // GetOrRegisterGaugeInfo returns an existing GaugeInfo or constructs and registers a @@ -84,11 +81,6 @@ func (GaugeInfoSnapshot) Update(GaugeInfoValue) { // 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{} @@ -101,9 +93,6 @@ 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 { @@ -130,13 +119,6 @@ func (g *StandardGaugeInfo) Value() GaugeInfoValue { 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 @@ -149,7 +131,8 @@ func (g FunctionalGaugeInfo) Value() GaugeInfoValue { // Value returns the gauge's current value in JSON string format func (g FunctionalGaugeInfo) ValueJsonString() string { - return gaugeInfoValueToJsonString(g.value()) + data, _ := json.Marshal(g.value()) + return string(data) } // Snapshot returns the snapshot. @@ -159,17 +142,3 @@ func (g FunctionalGaugeInfo) Snapshot() GaugeInfo { return GaugeInfoSnapshot(g.V 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 index c02bcd50e9..4227a6a85f 100644 --- a/metrics/gauge_info_test.go +++ b/metrics/gauge_info_test.go @@ -1,86 +1,62 @@ 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) { +func TestGaugeInfoJsonString(t *testing.T) { g := NewGaugeInfo() g.Update(GaugeInfoValue{ - {"chain_id", "5"}, + "chain_id": "5", + "anotherKey": "any_string_value", + "third_key": "anything", }, ) - 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]) - } + want := `{"anotherKey":"any_string_value","chain_id":"5","third_key":"anything"}` + if have := g.Value().String(); have != want { + t.Errorf("\nhave: %v\nwant: %v\n", have, want) } } 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"}, + g.Update(GaugeInfoValue{"value": "original"}) + snapshot := g.Snapshot() // Snapshot @chainid 5 + g.Update(GaugeInfoValue{"value": "updated"}) + // The 'g' should be updated + if have, want := g.Value().String(), `{"value":"updated"}`; have != want { + t.Errorf("\nhave: %v\nwant: %v\n", have, want) } - 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]) - } + // Snapshot should be unupdated + if have, want := snapshot.Value().String(), `{"value":"original"}`; have != want { + t.Errorf("\nhave: %v\nwant: %v\n", have, want) } } func TestGetOrRegisterGaugeInfo(t *testing.T) { r := NewRegistry() - NewRegisteredGaugeInfo("foo", r).Update(GaugeInfoValue{ - {"chain_id", "5"}, - }) - expected := GaugeInfoValue{ - {"chain_id", "5"}, - } + NewRegisteredGaugeInfo("foo", r).Update( + 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) - } + if have, want := g.Value().String(), `{"chain_id":"5"}`; have != want { + t.Errorf("have\n%v\nwant\n%v\n", have, want) } } func TestFunctionalGaugeInfo(t *testing.T) { - info := GaugeInfoValue{ - {"chain_id", "0"}, - } + info := GaugeInfoValue{"chain_id": "0"} counter := 1 + // A "functional" gauge invokes the method to obtain the value fg := NewFunctionalGaugeInfo(func() GaugeInfoValue { - info[0].Val = strconv.Itoa(counter) + info["chain_id"] = strconv.Itoa(counter) counter++ return info }) fg.Value() fg.Value() - if info[0].Val != "2" { - t.Error("info[0].Val != \"2\" -> ", info[0].Val) + if have, want := info["chain_id"], "2"; have != want { + t.Errorf("have %v want %v", have, want) } } @@ -88,42 +64,12 @@ func TestGetOrRegisterFunctionalGaugeInfo(t *testing.T) { r := NewRegistry() NewRegisteredFunctionalGaugeInfo("foo", r, func() GaugeInfoValue { return GaugeInfoValue{ - {"chain_id", "5"}, + "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) - } + want := `{"chain_id":"5"}` + have := GetOrRegisterGaugeInfo("foo", r).Value().String() + if have != want { + t.Errorf("have\n%v\nwant\n%v\n", have, want) } } - -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 f22538adf3..4e3dd3b3b8 100644 --- a/metrics/graphite.go +++ b/metrics/graphite.go @@ -74,7 +74,7 @@ func graphite(c *GraphiteConfig) error { 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) + fmt.Fprintf(w, "%s.%s.value %s %d\n", c.Prefix, name, metric.Value().String(), now) case Histogram: h := metric.Snapshot() ps := h.Percentiles(c.Percentiles) diff --git a/metrics/influxdb/influxdb.go b/metrics/influxdb/influxdb.go index 267ed4d61d..9354f1a633 100644 --- a/metrics/influxdb/influxdb.go +++ b/metrics/influxdb/influxdb.go @@ -36,7 +36,7 @@ func readMeter(namespace, name string, i interface{}) (string, map[string]interf ms := metric.Snapshot() measurement := fmt.Sprintf("%s%s.gauge", namespace, name) fields := map[string]interface{}{ - "value": ms.ValueJsonString(), + "value": ms.Value().String(), } return measurement, fields case metrics.Histogram: diff --git a/metrics/influxdb/influxdb_test.go b/metrics/influxdb/influxdb_test.go new file mode 100644 index 0000000000..70d278fe43 --- /dev/null +++ b/metrics/influxdb/influxdb_test.go @@ -0,0 +1,141 @@ +// Copyright 2023 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 . + +package influxdb + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/ethereum/go-ethereum/metrics" + influxdb2 "github.com/influxdata/influxdb-client-go/v2" +) + +func TestMain(m *testing.M) { + metrics.Enabled = true + os.Exit(m.Run()) +} + +func setupSampleRegistry(t *testing.T) metrics.Registry { + t.Helper() + r := metrics.NewOrderedRegistry() + metrics.NewRegisteredGaugeInfo("info", r).Update(metrics.GaugeInfoValue{ + "version": "1.10.18-unstable", + "arch": "amd64", + "os": "linux", + "commit": "7caa2d8163ae3132c1c2d6978c76610caee2d949", + "protocol_versions": "64 65 66", + }) + metrics.NewRegisteredGaugeFloat64("pi", r).Update(3.14) + metrics.NewRegisteredCounter("months", r).Inc(12) + metrics.NewRegisteredCounterFloat64("tau", r).Inc(1.57) + metrics.NewRegisteredMeter("elite", r).Mark(1337) + metrics.NewRegisteredTimer("second", r).Update(time.Second) + metrics.NewRegisteredCounterFloat64("tau", r).Inc(1.57) + metrics.NewRegisteredCounterFloat64("tau", r).Inc(1.57) + return r +} + +func TestExampleV1(t *testing.T) { + r := setupSampleRegistry(t) + var have, want string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + haveB, _ := io.ReadAll(r.Body) + have = string(haveB) + r.Body.Close() + })) + defer ts.Close() + u, _ := url.Parse(ts.URL) + rep := &reporter{ + reg: r, + url: *u, + namespace: "goth.", + } + if err := rep.makeClient(); err != nil { + t.Fatal(err) + } + if err := rep.send(978307200); err != nil { + t.Fatal(err) + } + if wantB, err := os.ReadFile("./testdata/influxdbv1.want"); err != nil { + t.Fatal(err) + } else { + want = string(wantB) + } + if have != want { + t.Errorf("\nhave:\n%v\nwant:\n%v\n", have, want) + t.Logf("have vs want:\n %v", findFirstDiffPos(have, want)) + } +} + +func TestExampleV2(t *testing.T) { + r := setupSampleRegistry(t) + var have, want string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + haveB, _ := io.ReadAll(r.Body) + have = string(haveB) + r.Body.Close() + })) + defer ts.Close() + + rep := &v2Reporter{ + reg: r, + endpoint: ts.URL, + namespace: "goth.", + } + rep.client = influxdb2.NewClient(rep.endpoint, rep.token) + defer rep.client.Close() + rep.write = rep.client.WriteAPI(rep.organization, rep.bucket) + + rep.send(978307200) + + if wantB, err := os.ReadFile("./testdata/influxdbv2.want"); err != nil { + t.Fatal(err) + } else { + want = string(wantB) + } + if have != want { + t.Errorf("\nhave:\n%v\nwant:\n%v\n", have, want) + t.Logf("have vs want:\n %v", findFirstDiffPos(have, want)) + } +} + +func findFirstDiffPos(a, b string) string { + x, y := []byte(a), []byte(b) + var res []byte + for i, ch := range x { + if i > len(y) { + res = append(res, ch) + res = append(res, fmt.Sprintf("<-- diff: %#x vs EOF", ch)...) + break + } + if ch != y[i] { + res = append(res, fmt.Sprintf("<-- diff: %#x (%c) vs %#x (%c)", ch, ch, y[i], y[i])...) + break + } + res = append(res, ch) + } + if len(res) > 100 { + res = res[len(res)-100:] + } + return string(res) +} diff --git a/metrics/influxdb/influxdbv1.go b/metrics/influxdb/influxdbv1.go index f65d30ef95..ac58280803 100644 --- a/metrics/influxdb/influxdbv1.go +++ b/metrics/influxdb/influxdbv1.go @@ -79,7 +79,7 @@ func InfluxDBWithTagsOnce(r metrics.Registry, url, database, username, password, return fmt.Errorf("unable to make InfluxDB client. err: %v", err) } - if err := rep.send(); err != nil { + if err := rep.send(0); err != nil { return fmt.Errorf("unable to send to InfluxDB. err: %v", err) } @@ -107,7 +107,7 @@ func (r *reporter) run() { for { select { case <-intervalTicker.C: - if err := r.send(); err != nil { + if err := r.send(0); err != nil { log.Warn("Unable to send to InfluxDB", "err", err) } case <-pingTicker.C: @@ -123,7 +123,9 @@ func (r *reporter) run() { } } -func (r *reporter) send() error { +// send sends the measurements. If provided tstamp is >0, it is used. Otherwise, +// a 'fresh' timestamp is used. +func (r *reporter) send(tstamp int64) error { bps, err := client.NewBatchPoints( client.BatchPointsConfig{ Database: r.database, @@ -132,7 +134,12 @@ func (r *reporter) send() error { return err } r.reg.Each(func(name string, i interface{}) { - now := time.Now() + var now time.Time + if tstamp <= 0 { + now = time.Now() + } else { + now = time.Unix(tstamp, 0) + } measurement, fields := readMeter(r.namespace, name, i) if fields == nil { return diff --git a/metrics/influxdb/influxdbv2.go b/metrics/influxdb/influxdbv2.go index 7984898f32..0be5137d5e 100644 --- a/metrics/influxdb/influxdbv2.go +++ b/metrics/influxdb/influxdbv2.go @@ -64,7 +64,7 @@ func (r *v2Reporter) run() { for { select { case <-intervalTicker.C: - r.send() + r.send(0) case <-pingTicker.C: _, err := r.client.Health(context.Background()) if err != nil { @@ -74,9 +74,16 @@ func (r *v2Reporter) run() { } } -func (r *v2Reporter) send() { +// send sends the measurements. If provided tstamp is >0, it is used. Otherwise, +// a 'fresh' timestamp is used. +func (r *v2Reporter) send(tstamp int64) { r.reg.Each(func(name string, i interface{}) { - now := time.Now() + var now time.Time + if tstamp <= 0 { + now = time.Now() + } else { + now = time.Unix(tstamp, 0) + } measurement, fields := readMeter(r.namespace, name, i) if fields == nil { return diff --git a/metrics/influxdb/testdata/influxdbv1.want b/metrics/influxdb/testdata/influxdbv1.want new file mode 100644 index 0000000000..d2b95ac4ac --- /dev/null +++ b/metrics/influxdb/testdata/influxdbv1.want @@ -0,0 +1,6 @@ +goth.elite.meter count=0i,m1=0,m15=0,m5=0,mean=0 978307200000000000 +goth.info.gauge value="{\"arch\":\"amd64\",\"commit\":\"7caa2d8163ae3132c1c2d6978c76610caee2d949\",\"os\":\"linux\",\"protocol_versions\":\"64 65 66\",\"version\":\"1.10.18-unstable\"}" 978307200000000000 +goth.months.count value=12i 978307200000000000 +goth.pi.gauge value=3.14 978307200000000000 +goth.second.timer count=1i,m1=0,m15=0,m5=0,max=1000000000i,mean=1000000000,meanrate=0,min=1000000000i,p50=1000000000,p75=1000000000,p95=1000000000,p99=1000000000,p999=1000000000,p9999=1000000000,stddev=0,variance=0 978307200000000000 +goth.tau.count value=1.57 978307200000000000 diff --git a/metrics/influxdb/testdata/influxdbv2.want b/metrics/influxdb/testdata/influxdbv2.want new file mode 100644 index 0000000000..d2b95ac4ac --- /dev/null +++ b/metrics/influxdb/testdata/influxdbv2.want @@ -0,0 +1,6 @@ +goth.elite.meter count=0i,m1=0,m15=0,m5=0,mean=0 978307200000000000 +goth.info.gauge value="{\"arch\":\"amd64\",\"commit\":\"7caa2d8163ae3132c1c2d6978c76610caee2d949\",\"os\":\"linux\",\"protocol_versions\":\"64 65 66\",\"version\":\"1.10.18-unstable\"}" 978307200000000000 +goth.months.count value=12i 978307200000000000 +goth.pi.gauge value=3.14 978307200000000000 +goth.second.timer count=1i,m1=0,m15=0,m5=0,max=1000000000i,mean=1000000000,meanrate=0,min=1000000000i,p50=1000000000,p75=1000000000,p95=1000000000,p99=1000000000,p999=1000000000,p9999=1000000000,stddev=0,variance=0 978307200000000000 +goth.tau.count value=1.57 978307200000000000 diff --git a/metrics/opentsdb.go b/metrics/opentsdb.go index 5140928373..4d2e209238 100644 --- a/metrics/opentsdb.go +++ b/metrics/opentsdb.go @@ -3,6 +3,7 @@ package metrics import ( "bufio" "fmt" + "io" "log" "net" "os" @@ -57,16 +58,10 @@ func getShortHostname() string { return shortHostName } -func openTSDB(c *OpenTSDBConfig) error { - shortHostname := getShortHostname() - now := time.Now().Unix() +// writeRegistry writes the registry-metrics on the opentsb format. +func (c *OpenTSDBConfig) writeRegistry(w io.Writer, now int64, shortHostname string) { du := float64(c.DurationUnit) - conn, err := net.DialTCP("tcp", nil, c.Addr) - if nil != err { - return err - } - defer conn.Close() - w := bufio.NewWriter(conn) + c.Registry.Each(func(name string, i interface{}) { switch metric := i.(type) { case Counter: @@ -78,7 +73,7 @@ func openTSDB(c *OpenTSDBConfig) error { 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) + fmt.Fprintf(w, "put %s.%s.value %d %s host=%s\n", c.Prefix, name, now, metric.Value().String(), shortHostname) case Histogram: h := metric.Snapshot() ps := h.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999}) @@ -117,7 +112,17 @@ func openTSDB(c *OpenTSDBConfig) error { fmt.Fprintf(w, "put %s.%s.fifteen-minute %d %.2f host=%s\n", c.Prefix, name, now, t.Rate15(), shortHostname) fmt.Fprintf(w, "put %s.%s.mean-rate %d %.2f host=%s\n", c.Prefix, name, now, t.RateMean(), shortHostname) } - w.Flush() }) +} + +func openTSDB(c *OpenTSDBConfig) error { + conn, err := net.DialTCP("tcp", nil, c.Addr) + if nil != err { + return err + } + defer conn.Close() + w := bufio.NewWriter(conn) + c.writeRegistry(w, time.Now().Unix(), getShortHostname()) + w.Flush() return nil } diff --git a/metrics/opentsdb_test.go b/metrics/opentsdb_test.go index c43728960e..c02b98af06 100644 --- a/metrics/opentsdb_test.go +++ b/metrics/opentsdb_test.go @@ -2,6 +2,9 @@ package metrics import ( "net" + "os" + "strings" + "testing" "time" ) @@ -19,3 +22,30 @@ func ExampleOpenTSDBWithConfig() { DurationUnit: time.Millisecond, }) } + +func TestExampleOpenTSB(t *testing.T) { + r := NewOrderedRegistry() + NewRegisteredGaugeInfo("foo", r).Update(GaugeInfoValue{"chain_id": "5"}) + NewRegisteredGaugeFloat64("pi", r).Update(3.14) + NewRegisteredCounter("months", r).Inc(12) + NewRegisteredCounterFloat64("tau", r).Inc(1.57) + NewRegisteredMeter("elite", r).Mark(1337) + NewRegisteredTimer("second", r).Update(time.Second) + NewRegisteredCounterFloat64("tau", r).Inc(1.57) + NewRegisteredCounterFloat64("tau", r).Inc(1.57) + + w := new(strings.Builder) + (&OpenTSDBConfig{ + Registry: r, + DurationUnit: time.Millisecond, + Prefix: "pre", + }).writeRegistry(w, 978307200, "hal9000") + + wantB, err := os.ReadFile("./testdata/opentsb.want") + if err != nil { + t.Fatal(err) + } + if have, want := w.String(), string(wantB); have != want { + t.Errorf("\nhave:\n%v\nwant:\n%v\n", have, want) + } +} diff --git a/metrics/prometheus/collector.go b/metrics/prometheus/collector.go index 5a91bdb27a..1fca7cfbef 100644 --- a/metrics/prometheus/collector.go +++ b/metrics/prometheus/collector.go @@ -19,6 +19,7 @@ package prometheus import ( "bytes" "fmt" + "sort" "strconv" "strings" @@ -109,14 +110,14 @@ func (c *collector) addResettingTimer(name string, m metrics.ResettingTimer) { 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(name) + c.buff.WriteString(" ") + var kvs []string + for k, v := range value { + kvs = append(kvs, fmt.Sprintf("%v=%q", k, v)) } - c.buff.WriteString("} 1 \n\n") + sort.Strings(kvs) + c.buff.WriteString(fmt.Sprintf("{%v} 1\n\n", strings.Join(kvs, ", "))) } func (c *collector) writeGaugeCounter(name string, value interface{}) { diff --git a/metrics/prometheus/collector_test.go b/metrics/prometheus/collector_test.go index ad1da15d43..f72a72a59f 100644 --- a/metrics/prometheus/collector_test.go +++ b/metrics/prometheus/collector_test.go @@ -1,6 +1,23 @@ +// Copyright 2023 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 . + package prometheus import ( + "fmt" "os" "testing" "time" @@ -34,11 +51,11 @@ func TestCollector(t *testing.T) { 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"), + "version": "1.10.18-unstable", + "arch": "amd64", + "os": "linux", + "commit": "7caa2d8163ae3132c1c2d6978c76610caee2d949", + "protocol_versions": "64 65 66", }) c.addGaugeInfo("geth/info", gaugeInfo) @@ -72,59 +89,37 @@ func TestCollector(t *testing.T) { emptyResettingTimer := metrics.NewResettingTimer().Snapshot() c.addResettingTimer("test/empty_resetting_timer", emptyResettingTimer) - const expectedOutput = `# TYPE test_counter gauge -test_counter 12345 - -# TYPE test_counter_float64 gauge -test_counter_float64 54321.98 - -# TYPE test_gauge gauge -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 - -# TYPE test_histogram summary -test_histogram {quantile="0.5"} 0 -test_histogram {quantile="0.75"} 0 -test_histogram {quantile="0.95"} 0 -test_histogram {quantile="0.99"} 0 -test_histogram {quantile="0.999"} 0 -test_histogram {quantile="0.9999"} 0 - -# TYPE test_meter gauge -test_meter 9999999 - -# TYPE test_timer_count counter -test_timer_count 6 - -# TYPE test_timer summary -test_timer {quantile="0.5"} 2.25e+07 -test_timer {quantile="0.75"} 4.8e+07 -test_timer {quantile="0.95"} 1.2e+08 -test_timer {quantile="0.99"} 1.2e+08 -test_timer {quantile="0.999"} 1.2e+08 -test_timer {quantile="0.9999"} 1.2e+08 - -# TYPE test_resetting_timer_count counter -test_resetting_timer_count 6 - -# TYPE test_resetting_timer summary -test_resetting_timer {quantile="0.50"} 12000000 -test_resetting_timer {quantile="0.95"} 120000000 -test_resetting_timer {quantile="0.99"} 120000000 - -` - exp := c.buff.String() - if exp != expectedOutput { - t.Log("Expected Output:\n", expectedOutput) - t.Log("Actual Output:\n", exp) + var want string + if wantB, err := os.ReadFile("./testdata/prometheus.want"); err != nil { + t.Fatal(err) + } else { + want = string(wantB) + } + have := c.buff.String() + if have != want { + t.Logf("have\n%v", have) + t.Logf("have vs want:\n %v", findFirstDiffPos(have, want)) t.Fatal("unexpected collector output") } } + +func findFirstDiffPos(a, b string) string { + x, y := []byte(a), []byte(b) + var res []byte + for i, ch := range x { + if i > len(y) { + res = append(res, ch) + res = append(res, fmt.Sprintf("<-- diff: %#x vs EOF", ch)...) + break + } + if ch != y[i] { + res = append(res, fmt.Sprintf("<-- diff: %#x (%c) vs %#x (%c)", ch, ch, y[i], y[i])...) + break + } + res = append(res, ch) + } + if len(res) > 100 { + res = res[len(res)-100:] + } + return string(res) +} diff --git a/metrics/prometheus/testdata/prometheus.want b/metrics/prometheus/testdata/prometheus.want new file mode 100644 index 0000000000..e158fa8308 --- /dev/null +++ b/metrics/prometheus/testdata/prometheus.want @@ -0,0 +1,48 @@ +# TYPE test_counter gauge +test_counter 12345 + +# TYPE test_counter_float64 gauge +test_counter_float64 54321.98 + +# TYPE test_gauge gauge +test_gauge 23456 + +# TYPE test_gauge_float64 gauge +test_gauge_float64 34567.89 + +# TYPE geth_info gauge +geth_info {arch="amd64", commit="7caa2d8163ae3132c1c2d6978c76610caee2d949", os="linux", protocol_versions="64 65 66", version="1.10.18-unstable"} 1 + +# TYPE test_histogram_count counter +test_histogram_count 0 + +# TYPE test_histogram summary +test_histogram {quantile="0.5"} 0 +test_histogram {quantile="0.75"} 0 +test_histogram {quantile="0.95"} 0 +test_histogram {quantile="0.99"} 0 +test_histogram {quantile="0.999"} 0 +test_histogram {quantile="0.9999"} 0 + +# TYPE test_meter gauge +test_meter 9999999 + +# TYPE test_timer_count counter +test_timer_count 6 + +# TYPE test_timer summary +test_timer {quantile="0.5"} 2.25e+07 +test_timer {quantile="0.75"} 4.8e+07 +test_timer {quantile="0.95"} 1.2e+08 +test_timer {quantile="0.99"} 1.2e+08 +test_timer {quantile="0.999"} 1.2e+08 +test_timer {quantile="0.9999"} 1.2e+08 + +# TYPE test_resetting_timer_count counter +test_resetting_timer_count 6 + +# TYPE test_resetting_timer summary +test_resetting_timer {quantile="0.50"} 12000000 +test_resetting_timer {quantile="0.95"} 120000000 +test_resetting_timer {quantile="0.99"} 120000000 + diff --git a/metrics/registry.go b/metrics/registry.go index b5c80ec2cd..66dbc890c0 100644 --- a/metrics/registry.go +++ b/metrics/registry.go @@ -3,6 +3,7 @@ package metrics import ( "fmt" "reflect" + "sort" "strings" "sync" ) @@ -47,17 +48,39 @@ type Registry interface { Unregister(string) } +type orderedRegistry struct { + StandardRegistry +} + +// Call the given function for each registered metric. +func (r *orderedRegistry) Each(f func(string, interface{})) { + var names []string + reg := r.registered() + for name := range reg { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + f(name, reg[name]) + } +} + +// NewRegistry creates a new registry. +func NewRegistry() Registry { + return new(StandardRegistry) +} + +// NewOrderedRegistry creates a new ordered registry (for testing). +func NewOrderedRegistry() Registry { + return new(orderedRegistry) +} + // The standard implementation of a Registry uses sync.map // of names to metrics. type StandardRegistry struct { metrics sync.Map } -// Create a new registry. -func NewRegistry() Registry { - return &StandardRegistry{} -} - // Call the given function for each registered metric. func (r *StandardRegistry) Each(f func(string, interface{})) { for name, i := range r.registered() { diff --git a/metrics/testdata/opentsb.want b/metrics/testdata/opentsb.want new file mode 100644 index 0000000000..c8e40a5250 --- /dev/null +++ b/metrics/testdata/opentsb.want @@ -0,0 +1,23 @@ +put pre.elite.count 978307200 0 host=hal9000 +put pre.elite.one-minute 978307200 0.00 host=hal9000 +put pre.elite.five-minute 978307200 0.00 host=hal9000 +put pre.elite.fifteen-minute 978307200 0.00 host=hal9000 +put pre.elite.mean 978307200 0.00 host=hal9000 +put pre.foo.value 978307200 {"chain_id":"5"} host=hal9000 +put pre.months.count 978307200 12 host=hal9000 +put pre.pi.value 978307200 3.140000 host=hal9000 +put pre.second.count 978307200 1 host=hal9000 +put pre.second.min 978307200 1000 host=hal9000 +put pre.second.max 978307200 1000 host=hal9000 +put pre.second.mean 978307200 1000.00 host=hal9000 +put pre.second.std-dev 978307200 0.00 host=hal9000 +put pre.second.50-percentile 978307200 1000.00 host=hal9000 +put pre.second.75-percentile 978307200 1000.00 host=hal9000 +put pre.second.95-percentile 978307200 1000.00 host=hal9000 +put pre.second.99-percentile 978307200 1000.00 host=hal9000 +put pre.second.999-percentile 978307200 1000.00 host=hal9000 +put pre.second.one-minute 978307200 0.00 host=hal9000 +put pre.second.five-minute 978307200 0.00 host=hal9000 +put pre.second.fifteen-minute 978307200 0.00 host=hal9000 +put pre.second.mean-rate 978307200 0.00 host=hal9000 +put pre.tau.count 978307200 1.570000 host=hal9000 diff --git a/metrics/writer.go b/metrics/writer.go index b70de0bc81..ec2e4f8c6a 100644 --- a/metrics/writer.go +++ b/metrics/writer.go @@ -41,7 +41,7 @@ func WriteOnce(r Registry, w io.Writer) { 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()) + fmt.Fprintf(w, " value: %s\n", metric.Value().String()) case Healthcheck: metric.Check() fmt.Fprintf(w, "healthcheck %s\n", namedMetric.name)