workgroups/vendor/github.com/yeya24/promlinter/promlinter.go

665 lines
15 KiB
Go
Raw Normal View History

2021-09-24 17:34:17 +02:00
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: &currentMetric, 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: &currentMetric, 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
}