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 }