github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/scripts/metricsgen/metricsgen.go (about)

     1  // metricsgen is a code generation tool for creating constructors for Tendermint
     2  // metrics types.
     3  package main
     4  
     5  import (
     6  	"bytes"
     7  	"flag"
     8  	"fmt"
     9  	"go/ast"
    10  	"go/format"
    11  	"go/parser"
    12  	"go/token"
    13  	"go/types"
    14  	"io"
    15  	"io/fs"
    16  	"log"
    17  	"os"
    18  	"path"
    19  	"path/filepath"
    20  	"reflect"
    21  	"regexp"
    22  	"strconv"
    23  	"strings"
    24  	"text/template"
    25  )
    26  
    27  func init() {
    28  	flag.Usage = func() {
    29  		fmt.Fprintf(os.Stderr, `Usage: %[1]s  -struct <struct>
    30  
    31  Generate constructors for the metrics type specified by -struct contained in
    32  the current directory. The tool creates a new file in the current directory
    33  containing the generated code.
    34  
    35  Options:
    36  `, filepath.Base(os.Args[0]))
    37  		flag.PrintDefaults()
    38  	}
    39  }
    40  
    41  const metricsPackageName = "github.com/go-kit/kit/metrics"
    42  
    43  const (
    44  	metricNameTag = "metrics_name"
    45  	labelsTag     = "metrics_labels"
    46  	bucketTypeTag = "metrics_buckettype"
    47  	bucketSizeTag = "metrics_bucketsizes"
    48  )
    49  
    50  var (
    51  	dir   = flag.String("dir", ".", "Path to the directory containing the target package")
    52  	strct = flag.String("struct", "Metrics", "Struct to parse for metrics")
    53  )
    54  
    55  var bucketType = map[string]string{
    56  	"exprange": "stdprometheus.ExponentialBucketsRange",
    57  	"exp":      "stdprometheus.ExponentialBuckets",
    58  	"lin":      "stdprometheus.LinearBuckets",
    59  }
    60  
    61  var tmpl = template.Must(template.New("tmpl").Parse(`// Code generated by metricsgen. DO NOT EDIT.
    62  
    63  package {{ .Package }}
    64  
    65  import (
    66  	"github.com/go-kit/kit/metrics/discard"
    67  	prometheus "github.com/go-kit/kit/metrics/prometheus"
    68  	stdprometheus "github.com/prometheus/client_golang/prometheus"
    69  )
    70  
    71  func PrometheusMetrics(namespace string, labelsAndValues...string) *Metrics {
    72  	labels := []string{}
    73  	for i := 0; i < len(labelsAndValues); i += 2 {
    74  		labels = append(labels, labelsAndValues[i])
    75  	}
    76  	return &Metrics{
    77  		{{ range $metric := .ParsedMetrics }}
    78  		{{- $metric.FieldName }}: prometheus.New{{ $metric.TypeName }}From(stdprometheus.{{$metric.TypeName }}Opts{
    79  			Namespace: namespace,
    80  			Subsystem: MetricsSubsystem,
    81  			Name:      "{{$metric.MetricName }}",
    82  			Help:      "{{ $metric.Description }}",
    83  			{{ if ne $metric.HistogramOptions.BucketType "" }}
    84  			Buckets: {{ $metric.HistogramOptions.BucketType }}({{ $metric.HistogramOptions.BucketSizes }}),
    85  			{{ else if ne $metric.HistogramOptions.BucketSizes "" }}
    86  			Buckets: []float64{ {{ $metric.HistogramOptions.BucketSizes }} },
    87  			{{ end }}
    88  		{{- if eq (len $metric.Labels) 0 }}
    89  		}, labels).With(labelsAndValues...),
    90  		{{ else }}
    91  		}, append(labels, {{$metric.Labels}})).With(labelsAndValues...),
    92  		{{ end }}
    93  		{{- end }}
    94  	}
    95  }
    96  
    97  
    98  func NopMetrics() *Metrics {
    99  	return &Metrics{
   100  		{{- range $metric := .ParsedMetrics }}
   101  		{{ $metric.FieldName }}: discard.New{{ $metric.TypeName }}(),
   102  		{{- end }}
   103  	}
   104  }
   105  `))
   106  
   107  // ParsedMetricField is the data parsed for a single field of a metric struct.
   108  type ParsedMetricField struct {
   109  	TypeName    string
   110  	FieldName   string
   111  	MetricName  string
   112  	Description string
   113  	Labels      string
   114  
   115  	HistogramOptions HistogramOpts
   116  }
   117  
   118  type HistogramOpts struct {
   119  	BucketType  string
   120  	BucketSizes string
   121  }
   122  
   123  // TemplateData is all of the data required for rendering a metric file template.
   124  type TemplateData struct {
   125  	Package       string
   126  	ParsedMetrics []ParsedMetricField
   127  }
   128  
   129  func main() {
   130  	flag.Parse()
   131  	if *strct == "" {
   132  		log.Fatal("You must specify a non-empty -struct")
   133  	}
   134  	td, err := ParseMetricsDir(".", *strct)
   135  	if err != nil {
   136  		log.Fatalf("Parsing file: %v", err)
   137  	}
   138  	out := filepath.Join(*dir, "metrics.gen.go")
   139  	f, err := os.Create(out)
   140  	if err != nil {
   141  		log.Fatalf("Opening file: %v", err)
   142  	}
   143  	err = GenerateMetricsFile(f, td)
   144  	if err != nil {
   145  		log.Fatalf("Generating code: %v", err)
   146  	}
   147  }
   148  func ignoreTestFiles(f fs.FileInfo) bool {
   149  	return !strings.Contains(f.Name(), "_test.go")
   150  }
   151  
   152  // ParseMetricsDir parses the dir and scans for a struct matching structName,
   153  // ignoring all test files. ParseMetricsDir iterates the fields of the metrics
   154  // struct and builds a TemplateData using the data obtained from the abstract syntax tree.
   155  func ParseMetricsDir(dir string, structName string) (TemplateData, error) {
   156  	fs := token.NewFileSet()
   157  	d, err := parser.ParseDir(fs, dir, ignoreTestFiles, parser.ParseComments)
   158  	if err != nil {
   159  		return TemplateData{}, err
   160  	}
   161  	if len(d) > 1 {
   162  		return TemplateData{}, fmt.Errorf("multiple packages found in %s", dir)
   163  	}
   164  	if len(d) == 0 {
   165  		return TemplateData{}, fmt.Errorf("no go pacakges found in %s", dir)
   166  	}
   167  
   168  	// Grab the package name.
   169  	var pkgName string
   170  	var pkg *ast.Package
   171  	for pkgName, pkg = range d {
   172  	}
   173  	td := TemplateData{
   174  		Package: pkgName,
   175  	}
   176  	// Grab the metrics struct
   177  	m, mPkgName, err := findMetricsStruct(pkg.Files, structName)
   178  	if err != nil {
   179  		return TemplateData{}, err
   180  	}
   181  	for _, f := range m.Fields.List {
   182  		if !isMetric(f.Type, mPkgName) {
   183  			continue
   184  		}
   185  		pmf := parseMetricField(f)
   186  		td.ParsedMetrics = append(td.ParsedMetrics, pmf)
   187  	}
   188  
   189  	return td, err
   190  }
   191  
   192  // GenerateMetricsFile executes the metrics file template, writing the result
   193  // into the io.Writer.
   194  func GenerateMetricsFile(w io.Writer, td TemplateData) error {
   195  	b := []byte{}
   196  	buf := bytes.NewBuffer(b)
   197  	err := tmpl.Execute(buf, td)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	b, err = format.Source(buf.Bytes())
   202  	if err != nil {
   203  		return err
   204  	}
   205  	_, err = io.Copy(w, bytes.NewBuffer(b))
   206  	if err != nil {
   207  		return err
   208  	}
   209  	return nil
   210  }
   211  
   212  func findMetricsStruct(files map[string]*ast.File, structName string) (*ast.StructType, string, error) {
   213  	var (
   214  		st *ast.StructType
   215  	)
   216  	for _, file := range files {
   217  		mPkgName, err := extractMetricsPackageName(file.Imports)
   218  		if err != nil {
   219  			return nil, "", fmt.Errorf("unable to determine metrics package name: %v", err)
   220  		}
   221  		if !ast.FilterFile(file, func(name string) bool {
   222  			return name == structName
   223  		}) {
   224  			continue
   225  		}
   226  		ast.Inspect(file, func(n ast.Node) bool {
   227  			switch f := n.(type) {
   228  			case *ast.TypeSpec:
   229  				if f.Name.Name == structName {
   230  					var ok bool
   231  					st, ok = f.Type.(*ast.StructType)
   232  					if !ok {
   233  						err = fmt.Errorf("found identifier for %q of wrong type", structName)
   234  					}
   235  				}
   236  				return false
   237  			default:
   238  				return true
   239  			}
   240  		})
   241  		if err != nil {
   242  			return nil, "", err
   243  		}
   244  		if st != nil {
   245  			return st, mPkgName, nil
   246  		}
   247  	}
   248  	return nil, "", fmt.Errorf("target struct %q not found in dir", structName)
   249  }
   250  
   251  func parseMetricField(f *ast.Field) ParsedMetricField {
   252  	pmf := ParsedMetricField{
   253  		Description: extractHelpMessage(f.Doc),
   254  		MetricName:  extractFieldName(f.Names[0].String(), f.Tag),
   255  		FieldName:   f.Names[0].String(),
   256  		TypeName:    extractTypeName(f.Type),
   257  		Labels:      extractLabels(f.Tag),
   258  	}
   259  	if pmf.TypeName == "Histogram" {
   260  		pmf.HistogramOptions = extractHistogramOptions(f.Tag)
   261  	}
   262  	return pmf
   263  }
   264  
   265  func extractTypeName(e ast.Expr) string {
   266  	return strings.TrimPrefix(path.Ext(types.ExprString(e)), ".")
   267  }
   268  
   269  func extractHelpMessage(cg *ast.CommentGroup) string {
   270  	if cg == nil {
   271  		return ""
   272  	}
   273  	var help []string //nolint: prealloc
   274  	for _, c := range cg.List {
   275  		mt := strings.TrimPrefix(c.Text, "//metrics:")
   276  		if mt != c.Text {
   277  			return strings.TrimSpace(mt)
   278  		}
   279  		help = append(help, strings.TrimSpace(strings.TrimPrefix(c.Text, "//")))
   280  	}
   281  	return strings.Join(help, " ")
   282  }
   283  
   284  func isMetric(e ast.Expr, mPkgName string) bool {
   285  	return strings.Contains(types.ExprString(e), fmt.Sprintf("%s.", mPkgName))
   286  }
   287  
   288  func extractLabels(bl *ast.BasicLit) string {
   289  	if bl != nil {
   290  		t := reflect.StructTag(strings.Trim(bl.Value, "`"))
   291  		if v := t.Get(labelsTag); v != "" {
   292  			var res []string
   293  			for _, s := range strings.Split(v, ",") {
   294  				res = append(res, strconv.Quote(strings.TrimSpace(s)))
   295  			}
   296  			return strings.Join(res, ",")
   297  		}
   298  	}
   299  	return ""
   300  }
   301  
   302  func extractFieldName(name string, tag *ast.BasicLit) string {
   303  	if tag != nil {
   304  		t := reflect.StructTag(strings.Trim(tag.Value, "`"))
   305  		if v := t.Get(metricNameTag); v != "" {
   306  			return v
   307  		}
   308  	}
   309  	return toSnakeCase(name)
   310  }
   311  
   312  func extractHistogramOptions(tag *ast.BasicLit) HistogramOpts {
   313  	h := HistogramOpts{}
   314  	if tag != nil {
   315  		t := reflect.StructTag(strings.Trim(tag.Value, "`"))
   316  		if v := t.Get(bucketTypeTag); v != "" {
   317  			h.BucketType = bucketType[v]
   318  		}
   319  		if v := t.Get(bucketSizeTag); v != "" {
   320  			h.BucketSizes = v
   321  		}
   322  	}
   323  	return h
   324  }
   325  
   326  func extractMetricsPackageName(imports []*ast.ImportSpec) (string, error) {
   327  	for _, i := range imports {
   328  		u, err := strconv.Unquote(i.Path.Value)
   329  		if err != nil {
   330  			return "", err
   331  		}
   332  		if u == metricsPackageName {
   333  			if i.Name != nil {
   334  				return i.Name.Name, nil
   335  			}
   336  			return path.Base(u), nil
   337  		}
   338  	}
   339  	return "", nil
   340  }
   341  
   342  var capitalChange = regexp.MustCompile("([a-z0-9])([A-Z])")
   343  
   344  func toSnakeCase(str string) string {
   345  	snake := capitalChange.ReplaceAllString(str, "${1}_${2}")
   346  	return strings.ToLower(snake)
   347  }