Skip to content

Commit

Permalink
Add monitoring linter
Browse files Browse the repository at this point in the history
Signed-off-by: assafad <[email protected]>
  • Loading branch information
assafad authored and nunnatsa committed Mar 4, 2024
1 parent 456a50e commit 11b8ae2
Show file tree
Hide file tree
Showing 1,160 changed files with 580,469 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ promlinter-build:
.PHONY: promlinter-push
promlinter-push:
${CONTAINER_RUNTIME} push ${IMG}

.PHONY: monitoringlinter-unit-test
monitoringlinter-unit-test:
cd monitoringlinter && go test ./...

.PHONY: monitoringlinter-build
monitoringlinter-build:
cd monitoringlinter && go build ./cmd/monitoringlinter

.PHONY: monitoringlinter-test
monitoringlinter-test: monitoringlinter-build
cd monitoringlinter && ./tests/e2e.sh
41 changes: 41 additions & 0 deletions monitoringlinter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# monitoring-linter

Monitoring Linter is a Golang linter designed to ensure that in Kubernetes operator projects,
monitoring-related practices are implemented within the `pkg/monitoring` directory using [operator-observability](https://github.com/machadovilaca/operator-observability/tree/main) methods.
It verifies that all metrics, alerts and recording rules registrations are centralized in this directory.
The use of [Prometheus registration methods](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Registerer) is restricted across the entire project.

## Installation

```shell
go install github.com/kubevirt/monitoring/monitoringlinter/cmd/monitoringlinter@latest
```

## Usage
Once installed, you can run the Monitoring Linter against your Kubernetes operator project by using the following command:

```shell
monitoringlinter ./...
```


## Linter Rules

- The following Prometheus registration methods calls are restricted across all the project:
```go
prometheus.Register()
prometheus.MustRegister()
```

- The following operator-observability methods calls are allowed only within `pkg/monitoring` directory:
```go
operatormetrics.RegisterMetrics()
operatorrules.RegisterAlerts()
operatorrules.RegisterRecordingRules()
```

- Examples for calls that are allowed across all the project, as they are not registering metrics, alerts or recording rules:
```go
prometheus.NewHistogram()
operatormetrics.ListMetrics()
```
121 changes: 121 additions & 0 deletions monitoringlinter/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package monitoringlinter

import (
"go/ast"
"path"
"strings"

"golang.org/x/tools/go/analysis"
)

const (
prometheusImportPath = `"github.com/prometheus/client_golang/prometheus"`
operatorMetricsImportPath = `"github.com/machadovilaca/operator-observability/pkg/operatormetrics"`
operatorRulesImportPath = `"github.com/machadovilaca/operator-observability/pkg/operatorrules"`
)

// NewAnalyzer returns an Analyzer.
func NewAnalyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "monitoringlinter",
Doc: "Ensures that in Kubernetes operators projects, monitoring related practices are implemented " +
"within pkg/monitoring directory, using operator-observability packages.",
Run: run,
}
}

// run is the main assertion function.
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
prometheusLocalName, prometheusIsImported := getPackageLocalName(file, prometheusImportPath)
operatorMetricsLocalName, OperatorMetricsIsImported := getPackageLocalName(file, operatorMetricsImportPath)
operatorRulesLocalName, operatorRulesIsImported := getPackageLocalName(file, operatorRulesImportPath)

if !prometheusIsImported && !OperatorMetricsIsImported && !operatorRulesIsImported {
continue // no monitoring related packages are imported => nothing to do in this file;
}

ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}

selectorExpr, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}

ident, ok := selectorExpr.X.(*ast.Ident)
if !ok {
return true
}

methodPackage := ident.Name
methodName := selectorExpr.Sel.Name

if prometheusIsImported && methodPackage == prometheusLocalName {
checkPrometheusMethodCall(methodName, pass, node)
return true

}

if !isMonitoringDir(pass.Fset.File(file.Pos()).Name()) {
if OperatorMetricsIsImported && methodPackage == operatorMetricsLocalName {
checkOperatorMetricsMethodCall(methodName, pass, node)
return true
}

if operatorRulesIsImported && methodPackage == operatorRulesLocalName {
checkOperatorRulesMethodCall(methodName, pass, node)
return true
}
}

