Marvin Preuss
d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
665 lines
15 KiB
Go
665 lines
15 KiB
Go
package promlinter
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
|
|
dto "github.com/prometheus/client_model/go"
|
|
)
|
|
|
|
var (
|
|
metricsType map[string]dto.MetricType
|
|
constMetricArgNum map[string]int
|
|
validOptsFields map[string]bool
|
|
lintFuncText map[string][]string
|
|
LintFuncNames []string
|
|
)
|
|
|
|
func init() {
|
|
metricsType = map[string]dto.MetricType{
|
|
"Counter": dto.MetricType_COUNTER,
|
|
"NewCounter": dto.MetricType_COUNTER,
|
|
"NewCounterVec": dto.MetricType_COUNTER,
|
|
"Gauge": dto.MetricType_GAUGE,
|
|
"NewGauge": dto.MetricType_GAUGE,
|
|
"NewGaugeVec": dto.MetricType_GAUGE,
|
|
"NewHistogram": dto.MetricType_HISTOGRAM,
|
|
"NewHistogramVec": dto.MetricType_HISTOGRAM,
|
|
"NewSummary": dto.MetricType_SUMMARY,
|
|
"NewSummaryVec": dto.MetricType_SUMMARY,
|
|
}
|
|
|
|
constMetricArgNum = map[string]int{
|
|
"MustNewConstMetric": 3,
|
|
"MustNewHistogram": 4,
|
|
"MustNewSummary": 4,
|
|
"NewLazyConstMetric": 3,
|
|
}
|
|
|
|
// Doesn't contain ConstLabels since we don't need this field here.
|
|
validOptsFields = map[string]bool{
|
|
"Name": true,
|
|
"Namespace": true,
|
|
"Subsystem": true,
|
|
"Help": true,
|
|
}
|
|
|
|
lintFuncText = map[string][]string{
|
|
"Help": {"no help text"},
|
|
"MetricUnits": {"use base unit"},
|
|
"Counter": {"counter metrics should"},
|
|
"HistogramSummaryReserved": {"non-histogram", "non-summary"},
|
|
"MetricTypeInName": {"metric name should not include type"},
|
|
"ReservedChars": {"metric names should not contain ':'"},
|
|
"CamelCase": {"'snake_case' not 'camelCase'"},
|
|
"lintUnitAbbreviations": {"metric names should not contain abbreviated units"},
|
|
}
|
|
|
|
LintFuncNames = []string{"Help", "MetricUnits", "Counter", "HistogramSummaryReserved",
|
|
"MetricTypeInName", "ReservedChars", "CamelCase", "lintUnitAbbreviations"}
|
|
}
|
|
|
|
type Setting struct {
|
|
Strict bool
|
|
DisabledLintFuncs []string
|
|
}
|
|
|
|
// Issue contains metric name, error text and metric position.
|
|
type Issue struct {
|
|
Text string
|
|
Metric string
|
|
Pos token.Position
|
|
}
|
|
|
|
type MetricFamilyWithPos struct {
|
|
MetricFamily *dto.MetricFamily
|
|
Pos token.Position
|
|
}
|
|
|
|
type visitor struct {
|
|
fs *token.FileSet
|
|
metrics []MetricFamilyWithPos
|
|
issues []Issue
|
|
strict bool
|
|
}
|
|
|
|
type opt struct {
|
|
namespace string
|
|
subsystem string
|
|
name string
|
|
}
|
|
|
|
func RunList(fs *token.FileSet, files []*ast.File, strict bool) []MetricFamilyWithPos {
|
|
v := &visitor{
|
|
fs: fs,
|
|
metrics: make([]MetricFamilyWithPos, 0),
|
|
issues: make([]Issue, 0),
|
|
strict: strict,
|
|
}
|
|
|
|
for _, file := range files {
|
|
ast.Walk(v, file)
|
|
}
|
|
|
|
sort.Slice(v.metrics, func(i, j int) bool {
|
|
return v.metrics[i].Pos.String() < v.metrics[j].Pos.String()
|
|
})
|
|
return v.metrics
|
|
}
|
|
|
|
func RunLint(fs *token.FileSet, files []*ast.File, s Setting) []Issue {
|
|
v := &visitor{
|
|
fs: fs,
|
|
metrics: make([]MetricFamilyWithPos, 0),
|
|
issues: make([]Issue, 0),
|
|
strict: s.Strict,
|
|
}
|
|
|
|
for _, file := range files {
|
|
ast.Walk(v, file)
|
|
}
|
|
|
|
// lint metrics
|
|
for _, mfp := range v.metrics {
|
|
problems, err := promlint.NewWithMetricFamilies([]*dto.MetricFamily{mfp.MetricFamily}).Lint()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, p := range problems {
|
|
for _, disabledFunc := range s.DisabledLintFuncs {
|
|
for _, pattern := range lintFuncText[disabledFunc] {
|
|
if strings.Contains(p.Text, pattern) {
|
|
goto END
|
|
}
|
|
}
|
|
}
|
|
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: mfp.Pos,
|
|
Metric: p.Metric,
|
|
Text: p.Text,
|
|
})
|
|
|
|
END:
|
|
}
|
|
}
|
|
|
|
sort.Slice(v.issues, func(i, j int) bool {
|
|
return v.issues[i].Pos.String() < v.issues[j].Pos.String()
|
|
})
|
|
return v.issues
|
|
}
|
|
|
|
func (v *visitor) Visit(n ast.Node) ast.Visitor {
|
|
if n == nil {
|
|
return v
|
|
}
|
|
|
|
switch t := n.(type) {
|
|
case *ast.CallExpr:
|
|
return v.parseCallerExpr(t)
|
|
|
|
case *ast.SendStmt:
|
|
return v.parseSendMetricChanExpr(t)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func (v *visitor) parseCallerExpr(call *ast.CallExpr) ast.Visitor {
|
|
var (
|
|
metricType dto.MetricType
|
|
methodName string
|
|
ok bool
|
|
)
|
|
|
|
switch stmt := call.Fun.(type) {
|
|
|
|
/*
|
|
That's the case of setting alias . to client_golang/prometheus or promauto package.
|
|
|
|
import . "github.com/prometheus/client_golang/prometheus"
|
|
metric := NewCounter(CounterOpts{})
|
|
*/
|
|
case *ast.Ident:
|
|
if stmt.Name == "NewCounterFunc" {
|
|
return v.parseOpts(call.Args[0], dto.MetricType_COUNTER)
|
|
}
|
|
|
|
if stmt.Name == "NewGaugeFunc" {
|
|
return v.parseOpts(call.Args[0], dto.MetricType_GAUGE)
|
|
}
|
|
|
|
if metricType, ok = metricsType[stmt.Name]; !ok {
|
|
return v
|
|
}
|
|
methodName = stmt.Name
|
|
|
|
/*
|
|
This case covers the most of cases to initialize metrics.
|
|
|
|
prometheus.NewCounter(CounterOpts{})
|
|
|
|
promauto.With(nil).NewCounter(CounterOpts{})
|
|
|
|
factory := promauto.With(nil)
|
|
factory.NewCounter(CounterOpts{})
|
|
|
|
prometheus.NewCounterFunc()
|
|
*/
|
|
case *ast.SelectorExpr:
|
|
if stmt.Sel.Name == "NewCounterFunc" {
|
|
return v.parseOpts(call.Args[0], dto.MetricType_COUNTER)
|
|
}
|
|
|
|
if stmt.Sel.Name == "NewGaugeFunc" {
|
|
return v.parseOpts(call.Args[0], dto.MetricType_GAUGE)
|
|
}
|
|
|
|
if stmt.Sel.Name == "NewFamilyGenerator" && len(call.Args) == 5 {
|
|
return v.parseKSMMetrics(call.Args[0], call.Args[1], call.Args[2])
|
|
}
|
|
|
|
if metricType, ok = metricsType[stmt.Sel.Name]; !ok {
|
|
return v
|
|
}
|
|
methodName = stmt.Sel.Name
|
|
|
|
default:
|
|
return v
|
|
}
|
|
|
|
argNum := 1
|
|
if strings.HasSuffix(methodName, "Vec") {
|
|
argNum = 2
|
|
}
|
|
// The methods used to initialize metrics should have at least one arg.
|
|
if len(call.Args) < argNum && v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(call.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("%s should have at least %d arguments", methodName, argNum),
|
|
})
|
|
return v
|
|
}
|
|
|
|
return v.parseOpts(call.Args[0], metricType)
|
|
}
|
|
|
|
func (v *visitor) parseOpts(optArg ast.Node, metricType dto.MetricType) ast.Visitor {
|
|
// position for the first arg of the CallExpr
|
|
optsPosition := v.fs.Position(optArg.Pos())
|
|
opts, help := v.parseOptsExpr(optArg)
|
|
if opts == nil {
|
|
return v
|
|
}
|
|
currentMetric := dto.MetricFamily{
|
|
Type: &metricType,
|
|
Help: help,
|
|
}
|
|
|
|
metricName := prometheus.BuildFQName(opts.namespace, opts.subsystem, opts.name)
|
|
// We skip the invalid metric if the name is an empty string.
|
|
// This kind of metric declaration might be used as a stud metric
|
|
// https://github.com/thanos-io/thanos/blob/main/cmd/thanos/tools_bucket.go#L538.
|
|
if metricName == "" {
|
|
return v
|
|
}
|
|
currentMetric.Name = &metricName
|
|
|
|
v.metrics = append(v.metrics, MetricFamilyWithPos{MetricFamily: ¤tMetric, Pos: optsPosition})
|
|
return v
|
|
}
|
|
|
|
// Parser for kube-state-metrics generators.
|
|
func (v *visitor) parseKSMMetrics(nameArg ast.Node, helpArg ast.Node, metricTypeArg ast.Node) ast.Visitor {
|
|
optsPosition := v.fs.Position(nameArg.Pos())
|
|
currentMetric := dto.MetricFamily{}
|
|
name, ok := v.parseValue("name", nameArg)
|
|
if !ok {
|
|
return v
|
|
}
|
|
currentMetric.Name = &name
|
|
|
|
help, ok := v.parseValue("help", helpArg)
|
|
if !ok {
|
|
return v
|
|
}
|
|
currentMetric.Help = &help
|
|
|
|
switch stmt := metricTypeArg.(type) {
|
|
case *ast.SelectorExpr:
|
|
if metricType, ok := metricsType[stmt.Sel.Name]; !ok {
|
|
return v
|
|
} else {
|
|
currentMetric.Type = &metricType
|
|
}
|
|
}
|
|
|
|
v.metrics = append(v.metrics, MetricFamilyWithPos{MetricFamily: ¤tMetric, Pos: optsPosition})
|
|
return v
|
|
}
|
|
|
|
func (v *visitor) parseSendMetricChanExpr(chExpr *ast.SendStmt) ast.Visitor {
|
|
var (
|
|
ok bool
|
|
requiredArgNum int
|
|
methodName string
|
|
metricType dto.MetricType
|
|
)
|
|
|
|
call, ok := chExpr.Value.(*ast.CallExpr)
|
|
if !ok {
|
|
return v
|
|
}
|
|
|
|
switch stmt := call.Fun.(type) {
|
|
case *ast.Ident:
|
|
if requiredArgNum, ok = constMetricArgNum[stmt.Name]; !ok {
|
|
return v
|
|
}
|
|
methodName = stmt.Name
|
|
|
|
case *ast.SelectorExpr:
|
|
if requiredArgNum, ok = constMetricArgNum[stmt.Sel.Name]; !ok {
|
|
return v
|
|
}
|
|
methodName = stmt.Sel.Name
|
|
}
|
|
|
|
if len(call.Args) < requiredArgNum && v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Metric: "",
|
|
Pos: v.fs.Position(call.Pos()),
|
|
Text: fmt.Sprintf("%s should have at least %d arguments", methodName, requiredArgNum),
|
|
})
|
|
return v
|
|
}
|
|
|
|
name, help := v.parseConstMetricOptsExpr(call.Args[0])
|
|
if name == nil {
|
|
return v
|
|
}
|
|
|
|
metric := &dto.MetricFamily{
|
|
Name: name,
|
|
Help: help,
|
|
}
|
|
switch methodName {
|
|
case "MustNewConstMetric", "NewLazyConstMetric":
|
|
switch t := call.Args[1].(type) {
|
|
case *ast.Ident:
|
|
metric.Type = getConstMetricType(t.Name)
|
|
case *ast.SelectorExpr:
|
|
metric.Type = getConstMetricType(t.Sel.Name)
|
|
}
|
|
|
|
case "MustNewHistogram":
|
|
metricType = dto.MetricType_HISTOGRAM
|
|
metric.Type = &metricType
|
|
case "MustNewSummary":
|
|
metricType = dto.MetricType_SUMMARY
|
|
metric.Type = &metricType
|
|
}
|
|
|
|
v.metrics = append(v.metrics, MetricFamilyWithPos{MetricFamily: metric, Pos: v.fs.Position(call.Pos())})
|
|
return v
|
|
}
|
|
|
|
func (v *visitor) parseOptsExpr(n ast.Node) (*opt, *string) {
|
|
switch stmt := n.(type) {
|
|
case *ast.CompositeLit:
|
|
return v.parseCompositeOpts(stmt)
|
|
|
|
case *ast.Ident:
|
|
if stmt.Obj != nil {
|
|
if decl, ok := stmt.Obj.Decl.(*ast.AssignStmt); ok && len(decl.Rhs) > 0 {
|
|
if t, ok := decl.Rhs[0].(*ast.CompositeLit); ok {
|
|
return v.parseCompositeOpts(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
case *ast.UnaryExpr:
|
|
return v.parseOptsExpr(stmt.X)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (v *visitor) parseCompositeOpts(stmt *ast.CompositeLit) (*opt, *string) {
|
|
metricOption := &opt{}
|
|
var help *string
|
|
for _, elt := range stmt.Elts {
|
|
kvExpr, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
object, ok := kvExpr.Key.(*ast.Ident)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if _, ok := validOptsFields[object.Name]; !ok {
|
|
continue
|
|
}
|
|
|
|
// If failed to parse field value, stop parsing.
|
|
stringLiteral, ok := v.parseValue(object.Name, kvExpr.Value)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
switch object.Name {
|
|
case "Namespace":
|
|
metricOption.namespace = stringLiteral
|
|
case "Subsystem":
|
|
metricOption.subsystem = stringLiteral
|
|
case "Name":
|
|
metricOption.name = stringLiteral
|
|
case "Help":
|
|
help = &stringLiteral
|
|
}
|
|
}
|
|
|
|
return metricOption, help
|
|
}
|
|
|
|
func (v *visitor) parseValue(object string, n ast.Node) (string, bool) {
|
|
switch t := n.(type) {
|
|
|
|
// make sure it is string literal value
|
|
case *ast.BasicLit:
|
|
if t.Kind == token.STRING {
|
|
return mustUnquote(t.Value), true
|
|
}
|
|
|
|
return "", false
|
|
|
|
case *ast.Ident:
|
|
if t.Obj == nil {
|
|
return "", false
|
|
}
|
|
|
|
if vs, ok := t.Obj.Decl.(*ast.ValueSpec); ok {
|
|
return v.parseValue(object, vs)
|
|
}
|
|
|
|
case *ast.ValueSpec:
|
|
if len(t.Values) == 0 {
|
|
return "", false
|
|
}
|
|
return v.parseValue(object, t.Values[0])
|
|
|
|
// For binary expr, we only support adding two strings like `foo` + `bar`.
|
|
case *ast.BinaryExpr:
|
|
if t.Op == token.ADD {
|
|
x, ok := v.parseValue(object, t.X)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
y, ok := v.parseValue(object, t.Y)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
return x + y, true
|
|
}
|
|
|
|
// We can only cover some basic cases here
|
|
case *ast.CallExpr:
|
|
return v.parseValueCallExpr(object, t)
|
|
|
|
default:
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(n.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing %s with type %T is not supported", object, t),
|
|
})
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func (v *visitor) parseValueCallExpr(object string, call *ast.CallExpr) (string, bool) {
|
|
var (
|
|
methodName string
|
|
namespace string
|
|
subsystem string
|
|
name string
|
|
ok bool
|
|
)
|
|
switch expr := call.Fun.(type) {
|
|
case *ast.SelectorExpr:
|
|
methodName = expr.Sel.Name
|
|
case *ast.Ident:
|
|
methodName = expr.Name
|
|
default:
|
|
return "", false
|
|
}
|
|
|
|
if methodName == "BuildFQName" && len(call.Args) == 3 {
|
|
namespace, ok = v.parseValue("namespace", call.Args[0])
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
subsystem, ok = v.parseValue("subsystem", call.Args[1])
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
name, ok = v.parseValue("name", call.Args[2])
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return prometheus.BuildFQName(namespace, subsystem, name), true
|
|
}
|
|
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Metric: "",
|
|
Pos: v.fs.Position(call.Pos()),
|
|
Text: fmt.Sprintf("parsing %s with function %s is not supported", object, methodName),
|
|
})
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func (v *visitor) parseConstMetricOptsExpr(n ast.Node) (*string, *string) {
|
|
switch stmt := n.(type) {
|
|
case *ast.CallExpr:
|
|
return v.parseNewDescCallExpr(stmt)
|
|
|
|
case *ast.Ident:
|
|
if stmt.Obj != nil {
|
|
switch t := stmt.Obj.Decl.(type) {
|
|
case *ast.AssignStmt:
|
|
if len(t.Rhs) > 0 {
|
|
if call, ok := t.Rhs[0].(*ast.CallExpr); ok {
|
|
return v.parseNewDescCallExpr(call)
|
|
}
|
|
}
|
|
case *ast.ValueSpec:
|
|
if len(t.Values) > 0 {
|
|
if call, ok := t.Values[0].(*ast.CallExpr); ok {
|
|
return v.parseNewDescCallExpr(call)
|
|
}
|
|
}
|
|
}
|
|
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(stmt.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing desc of type %T is not supported", stmt.Obj.Decl),
|
|
})
|
|
}
|
|
}
|
|
|
|
default:
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(stmt.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing desc of type %T is not supported", stmt),
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (v *visitor) parseNewDescCallExpr(call *ast.CallExpr) (*string, *string) {
|
|
var (
|
|
help string
|
|
name string
|
|
ok bool
|
|
)
|
|
|
|
switch expr := call.Fun.(type) {
|
|
case *ast.Ident:
|
|
if expr.Name != "NewDesc" {
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(expr.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing desc with function %s is not supported", expr.Name),
|
|
})
|
|
}
|
|
return nil, nil
|
|
}
|
|
case *ast.SelectorExpr:
|
|
if expr.Sel.Name != "NewDesc" {
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(expr.Sel.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing desc with function %s is not supported", expr.Sel.Name),
|
|
})
|
|
}
|
|
return nil, nil
|
|
}
|
|
default:
|
|
if v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Pos: v.fs.Position(expr.Pos()),
|
|
Metric: "",
|
|
Text: fmt.Sprintf("parsing desc of %T is not supported", expr),
|
|
})
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// k8s.io/component-base/metrics.NewDesc has 6 args
|
|
// while prometheus.NewDesc has 4 args
|
|
if len(call.Args) < 4 && v.strict {
|
|
v.issues = append(v.issues, Issue{
|
|
Metric: "",
|
|
Pos: v.fs.Position(call.Pos()),
|
|
Text: "NewDesc should have at least 4 args",
|
|
})
|
|
return nil, nil
|
|
}
|
|
|
|
name, ok = v.parseValue("fqName", call.Args[0])
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
help, ok = v.parseValue("help", call.Args[1])
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
return &name, &help
|
|
}
|
|
|
|
func mustUnquote(str string) string {
|
|
stringLiteral, err := strconv.Unquote(str)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return stringLiteral
|
|
}
|
|
|
|
func getConstMetricType(name string) *dto.MetricType {
|
|
metricType := dto.MetricType_UNTYPED
|
|
if name == "CounterValue" {
|
|
metricType = dto.MetricType_COUNTER
|
|
} else if name == "GaugeValue" {
|
|
metricType = dto.MetricType_GAUGE
|
|
}
|
|
|
|
return &metricType
|
|
}
|