package test import ( "bytes" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "testing" "github.com/coredns/coredns/plugin" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "github.com/prometheus/prometheus/util/promlint" ) func TestMetricNaming(t *testing.T) { walker := validMetricWalker{} err := filepath.Walk("..", walker.walk) if err != nil { t.Fatal(err) } if len(walker.Metrics) > 0 { buf := &bytes.Buffer{} encoder := expfmt.NewEncoder(buf, expfmt.FmtText) for _, mf := range walker.Metrics { if err := encoder.Encode(mf); err != nil { t.Fatalf("Encoding and sending metric family: %s", err) } } l := promlint.New(buf) problems, err := l.Lint() if err != nil { t.Fatalf("Link found error: %s", err) } if len(problems) > 0 { t.Fatalf("A slice of Problems indicating any issues found in the metrics stream: %s", problems) } } } type validMetricWalker struct { Metrics []*dto.MetricFamily } func (w *validMetricWalker) walk(path string, info os.FileInfo, _ error) error { // only for regular files, not starting with a . and those that are go files. if !info.Mode().IsRegular() { return nil } // Is it appropriate to compare the file name equals metrics.go directly? if strings.HasPrefix(path, "../.") { return nil } if strings.HasSuffix(path, "_test.go") { return nil } if !strings.HasSuffix(path, ".go") { return nil } fs := token.NewFileSet() f, err := parser.ParseFile(fs, path, nil, parser.AllErrors) if err != nil { return err } l := &metric{} ast.Walk(l, f) if l.Metric != nil { w.Metrics = append(w.Metrics, l.Metric) } return nil } type metric struct { Metric *dto.MetricFamily } func (l metric) Visit(n ast.Node) ast.Visitor { if n == nil { return nil } ce, ok := n.(*ast.CallExpr) if !ok { return l } se, ok := ce.Fun.(*ast.SelectorExpr) if !ok { return l } id, ok := se.X.(*ast.Ident) if !ok { return l } if id.Name != "prometheus" { //prometheus return l } var metricsType dto.MetricType switch se.Sel.Name { case "NewCounterVec", "NewCounter": metricsType = dto.MetricType_COUNTER case "NewGaugeVec", "NewGauge": metricsType = dto.MetricType_GAUGE case "NewHistogramVec", "NewHistogram": metricsType = dto.MetricType_HISTOGRAM case "NewSummaryVec", "NewSummary": metricsType = dto.MetricType_SUMMARY default: return l } // Check first arg, that should have basic lit with capital if len(ce.Args) < 1 { return l } bl, ok := ce.Args[0].(*ast.CompositeLit) if !ok { return l } // parse Namespace Subsystem Name Help var subsystem, name, help string for _, elt := range bl.Elts { expr, ok := elt.(*ast.KeyValueExpr) if !ok { continue } object, ok := expr.Key.(*ast.Ident) if !ok { continue } value, ok := expr.Value.(*ast.BasicLit) if !ok { continue } switch object.Name { case "Subsystem": subsystem = value.Value case "Name": name = value.Value case "Help": help = value.Value } } // validate metrics field if len(name) == 0 || len(help) == 0 { return l } var metricName string if len(subsystem) > 0 { metricName = strings.Join([]string{plugin.Namespace, subsystem, name}, "_") } else { metricName = strings.Join([]string{plugin.Namespace, name}, "_") } l.Metric = &dto.MetricFamily{ Name: &metricName, Help: &help, Type: &metricsType, } return l }