diff --git a/Makefile b/Makefile
index a0b970f76..aa12a721b 100644
--- a/Makefile
+++ b/Makefile
@@ -251,6 +251,14 @@ mockery:
go generate -run="./scripts/mockery_generate.sh" ./...
.PHONY: mockery
+###############################################################################
+### Metrics ###
+###############################################################################
+
+metrics:
+ go generate -run="go run github.com/tendermint/tendermint/scripts/metricsgen" ./...
+.PHONY: mockery
+
###############################################################################
### Local testnet using docker ###
###############################################################################
diff --git a/internal/state/indexer/metrics.gen.go b/internal/state/indexer/metrics.gen.go
new file mode 100644
index 000000000..8b079d8d5
--- /dev/null
+++ b/internal/state/indexer/metrics.gen.go
@@ -0,0 +1,51 @@
+// Code generated by metricsgen. DO NOT EDIT.
+
+package indexer
+
+import (
+ "github.com/go-kit/kit/metrics/discard"
+ prometheus "github.com/go-kit/kit/metrics/prometheus"
+ stdprometheus "github.com/prometheus/client_golang/prometheus"
+)
+
+func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics {
+ labels := []string{}
+ for i := 0; i < len(labelsAndValues); i += 2 {
+ labels = append(labels, labelsAndValues[i])
+ }
+ return &Metrics{
+ BlockEventsSeconds: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
+ Namespace: namespace,
+ Subsystem: MetricsSubsystem,
+ Name: "block_events_seconds",
+ Help: "Latency for indexing block events.",
+ }, labels).With(labelsAndValues...),
+ TxEventsSeconds: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
+ Namespace: namespace,
+ Subsystem: MetricsSubsystem,
+ Name: "tx_events_seconds",
+ Help: "Latency for indexing transaction events.",
+ }, labels).With(labelsAndValues...),
+ BlocksIndexed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{
+ Namespace: namespace,
+ Subsystem: MetricsSubsystem,
+ Name: "blocks_indexed",
+ Help: "Number of complete blocks indexed.",
+ }, labels).With(labelsAndValues...),
+ TransactionsIndexed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{
+ Namespace: namespace,
+ Subsystem: MetricsSubsystem,
+ Name: "transactions_indexed",
+ Help: "Number of transactions indexed.",
+ }, labels).With(labelsAndValues...),
+ }
+}
+
+func NopMetrics() *Metrics {
+ return &Metrics{
+ BlockEventsSeconds: discard.NewHistogram(),
+ TxEventsSeconds: discard.NewHistogram(),
+ BlocksIndexed: discard.NewCounter(),
+ TransactionsIndexed: discard.NewCounter(),
+ }
+}
diff --git a/internal/state/indexer/metrics.go b/internal/state/indexer/metrics.go
index aa64a4bb2..0b92b879e 100644
--- a/internal/state/indexer/metrics.go
+++ b/internal/state/indexer/metrics.go
@@ -2,12 +2,10 @@ package indexer
import (
"github.com/go-kit/kit/metrics"
- "github.com/go-kit/kit/metrics/discard"
-
- prometheus "github.com/go-kit/kit/metrics/prometheus"
- stdprometheus "github.com/prometheus/client_golang/prometheus"
)
+//go:generate go run github.com/tendermint/tendermint/scripts/metricsgen -struct=Metrics
+
// MetricsSubsystem is a the subsystem label for the indexer package.
const MetricsSubsystem = "indexer"
@@ -25,49 +23,3 @@ type Metrics struct {
// Number of transactions indexed.
TransactionsIndexed metrics.Counter
}
-
-// PrometheusMetrics returns Metrics build using Prometheus client library.
-// Optionally, labels can be provided along with their values ("foo",
-// "fooValue").
-func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics {
- labels := []string{}
- for i := 0; i < len(labelsAndValues); i += 2 {
- labels = append(labels, labelsAndValues[i])
- }
- return &Metrics{
- BlockEventsSeconds: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
- Namespace: namespace,
- Subsystem: MetricsSubsystem,
- Name: "block_events_seconds",
- Help: "Latency for indexing block events.",
- }, labels).With(labelsAndValues...),
- TxEventsSeconds: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
- Namespace: namespace,
- Subsystem: MetricsSubsystem,
- Name: "tx_events_seconds",
- Help: "Latency for indexing transaction events.",
- }, labels).With(labelsAndValues...),
- BlocksIndexed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{
- Namespace: namespace,
- Subsystem: MetricsSubsystem,
- Name: "blocks_indexed",
- Help: "Number of complete blocks indexed.",
- }, labels).With(labelsAndValues...),
- TransactionsIndexed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{
- Namespace: namespace,
- Subsystem: MetricsSubsystem,
- Name: "transactions_indexed",
- Help: "Number of transactions indexed.",
- }, labels).With(labelsAndValues...),
- }
-}
-
-// NopMetrics returns an indexer metrics stub that discards all samples.
-func NopMetrics() *Metrics {
- return &Metrics{
- BlockEventsSeconds: discard.NewHistogram(),
- TxEventsSeconds: discard.NewHistogram(),
- BlocksIndexed: discard.NewCounter(),
- TransactionsIndexed: discard.NewCounter(),
- }
-}
diff --git a/scripts/metricsgen/metricsgen.go b/scripts/metricsgen/metricsgen.go
new file mode 100644
index 000000000..a63fdeb99
--- /dev/null
+++ b/scripts/metricsgen/metricsgen.go
@@ -0,0 +1,314 @@
+// metricsgen is a code generation tool for creating constructors for Tendermint
+// metrics types.
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "io"
+ "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strings"
+ "text/template"
+)
+
+func init() {
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, `Usage: %[1]s -dir
-struct
+
+Generate constructors for the metrics type contained in the specified -dir
+Go directory. The tool creates a new file in the same directory as the specified directory
+containing the generated code.
+
+Options:
+`, filepath.Base(os.Args[0]))
+ flag.PrintDefaults()
+ }
+}
+
+const (
+ metricNameTag = "metricsgen_name"
+ labelsTag = "metricsgen_labels"
+ bucketTypeTag = "metricsgen_bucketsType"
+ bucketSizeTag = "metricsgen_bucketSizes"
+)
+
+var (
+ dir = flag.String("dir", ".", "Path to the directory containing the target package")
+ strct = flag.String("struct", "Metrics", "Struct to parse for metrics")
+)
+
+var bucketType = map[string]string{
+ "exprange": "stdprometheus.ExponentialBucketsRange",
+ "exp": "stdprometheus.ExponentialBuckets",
+ "lin": "stdprometheus.LinearBuckets",
+}
+
+var tmpl = template.Must(template.New("tmpl").Parse(`// Code generated by metricsgen. DO NOT EDIT.
+
+package {{ .Package }}
+
+import (
+ "github.com/go-kit/kit/metrics/discard"
+ prometheus "github.com/go-kit/kit/metrics/prometheus"
+ stdprometheus "github.com/prometheus/client_golang/prometheus"
+)
+
+func PrometheusMetrics(namespace string, labelsAndValues...string) *Metrics {
+ labels := []string{}
+ for i := 0; i < len(labelsAndValues); i += 2 {
+ labels = append(labels, labelsAndValues[i])
+ }
+ return &Metrics{
+ {{ range $metric := .ParsedMetrics }}
+ {{- $metric.FieldName }}: prometheus.New{{ $metric.TypeName }}From(stdprometheus.{{$metric.TypeName }}Opts{
+ Namespace: namespace,
+ Subsystem: MetricsSubsystem,
+ Name: "{{$metric.MetricName }}",
+ Help: "{{ $metric.Description }}",
+ {{ if ne $metric.HistogramOptions.BucketType "" }}
+ Buckets: {{ $metric.HistogramOptions.BucketType }}({{ $metric.HistogramOptions.BucketSizes }}),
+ {{ else if ne $metric.HistogramOptions.BucketSizes "" }}
+ Buckets: []float64{ {{ $metric.HistogramOptions.BucketSizes }} },
+ {{ end }}
+ {{- if eq (len $metric.Labels) 0 }}
+ }, labels).With(labelsAndValues...),
+ {{ else }}
+ }, append(labels, {{$metric.Labels | printf "%q" }})).With(labelsAndValues...),
+ {{- end }}
+ {{- end }}
+ }
+}
+
+
+func NopMetrics() *Metrics {
+ return &Metrics{
+ {{- range $metric := .ParsedMetrics }}
+ {{ $metric.FieldName }}: discard.New{{ $metric.TypeName }}(),
+ {{- end }}
+ }
+}
+`))
+
+// ParsedMetricField is the data parsed for a single field of a metric struct.
+type ParsedMetricField struct {
+ TypeName string
+ FieldName string
+ MetricName string
+ Description string
+ Labels string
+
+ HistogramOptions HistogramOpts
+}
+
+type HistogramOpts struct {
+ BucketType string
+ BucketSizes string
+}
+
+// TemplateData is all of the data required for rendering a metric file template.
+type TemplateData struct {
+ Package string
+ ParsedMetrics []ParsedMetricField
+}
+
+func main() {
+ flag.Parse()
+ if *strct == "" {
+ log.Fatal("You must specify a non-empty -struct")
+ }
+ if *dir == "" {
+ log.Fatal("You must specify a non-empty -dir")
+ }
+ td, err := ParseMetricsDir(*dir, *strct)
+ if err != nil {
+ log.Fatalf("Parsing file: %v", err)
+ }
+ out := filepath.Join(*dir, "metrics.gen.go")
+ f, err := os.Create(out)
+ if err != nil {
+ log.Fatalf("Opening file: %v", err)
+ }
+ err = GenerateMetricsFile(f, td)
+ if err != nil {
+ log.Fatalf("Generating code: %v", err)
+ }
+}
+
+// ParseMetricsDir parses the dir and scans for a struct matching structName,
+// ignoring all test files. ParseMetricsDir iterates the fields of the metrics
+// struct and builds a TemplateData using the data obtained from the abstract syntax tree.
+func ParseMetricsDir(dir string, structName string) (TemplateData, error) {
+ ignoreTestFiles := func(f fs.FileInfo) bool {
+ return !strings.Contains(f.Name(), "_test.go")
+ }
+ fs := token.NewFileSet()
+ d, err := parser.ParseDir(fs, dir, ignoreTestFiles, parser.ParseComments)
+ if err != nil {
+ return TemplateData{}, err
+ }
+ if len(d) > 1 {
+ return TemplateData{}, fmt.Errorf("multiple packages found in %s", dir)
+ }
+ if len(d) == 0 {
+ return TemplateData{}, fmt.Errorf("no go pacakges found in %s", dir)
+ }
+
+ // Grab the package name.
+ var pkgName string
+ var pkg *ast.Package
+ for pkgName, pkg = range d {
+ }
+ td := TemplateData{
+ Package: pkgName,
+ }
+ // Grab the metrics struct
+ m, err := fetchMetricsStruct(pkg.Files, structName)
+ if err != nil {
+ return TemplateData{}, err
+ }
+ for _, f := range m.Fields.List {
+ if !isMetric(f.Type) {
+ continue
+ }
+ pmf := parseMetricField(f)
+ td.ParsedMetrics = append(td.ParsedMetrics, pmf)
+ }
+
+ return td, err
+}
+
+// GenerateMetricsFile executes the metrics file template, writing the result
+// into the io.Writer.
+func GenerateMetricsFile(w io.Writer, td TemplateData) error {
+ b := []byte{}
+ buf := bytes.NewBuffer(b)
+ err := tmpl.Execute(buf, td)
+ if err != nil {
+ return err
+ }
+ b, err = format.Source(buf.Bytes())
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, bytes.NewBuffer(b))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func fetchMetricsStruct(files map[string]*ast.File, structName string) (*ast.StructType, error) {
+ var (
+ err error
+ st *ast.StructType
+ )
+ for _, file := range files {
+ if !ast.FilterFile(file, func(name string) bool {
+ return name == structName
+ }) {
+ continue
+ }
+ ast.Inspect(file, func(n ast.Node) bool {
+ switch f := n.(type) {
+ case *ast.TypeSpec:
+ if f.Name.Name == structName {
+ var ok bool
+ st, ok = f.Type.(*ast.StructType)
+ if !ok {
+ err = fmt.Errorf("found identifier for %q of wrong type", structName)
+ }
+ }
+ return false
+ default:
+ return true
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+ if st != nil {
+ return st, nil
+ }
+ }
+ return nil, fmt.Errorf("target struct %q not found in dir", structName)
+}
+
+func parseMetricField(f *ast.Field) ParsedMetricField {
+ var comment string
+ if f.Doc != nil {
+ for _, c := range f.Doc.List {
+ comment += strings.TrimPrefix(c.Text, "// ")
+ }
+ }
+ pmf := ParsedMetricField{
+ Description: comment,
+ MetricName: extractFieldName(f.Names[0].String(), f.Tag),
+ FieldName: f.Names[0].String(),
+ TypeName: extractTypeName(f.Type),
+ Labels: extractLabels(f.Tag),
+ }
+ if pmf.TypeName == "Histogram" {
+ pmf.HistogramOptions = extractHistogramOptions(f.Tag)
+ }
+ return pmf
+}
+
+func extractTypeName(e ast.Expr) string {
+ return strings.TrimPrefix(types.ExprString(e), "metrics.")
+}
+
+func isMetric(e ast.Expr) bool {
+ return strings.Contains(types.ExprString(e), "metrics.")
+}
+
+func extractLabels(bl *ast.BasicLit) string {
+ if bl != nil {
+ t := reflect.StructTag(strings.Trim(bl.Value, "`"))
+ if v := t.Get(labelsTag); v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func extractFieldName(name string, tag *ast.BasicLit) string {
+ if tag != nil {
+ t := reflect.StructTag(strings.Trim(tag.Value, "`"))
+ if v := t.Get(metricNameTag); v != "" {
+ return v
+ }
+ }
+ return toSnakeCase(name)
+}
+
+func extractHistogramOptions(tag *ast.BasicLit) HistogramOpts {
+ h := HistogramOpts{}
+ if tag != nil {
+ t := reflect.StructTag(strings.Trim(tag.Value, "`"))
+ if v := t.Get(bucketTypeTag); v != "" {
+ h.BucketType = bucketType[v]
+ }
+ if v := t.Get(bucketSizeTag); v != "" {
+ h.BucketSizes = v
+ }
+ }
+ return h
+}
+
+var capitalChange = regexp.MustCompile("([a-z0-9])([A-Z])")
+
+func toSnakeCase(str string) string {
+ snake := capitalChange.ReplaceAllString(str, "${1}_${2}")
+ return strings.ToLower(snake)
+}
diff --git a/scripts/metricsgen/metricsgen_test.go b/scripts/metricsgen/metricsgen_test.go
new file mode 100644
index 000000000..66d142ce0
--- /dev/null
+++ b/scripts/metricsgen/metricsgen_test.go
@@ -0,0 +1,200 @@
+package main_test
+
+import (
+ "bytes"
+ "fmt"
+ "go/parser"
+ "go/token"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ metricsgen "github.com/tendermint/tendermint/scripts/metricsgen"
+)
+
+const testDataDir = "./testdata"
+
+func TestSimpleTemplate(t *testing.T) {
+ m := metricsgen.ParsedMetricField{
+ TypeName: "Histogram",
+ FieldName: "MyMetric",
+ MetricName: "request_count",
+ Description: "how many requests were made since the start of the process",
+ Labels: "first, second, third",
+ }
+ td := metricsgen.TemplateData{
+ Package: "mypack",
+ ParsedMetrics: []metricsgen.ParsedMetricField{m},
+ }
+ b := bytes.NewBuffer([]byte{})
+ err := metricsgen.GenerateMetricsFile(b, td)
+ if err != nil {
+ t.Fatalf("unable to parse template %v", err)
+ }
+}
+
+func TestFromData(t *testing.T) {
+ infos, err := ioutil.ReadDir(testDataDir)
+ if err != nil {
+ t.Fatalf("unable to open file %v", err)
+ }
+ for _, dir := range infos {
+ t.Run(dir.Name(), func(t *testing.T) {
+ if !dir.IsDir() {
+ t.Fatalf("expected file %s to be directory", dir.Name())
+ }
+ dirName := path.Join(testDataDir, dir.Name())
+ pt, err := metricsgen.ParseMetricsDir(dirName, "Metrics")
+ if err != nil {
+ t.Fatalf("unable to parse from dir %q: %v", dir, err)
+ }
+ outFile := path.Join(dirName, "out.go")
+ if err != nil {
+ t.Fatalf("unable to open file %s: %v", outFile, err)
+ }
+ of, err := os.Create(outFile)
+ if err != nil {
+ t.Fatalf("unable to open file %s: %v", outFile, err)
+ }
+ defer os.Remove(outFile)
+ if err := metricsgen.GenerateMetricsFile(of, pt); err != nil {
+ t.Fatalf("unable to generate metrics file %s: %v", outFile, err)
+ }
+ if _, err := parser.ParseFile(token.NewFileSet(), outFile, nil, parser.AllErrors); err != nil {
+ t.Fatalf("unable to parse generated file %s: %v", outFile, err)
+ }
+ })
+ }
+}
+
+func TestParseMetricsStruct(t *testing.T) {
+ const pkgName = "mypkg"
+ metricsTests := []struct {
+ name string
+ shouldError bool
+ metricsStruct string
+ expected metricsgen.TemplateData
+ }{
+ {
+ name: "basic",
+ metricsStruct: `type Metrics struct {
+ myGauge metrics.Gauge
+ }`,
+ expected: metricsgen.TemplateData{
+ Package: pkgName,
+ ParsedMetrics: []metricsgen.ParsedMetricField{
+ {
+ TypeName: "Gauge",
+ FieldName: "myGauge",
+ MetricName: "my_gauge",
+ },
+ },
+ },
+ },
+ {
+ name: "histogram",
+ metricsStruct: "type Metrics struct {\n" +
+ "myHistogram metrics.Histogram `metricsgen_bucketsType:\"exp\" metricsgen_bucketSizes:\"1, 100, .8\"`\n" +
+ "}",
+ expected: metricsgen.TemplateData{
+ Package: pkgName,
+ ParsedMetrics: []metricsgen.ParsedMetricField{
+ {
+ TypeName: "Histogram",
+ FieldName: "myHistogram",
+ MetricName: "my_histogram",
+
+ HistogramOptions: metricsgen.HistogramOpts{
+ BucketType: "stdprometheus.ExponentialBuckets",
+ BucketSizes: "1, 100, .8",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "labeled name",
+ metricsStruct: "type Metrics struct {\n" +
+ "myCounter metrics.Counter `metricsgen_name:\"new_name\"`\n" +
+ "}",
+ expected: metricsgen.TemplateData{
+ Package: pkgName,
+ ParsedMetrics: []metricsgen.ParsedMetricField{
+ {
+ TypeName: "Counter",
+ FieldName: "myCounter",
+ MetricName: "new_name",
+ },
+ },
+ },
+ },
+ {
+ name: "metric labels",
+ metricsStruct: "type Metrics struct {\n" +
+ "myCounter metrics.Counter `metricsgen_labels:\"label1, label2\"`\n" +
+ "}",
+ expected: metricsgen.TemplateData{
+ Package: pkgName,
+ ParsedMetrics: []metricsgen.ParsedMetricField{
+ {
+ TypeName: "Counter",
+ FieldName: "myCounter",
+ MetricName: "my_counter",
+ Labels: "label1, label2",
+ },
+ },
+ },
+ },
+ {
+ name: "ignore non-metric field",
+ metricsStruct: `type Metrics struct {
+ myCounter metrics.Counter
+ nonMetric string
+ }`,
+ expected: metricsgen.TemplateData{
+ Package: pkgName,
+ ParsedMetrics: []metricsgen.ParsedMetricField{
+ {
+ TypeName: "Counter",
+ FieldName: "myCounter",
+ MetricName: "my_counter",
+ },
+ },
+ },
+ },
+ }
+ for _, testCase := range metricsTests {
+ t.Run(testCase.name, func(t *testing.T) {
+ dir, err := os.MkdirTemp(os.TempDir(), "metricsdir")
+ if err != nil {
+ t.Fatalf("unable to create directory: %v", err)
+ }
+ defer os.Remove(dir)
+ f, err := os.Create(filepath.Join(dir, "metrics.go"))
+ if err != nil {
+ t.Fatalf("unable to open file: %v", err)
+ }
+ pkgLine := fmt.Sprintf("package %s\n", pkgName)
+ importClause := `
+ import(
+ "github.com/go-kit/kit/metrics"
+ )
+ `
+
+ f.Write([]byte(pkgLine))
+ f.Write([]byte(importClause))
+ f.Write([]byte(testCase.metricsStruct))
+
+ td, err := metricsgen.ParseMetricsDir(dir, "Metrics")
+ if testCase.shouldError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, testCase.expected, td)
+ }
+ })
+ }
+}
diff --git a/scripts/metricsgen/testdata/basic/metrics.go b/scripts/metricsgen/testdata/basic/metrics.go
new file mode 100644
index 000000000..ae5be16fd
--- /dev/null
+++ b/scripts/metricsgen/testdata/basic/metrics.go
@@ -0,0 +1,9 @@
+package basic
+
+import "github.com/go-kit/kit/metrics"
+
+// Metrics contains metrics exposed by this package.
+type Metrics struct {
+ // simple metric that tracks the height of the chain.
+ Height metrics.Gauge
+}
diff --git a/scripts/metricsgen/testdata/commented/metrics.go b/scripts/metricsgen/testdata/commented/metrics.go
new file mode 100644
index 000000000..e6233748f
--- /dev/null
+++ b/scripts/metricsgen/testdata/commented/metrics.go
@@ -0,0 +1,9 @@
+package commented
+
+import "github.com/go-kit/kit/metrics"
+
+type Metrics struct {
+ // Height of the chain.
+ // We expect multi-line comments to parse correctly.
+ Field metrics.Gauge
+}
diff --git a/scripts/metricsgen/testdata/tags/metrics.go b/scripts/metricsgen/testdata/tags/metrics.go
new file mode 100644
index 000000000..968e9c0bc
--- /dev/null
+++ b/scripts/metricsgen/testdata/tags/metrics.go
@@ -0,0 +1,10 @@
+package tags
+
+import "github.com/go-kit/kit/metrics"
+
+type Metrics struct {
+ WithLabels metrics.Counter `metricsgen_labels:"step,time"`
+ WithExpBuckets metrics.Histogram `metricsgen_bucketsType:"exp" metricsgen_bucketSizes:".1,100,8"`
+ WithBuckets metrics.Histogram `metricsgen_bucketSizes:"1, 2, 3, 4, 5"`
+ Named metrics.Counter `metricsgen_name:"metric_with_name"`
+}