mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-06 13:26:23 +00:00
## What does this change do? This pull request completes the change to the `metricsgen` metrics. It adds `go generate` directives to all of the files containing the `Metrics` structs. Using the outputs of `metricsdiff` between these generated metrics and `master`, we can see that there is not a diff between the two sets of metrics when run locally. ``` [william@sidewinder] tendermint[wb/metrics-gen-transition]:. ◆ ./scripts/metricsgen/metricsdiff/metricsdiff metrics_master metrics_generated [william@sidewinder] tendermint[wb/metrics-gen-transition]:. ◆ ``` This change also adds parsing for a `metrics:` key in a field comment. If a comment line begins with `//metrics:` the rest of the line is interpreted to be the metric help text. Additionally, a bug where lists of labels were not properly quoted in the `metricsgen` rendered output was fixed.
348 lines
8.4 KiB
Go
348 lines
8.4 KiB
Go
// 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"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
func init() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, `Usage: %[1]s -struct <struct>
|
|
|
|
Generate constructors for the metrics type specified by -struct contained in
|
|
the current directory. The tool creates a new file in the current directory
|
|
containing the generated code.
|
|
|
|
Options:
|
|
`, filepath.Base(os.Args[0]))
|
|
flag.PrintDefaults()
|
|
}
|
|
}
|
|
|
|
const metricsPackageName = "github.com/go-kit/kit/metrics"
|
|
|
|
const (
|
|
metricNameTag = "metrics_name"
|
|
labelsTag = "metrics_labels"
|
|
bucketTypeTag = "metrics_buckettype"
|
|
bucketSizeTag = "metrics_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}})).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")
|
|
}
|
|
td, err := ParseMetricsDir(".", *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)
|
|
}
|
|
}
|
|
func ignoreTestFiles(f fs.FileInfo) bool {
|
|
return !strings.Contains(f.Name(), "_test.go")
|
|
}
|
|
|
|
// 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) {
|
|
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, mPkgName, err := findMetricsStruct(pkg.Files, structName)
|
|
if err != nil {
|
|
return TemplateData{}, err
|
|
}
|
|
for _, f := range m.Fields.List {
|
|
if !isMetric(f.Type, mPkgName) {
|
|
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 findMetricsStruct(files map[string]*ast.File, structName string) (*ast.StructType, string, error) {
|
|
var (
|
|
st *ast.StructType
|
|
)
|
|
for _, file := range files {
|
|
mPkgName, err := extractMetricsPackageName(file.Imports)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("unable to determine metrics package name: %v", err)
|
|
}
|
|
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, mPkgName, nil
|
|
}
|
|
}
|
|
return nil, "", fmt.Errorf("target struct %q not found in dir", structName)
|
|
}
|
|
|
|
func parseMetricField(f *ast.Field) ParsedMetricField {
|
|
pmf := ParsedMetricField{
|
|
Description: extractHelpMessage(f.Doc),
|
|
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(path.Ext(types.ExprString(e)), ".")
|
|
}
|
|
|
|
func extractHelpMessage(cg *ast.CommentGroup) string {
|
|
if cg == nil {
|
|
return ""
|
|
}
|
|
var help []string //nolint: prealloc
|
|
for _, c := range cg.List {
|
|
mt := strings.TrimPrefix(c.Text, "//metrics:")
|
|
if mt != c.Text {
|
|
return strings.TrimSpace(mt)
|
|
}
|
|
help = append(help, strings.TrimSpace(strings.TrimPrefix(c.Text, "//")))
|
|
}
|
|
return strings.Join(help, " ")
|
|
}
|
|
|
|
func isMetric(e ast.Expr, mPkgName string) bool {
|
|
return strings.Contains(types.ExprString(e), fmt.Sprintf("%s.", mPkgName))
|
|
}
|
|
|
|
func extractLabels(bl *ast.BasicLit) string {
|
|
if bl != nil {
|
|
t := reflect.StructTag(strings.Trim(bl.Value, "`"))
|
|
if v := t.Get(labelsTag); v != "" {
|
|
var res []string
|
|
for _, s := range strings.Split(v, ",") {
|
|
res = append(res, strconv.Quote(strings.TrimSpace(s)))
|
|
}
|
|
return strings.Join(res, ",")
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
func extractMetricsPackageName(imports []*ast.ImportSpec) (string, error) {
|
|
for _, i := range imports {
|
|
u, err := strconv.Unquote(i.Path.Value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if u == metricsPackageName {
|
|
if i.Name != nil {
|
|
return i.Name.Name, nil
|
|
}
|
|
return path.Base(u), nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
var capitalChange = regexp.MustCompile("([a-z0-9])([A-Z])")
|
|
|
|
func toSnakeCase(str string) string {
|
|
snake := capitalChange.ReplaceAllString(str, "${1}_${2}")
|
|
return strings.ToLower(snake)
|
|
}
|