return true
})
}

return nil, nil
}

// checkPrometheusMethodCall checks if Prometheus method call should be reported.
func checkPrometheusMethodCall(methodName string, pass *analysis.Pass, node ast.Node) {
if methodName == "Register" || methodName == "MustRegister" {
pass.Reportf(node.Pos(), "monitoring-linter: metrics should be registered only within pkg/monitoring directory, "+
"using operator-observability packages.")
}
}

// checkOperatorMetricsMethodCall checks if operatormetrics method call should be reported.
func checkOperatorMetricsMethodCall(methodName string, pass *analysis.Pass, node ast.Node) {
if methodName == "RegisterMetrics" {
pass.Reportf(node.Pos(), "monitoring-linter: metrics should be registered only within pkg/monitoring directory.")
}
}

// checkOperatorRulesMethodCall checks if operatorrules method call should be reported.
func checkOperatorRulesMethodCall(methodName string, pass *analysis.Pass, node ast.Node) {
if methodName == "RegisterAlerts" || methodName == "RegisterRecordingRules" {
pass.Reportf(node.Pos(), "monitoring-linter: alerts and recording rules should be registered only within pkg/monitoring directory.")
}
}

// getPackageLocalName returns the name a package was imported with in the file.
// e.g. import prom "github.com/prometheus/client_golang/prometheus"
func getPackageLocalName(file *ast.File, importPath string) (string, bool) {
for _, imp := range file.Imports {
if imp.Path.Value == importPath {
if name := imp.Name.String(); name != "<nil>" {
return name, true
}
pathWithoutQuotes := strings.Trim(importPath, `"`)
return path.Base(pathWithoutQuotes), true
}
}
return "", false
}

func isMonitoringDir(filePath string) bool {
return strings.Contains(filePath, "pkg/monitoring")
}
37 changes: 37 additions & 0 deletions monitoringlinter/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package monitoringlinter_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"

"github.com/monitoring/monitoringlinter"
)

func TestAllUseCases(t *testing.T) {
for _, testcase := range []struct {
name string
data string
}{
{
name: "Verify metrics registrations in pkg/controller, using operatorobservability, is reported.",
data: "a/testrepo/pkg/controller/operatorobservability",
},
{
name: "Verify metrics registrations in pkg/controller, using prometheus, is reported.",
data: "a/testrepo/pkg/controller/prometheus",
},
{
name: "Verify metrics registrations in pkg/monitoring, using operatorobservability, is not reported.",
data: "a/testrepo/pkg/monitoring/operatorobservability",
},
{
name: "Verify metrics registrations in pkg/monitoring, using prometheus, is reported.",
data: "a/testrepo/pkg/monitoring/prometheus",
},
} {
t.Run(testcase.name, func(tt *testing.T) {
analysistest.Run(tt, analysistest.TestData(), monitoringlinter.NewAnalyzer(), testcase.data)
})
}
}
11 changes: 11 additions & 0 deletions monitoringlinter/cmd/monitoringlinter/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import (
"golang.org/x/tools/go/analysis/singlechecker"

"github.com/monitoring/monitoringlinter"
)

func main() {
singlechecker.Main(monitoringlinter.NewAnalyzer())
}
7 changes: 7 additions & 0 deletions monitoringlinter/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/monitoring/monitoringlinter

go 1.21

require golang.org/x/tools v0.18.0

require golang.org/x/mod v0.15.0 // indirect
6 changes: 6 additions & 0 deletions monitoringlinter/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
36 changes: 36 additions & 0 deletions monitoringlinter/testdata/src/a/testrepo/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module testrepo

go 1.21

require (
github.com/machadovilaca/operator-observability v0.0.14
github.com/prometheus/client_golang v1.18.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.68.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.28.1 // indirect
k8s.io/apimachinery v0.28.1 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
)
Loading

0 comments on commit 11b8ae2

Please sign in to comment.