├── .gitignore ├── birdwatcher ├── testdata │ ├── config │ │ ├── no_services │ │ ├── invalidtoml │ │ ├── service_noprefixes │ │ ├── service_nocommand │ │ ├── minimal │ │ ├── service_invalidprefix │ │ ├── service_duplicateprefix │ │ └── overridden │ └── bird │ │ ├── config_empty │ │ ├── config_compat │ │ └── config ├── doc.go ├── action.go ├── templates │ └── functions.tpl ├── servicecheck_test.go ├── prefixset.go ├── prefixset_test.go ├── bird.go ├── config.go ├── bird_test.go ├── healthcheck_test.go ├── config_test.go ├── healthcheck.go └── servicecheck.go ├── .github └── workflows │ ├── golangci-lint.yml │ └── go.yml ├── dist ├── systemd │ └── birdwatcher.service └── birdwatcher.conf ├── go.mod ├── .goreleaser.yaml ├── go.sum ├── main.go ├── README.md └── .golangci.yml /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/no_services: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/invalidtoml: -------------------------------------------------------------------------------- 1 | sdffd 2 | dfsfds 3 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/service_noprefixes: -------------------------------------------------------------------------------- 1 | [services] 2 | [services."foo"] 3 | command = "/usr/bin/true" 4 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/service_nocommand: -------------------------------------------------------------------------------- 1 | [services] 2 | [services."foo"] 3 | prefixes = ["192.168.0.0/24"] 4 | -------------------------------------------------------------------------------- /birdwatcher/doc.go: -------------------------------------------------------------------------------- 1 | // Package birdwatcher holds all the functional logic for the birdwatcher application 2 | package birdwatcher 3 | -------------------------------------------------------------------------------- /birdwatcher/testdata/bird/config_empty: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT MANUALLY 2 | function match_route() -> bool 3 | { 4 | return false; 5 | } 6 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/minimal: -------------------------------------------------------------------------------- 1 | [services] 2 | [services."foo"] 3 | command = "/usr/bin/true" 4 | prefixes = ["192.168.0.0/24"] 5 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/service_invalidprefix: -------------------------------------------------------------------------------- 1 | [services] 2 | [services."foo"] 3 | command = "/usr/bin/true" 4 | prefixes = ["foobar"] 5 | -------------------------------------------------------------------------------- /birdwatcher/testdata/bird/config_compat: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT MANUALLY 2 | function other_function() 3 | { 4 | return net ~ [ 5 | 5.6.7.8/32, 6 | 6.7.8.0/26, 7 | 7.8.9.0/24 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /birdwatcher/testdata/bird/config: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT MANUALLY 2 | function match_route() -> bool 3 | { 4 | return net ~ [ 5 | 1.2.3.4/32, 6 | 2.3.4.0/26, 7 | 3.4.5.0/24, 8 | 4.5.0.0/21 9 | ]; 10 | } 11 | -------------------------------------------------------------------------------- /birdwatcher/action.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import "net" 4 | 5 | // Action reflects the change to a specific state for a service and its prefixes 6 | type Action struct { 7 | Service *ServiceCheck 8 | State ServiceState 9 | Prefixes []net.IPNet 10 | } 11 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/service_duplicateprefix: -------------------------------------------------------------------------------- 1 | [services] 2 | [services."foo"] 3 | command = "/usr/bin/true" 4 | prefixes = ["192.168.0.0/24"] 5 | 6 | [services."bar"] 7 | command = "/usr/bin/true" 8 | prefixes = ["192.168.0.0/24", "192.168.1.0/24"] 9 | -------------------------------------------------------------------------------- /birdwatcher/templates/functions.tpl: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT MANUALLY 2 | {{- range .Collections }} 3 | function {{.FunctionName}}(){{- if not $.CompatBird213 }} -> bool{{- end }} 4 | { 5 | {{- with .Prefixes}} 6 | return net ~ [ 7 | {{- range prefixPad . }} 8 | {{.}} 9 | {{- end }} 10 | ]; 11 | {{- else }} 12 | return false; 13 | {{- end }} 14 | } 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v2 17 | with: 18 | version: latest 19 | -------------------------------------------------------------------------------- /dist/systemd/birdwatcher.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=healthchecker for BIRD-anycasted services 3 | Documentation=https://github.com/skoef/birdwatcher 4 | After=bird.service 5 | 6 | [Service] 7 | Type=notify 8 | Environment=CONFIG_FILE=/etc/birdwatcher.conf 9 | ExecStartPre=/usr/sbin/birdwatcher -config $CONFIG_FILE -check-config 10 | ExecStart=/usr/sbin/birdwatcher -config $CONFIG_FILE -systemd 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /birdwatcher/testdata/config/overridden: -------------------------------------------------------------------------------- 1 | configfile = "/etc/birdwatcher.conf" 2 | reloadcommand = "/sbin/birdc configure" 3 | compatbird213 = true 4 | 5 | [prometheus] 6 | enabled = true 7 | port = 1234 8 | path = "/something" 9 | 10 | [services] 11 | [services."foo"] 12 | command = "/bin/true" 13 | prefixes = ["192.168.0.0/24"] 14 | functionname = "foo_bar" 15 | interval = 10 16 | rise = 20 17 | fail = 30 18 | timeout = "40s" 19 | [services."bar"] 20 | command = "/bin/false" 21 | prefixes = ["192.168.1.0/24", "fc00::/7"] 22 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: go 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.23' 21 | - name: Test 22 | run: go test -v ./... 23 | - name: GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | if: startsWith(github.ref, 'refs/tags/') 26 | with: 27 | version: '~> v2' 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /dist/birdwatcher.conf: -------------------------------------------------------------------------------- 1 | # This is the default birdwatcher config file. 2 | # Refer to https://github.com/skoef/birdwatcher for all configuration options 3 | 4 | # the config file BIRD should be including 5 | configfile = "/etc/bird/birdwatcher.conf" 6 | # reload command birdwatcher will call when configfile was updated 7 | reloadcommand = "/usr/sbin/birdc configure" 8 | 9 | # configuration about the prometheus metrics exporter 10 | [prometheus] 11 | enabled = false 12 | # TCP port to expose the prometheus exporter on 13 | port = 9091 14 | # HTTP path to expose the prometheus exporter on 15 | path = "/metrics" 16 | 17 | [services] 18 | # example service 19 | # 20 | # [services."foo"] 21 | # command = "/usr/bin/my_check.sh" 22 | # functionname = "match_route" 23 | # interval = 1 24 | # timeout = "10s" 25 | # fail = 1 26 | # rise = 1 27 | # prefixes = ["192.168.0.0/24", "fc00::/7"] 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skoef/birdwatcher 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.4.0 7 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf 8 | github.com/prometheus/client_golang v1.20.5 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/stretchr/testify v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/klauspost/compress v1.17.9 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/kylelemons/godebug v1.1.0 // indirect 20 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/prometheus/client_model v0.6.1 // indirect 23 | github.com/prometheus/common v0.55.0 // indirect 24 | github.com/prometheus/procfs v0.15.1 // indirect 25 | golang.org/x/sys v0.22.0 // indirect 26 | google.golang.org/protobuf v1.34.2 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /birdwatcher/servicecheck_test.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestServiceCheckPushChannel(t *testing.T) { 12 | t.Parallel() 13 | 14 | buf := make(chan *Action) 15 | sc := ServiceCheck{ 16 | disablePrefixCheck: true, 17 | name: "test", 18 | Command: "/usr/bin/true", 19 | Fail: 3, 20 | Rise: 2, 21 | Interval: 1, 22 | Timeout: 2 * time.Second, 23 | prefixes: []net.IPNet{ 24 | {IP: net.IP{1, 2, 3, 4}, Mask: net.IPMask{255, 255, 255, 0}}, 25 | }, 26 | } 27 | 28 | // start the check 29 | go sc.Start(&buf) 30 | defer sc.Stop() 31 | 32 | // wait for action on channel 33 | action := <-buf 34 | assert.Equal(t, ServiceStateUp, action.State) 35 | assert.Len(t, action.Prefixes, 1) 36 | assert.Equal(t, sc.prefixes[0], action.Prefixes[0]) 37 | 38 | // all of a sudden, the check gives wrong result 39 | sc.Command = "/usr/bin/false" 40 | 41 | // wait for action on channel 42 | action = <-buf 43 | assert.Equal(t, ServiceStateDown, action.State) 44 | } 45 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 3 | 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | - go mod tidy 9 | 10 | dist: build 11 | 12 | builds: 13 | - env: 14 | - CGO_ENABLED=0 15 | goos: 16 | - linux 17 | goarch: 18 | - amd64 19 | - arm64 20 | 21 | archives: 22 | - formats: 23 | - binary 24 | 25 | changelog: 26 | sort: asc 27 | filters: 28 | exclude: 29 | - "^docs:" 30 | - "^test:" 31 | - '^Merge pull request' 32 | - '^Merge branch' 33 | 34 | nfpms: 35 | - package_name: birdwatcher 36 | homepage: https://github.com/skoef/birdwatcher 37 | maintainer: "Reinier Schoof " 38 | description: healthchecker for BIRD-anycasted services 39 | license: MIT 40 | bindir: /usr/sbin 41 | contents: 42 | - src: dist/systemd/birdwatcher.service 43 | dst: /lib/systemd/system/birdwatcher.service 44 | packager: deb 45 | file_info: 46 | owner: root 47 | group: root 48 | mode: 0644 49 | - src: dist/systemd/birdwatcher.service 50 | dst: /usr/lib/systemd/system/birdwatcher.service 51 | packager: rpm 52 | file_info: 53 | owner: root 54 | group: root 55 | mode: 0644 56 | - src: dist/birdwatcher.conf 57 | dst: /etc/birdwatcher.conf 58 | type: config|noreplace 59 | file_info: 60 | owner: root 61 | group: root 62 | mode: 0644 63 | formats: 64 | - deb 65 | - rpm 66 | overrides: 67 | deb: 68 | dependencies: 69 | - libc6 70 | rpm: 71 | dependencies: 72 | - glibc 73 | -------------------------------------------------------------------------------- /birdwatcher/prefixset.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "bytes" 5 | // use embed for embedding the function template 6 | _ "embed" 7 | "net" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // PrefixCollection represents prefixsets per function name 13 | type PrefixCollection map[string]*PrefixSet 14 | 15 | // PrefixSet represents a list of prefixes alongside a function name 16 | type PrefixSet struct { 17 | prefixes []net.IPNet 18 | functionName string 19 | } 20 | 21 | // NewPrefixSet returns a new prefixset with given function name 22 | func NewPrefixSet(functionName string) *PrefixSet { 23 | return &PrefixSet{functionName: functionName} 24 | } 25 | 26 | // FunctionName returns the function name 27 | func (p PrefixSet) FunctionName() string { 28 | return p.functionName 29 | } 30 | 31 | // Prefixes returns the prefixes 32 | func (p PrefixSet) Prefixes() []net.IPNet { 33 | return p.prefixes 34 | } 35 | 36 | // Add adds a prefix to the PrefixSet if it wasn't already in it 37 | func (p *PrefixSet) Add(prefix net.IPNet) { 38 | pLog := log.WithFields(log.Fields{ 39 | "prefix": prefix, 40 | }) 41 | pLog.Debug("adding prefix to prefix set") 42 | 43 | // skip prefix if it's already in the list 44 | // shouldn't really happen though 45 | for _, pref := range p.prefixes { 46 | if pref.IP.Equal(prefix.IP) && bytes.Equal(pref.Mask, prefix.Mask) { 47 | pLog.Warn("duplicate prefix, skipping") 48 | 49 | return 50 | } 51 | } 52 | 53 | // add prefix to the prefix set 54 | p.prefixes = append(p.prefixes, prefix) 55 | } 56 | 57 | // Remove removes a prefix from the PrefixSet 58 | func (p *PrefixSet) Remove(prefix net.IPNet) { 59 | pLog := log.WithFields(log.Fields{ 60 | "prefix": prefix, 61 | }) 62 | pLog.Debug("removing prefix from prefix set") 63 | 64 | // go over global prefix list and remove it when found 65 | for i, pref := range p.prefixes { 66 | if pref.IP.Equal(prefix.IP) && bytes.Equal(pref.Mask, prefix.Mask) { 67 | // remove entry from slice, fast approach 68 | p.prefixes[i] = p.prefixes[len(p.prefixes)-1] // copy last element to index i 69 | p.prefixes = p.prefixes[:len(p.prefixes)-1] // truncate slice 70 | 71 | return 72 | } 73 | } 74 | 75 | pLog.Warn("prefix not found in prefix set, skipping") 76 | } 77 | -------------------------------------------------------------------------------- /birdwatcher/prefixset_test.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrefixSet_Add(t *testing.T) { 11 | t.Parallel() 12 | 13 | p := NewPrefixSet("foobar") 14 | // should be empty 15 | assert.Empty(t, p.prefixes) 16 | 17 | // add some prefixes 18 | for _, pref := range []string{"1.2.3.0/24", "2.3.4.0/24", "3.4.5.0/24", "3.4.5.0/26"} { 19 | _, prf, _ := net.ParseCIDR(pref) 20 | p.Add(*prf) 21 | } 22 | 23 | // check if all 4 prefixes are there 24 | if assert.Len(t, p.prefixes, 4) { 25 | assert.Equal(t, "1.2.3.0/24", p.prefixes[0].String()) 26 | assert.Equal(t, "2.3.4.0/24", p.prefixes[1].String()) 27 | assert.Equal(t, "3.4.5.0/24", p.prefixes[2].String()) 28 | assert.Equal(t, "3.4.5.0/26", p.prefixes[3].String()) 29 | } 30 | 31 | // try to add a duplicate prefix 32 | _, prf, _ := net.ParseCIDR("1.2.3.0/24") 33 | p.Add(*prf) 34 | 35 | // this shouldn't have changed the content of the PrefixSet 36 | if assert.Len(t, p.prefixes, 4) { 37 | assert.Equal(t, "1.2.3.0/24", p.prefixes[0].String()) 38 | assert.Equal(t, "2.3.4.0/24", p.prefixes[1].String()) 39 | assert.Equal(t, "3.4.5.0/24", p.prefixes[2].String()) 40 | assert.Equal(t, "3.4.5.0/26", p.prefixes[3].String()) 41 | } 42 | } 43 | 44 | func TestPrefixSet_Remove(t *testing.T) { 45 | t.Parallel() 46 | 47 | p := NewPrefixSet("foobar") 48 | 49 | // add some prefixes 50 | prefixes := make([]net.IPNet, 4) 51 | 52 | for i, pref := range []string{"1.2.3.0/24", "2.3.4.0/24", "3.4.5.0/24", "3.4.5.0/26"} { 53 | _, prf, _ := net.ParseCIDR(pref) 54 | p.Add(*prf) 55 | prefixes[i] = *prf 56 | } 57 | 58 | // remove last prefix 59 | // array should only be truncated 60 | p.Remove(prefixes[3]) 61 | 62 | if assert.Len(t, p.prefixes, 3) { 63 | assert.Equal(t, "1.2.3.0/24", p.prefixes[0].String()) 64 | assert.Equal(t, "2.3.4.0/24", p.prefixes[1].String()) 65 | assert.Equal(t, "3.4.5.0/24", p.prefixes[2].String()) 66 | } 67 | 68 | // remove first prefix 69 | // last prefix will be first now 70 | p.Remove(prefixes[0]) 71 | 72 | if assert.Len(t, p.prefixes, 2) { 73 | assert.Equal(t, "3.4.5.0/24", p.prefixes[0].String()) 74 | assert.Equal(t, "2.3.4.0/24", p.prefixes[1].String()) 75 | } 76 | 77 | // removing same prefix again, should make no difference 78 | p.Remove(prefixes[0]) 79 | 80 | if assert.Len(t, p.prefixes, 2) { 81 | assert.Equal(t, "3.4.5.0/24", p.prefixes[0].String()) 82 | assert.Equal(t, "2.3.4.0/24", p.prefixes[1].String()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /birdwatcher/bird.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "net" 8 | "os" 9 | "text/template" 10 | ) 11 | 12 | //go:embed templates/functions.tpl 13 | var functionsTemplate string 14 | 15 | // make sure prefixPad can be used in templates 16 | var tplFuncs = template.FuncMap{ 17 | "prefixPad": prefixPad, 18 | } 19 | 20 | var errConfigIdentical = errors.New("configuration file is identical") 21 | 22 | func updateBirdConfig(config Config, prefixes PrefixCollection) error { 23 | // write config to temp file 24 | tmpFilename := config.ConfigFile + ".tmp" 25 | // make sure we don't keep tmp file around when something goes wrong 26 | defer func(x string) { 27 | if _, err := os.Stat(x); !os.IsNotExist(err) { 28 | //nolint:errcheck,gosec // it's just a temp file anyway 29 | os.Remove(tmpFilename) 30 | } 31 | }(tmpFilename) 32 | 33 | if err := writeBirdConfig(tmpFilename, prefixes, config.CompatBird213); err != nil { 34 | return err 35 | } 36 | 37 | // compare new file with original config file 38 | if compareFiles(tmpFilename, config.ConfigFile) { 39 | return errConfigIdentical 40 | } 41 | 42 | // move tmp file to right place 43 | return os.Rename(tmpFilename, config.ConfigFile) 44 | } 45 | 46 | func writeBirdConfig(filename string, prefixes PrefixCollection, compatBird213 bool) error { 47 | var err error 48 | 49 | // open file 50 | f, err := os.Create(filename) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | tmpl := template.Must(template.New("func").Funcs(tplFuncs).Parse(functionsTemplate)) 56 | 57 | tplBody := struct { 58 | Collections PrefixCollection 59 | CompatBird213 bool 60 | }{ 61 | Collections: prefixes, 62 | CompatBird213: compatBird213, 63 | } 64 | 65 | var buf bytes.Buffer 66 | if err := tmpl.Execute(&buf, tplBody); err != nil { 67 | return err 68 | } 69 | 70 | // write data to file 71 | _, err = f.Write(buf.Bytes()) 72 | 73 | return err 74 | } 75 | 76 | // prefixPad is a helper function for the template 77 | // basically returns CIDR notations per IPNet, each suffixed with a , except for 78 | // the last entry 79 | func prefixPad(x []net.IPNet) []string { 80 | pp := make([]string, len(x)) 81 | for i, p := range x { 82 | pp[i] = p.String() 83 | if i < len(x)-1 { 84 | pp[i] += "," 85 | } 86 | } 87 | 88 | return pp 89 | } 90 | 91 | func compareFiles(fileA, fileB string) bool { 92 | data, err := os.ReadFile(fileA) 93 | if err != nil { 94 | return false 95 | } 96 | 97 | datb, err := os.ReadFile(fileB) 98 | if err != nil { 99 | return false 100 | } 101 | 102 | return bytes.Equal(data, datb) 103 | } 104 | -------------------------------------------------------------------------------- /birdwatcher/config.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "time" 9 | 10 | "github.com/BurntSushi/toml" 11 | ) 12 | 13 | // Config holds definitions from configuration file 14 | type Config struct { 15 | ConfigFile string 16 | ReloadCommand string 17 | CompatBird213 bool 18 | Prometheus PrometheusConfig 19 | Services map[string]*ServiceCheck 20 | } 21 | 22 | // PrometheusConfig holds configuration related to prometheus 23 | type PrometheusConfig struct { 24 | Enabled bool 25 | Port int 26 | Path string 27 | } 28 | 29 | const ( 30 | defaultConfigFile = "/etc/bird/birdwatcher.conf" 31 | defaultReloadCommand = "/usr/sbin/birdc configure" 32 | defaultPrometheusPort = 9091 33 | defaultPrometheusPath = "/metrics" 34 | 35 | defaultFunctionName = "match_route" 36 | defaultCheckInterval = 1 37 | defaultServiceTimeout = 10 * time.Second 38 | defaultServiceFail = 1 39 | defaultServiceRise = 1 40 | ) 41 | 42 | // ReadConfig reads TOML config from given file into given Config or returns 43 | // error on invalid configuration 44 | func ReadConfig(conf *Config, configFile string) error { 45 | if _, err := os.Stat(configFile); err != nil { 46 | return fmt.Errorf("config file %s not found", configFile) 47 | } 48 | 49 | if _, err := toml.DecodeFile(configFile, conf); err != nil { 50 | errMsg := err.Error() 51 | 52 | var parseErr toml.ParseError 53 | 54 | if errors.As(err, &parseErr) { 55 | errMsg = parseErr.ErrorWithPosition() 56 | } 57 | 58 | return fmt.Errorf("could not parse config: %s", errMsg) 59 | } 60 | 61 | if conf.ConfigFile == "" { 62 | conf.ConfigFile = defaultConfigFile 63 | } 64 | 65 | if conf.ReloadCommand == "" { 66 | conf.ReloadCommand = defaultReloadCommand 67 | } 68 | 69 | if conf.Prometheus.Path == "" { 70 | conf.Prometheus.Path = defaultPrometheusPath 71 | } 72 | 73 | if conf.Prometheus.Port == 0 { 74 | conf.Prometheus.Port = defaultPrometheusPort 75 | } 76 | 77 | if len(conf.Services) == 0 { 78 | return errors.New("no services configured") 79 | } 80 | 81 | allPrefixes := map[string]bool{} 82 | 83 | for name, s := range conf.Services { 84 | // copy service name to ServiceCheck 85 | s.name = name 86 | 87 | if s.FunctionName == "" { 88 | s.FunctionName = defaultFunctionName 89 | } 90 | 91 | // validate service 92 | if err := validateService(s); err != nil { 93 | return err 94 | } 95 | 96 | // convert all prefixes into ipnets 97 | s.prefixes = make([]net.IPNet, len(s.Prefixes)) 98 | for i, p := range s.Prefixes { 99 | _, ipn, err := net.ParseCIDR(p) 100 | if err != nil { 101 | return fmt.Errorf("could not parse prefix for service %s: %w", name, err) 102 | } 103 | 104 | s.prefixes[i] = *ipn 105 | 106 | // validate whether the prefixes overlap 107 | if _, found := allPrefixes[ipn.String()]; found { 108 | return fmt.Errorf("duplicate prefix %s found", ipn.String()) 109 | } 110 | 111 | allPrefixes[ipn.String()] = true 112 | } 113 | 114 | // map name to each search 115 | conf.Services[name] = s 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func validateService(s *ServiceCheck) error { 122 | if s.Command == "" { 123 | return fmt.Errorf("service %s has no command set", s.name) 124 | } 125 | 126 | if s.Interval <= 0 { 127 | s.Interval = defaultCheckInterval 128 | } 129 | 130 | if s.Timeout <= 0 { 131 | s.Timeout = defaultServiceTimeout 132 | } 133 | 134 | if s.Fail <= 0 { 135 | s.Fail = defaultServiceFail 136 | } 137 | 138 | if s.Rise <= 0 { 139 | s.Rise = defaultServiceRise 140 | } 141 | 142 | if len(s.Prefixes) == 0 { 143 | return fmt.Errorf("service %s has no prefixes set", s.name) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // GetServices converts the services map into a slice of ServiceChecks and returns it 150 | func (c Config) GetServices() []*ServiceCheck { 151 | sc := make([]*ServiceCheck, len(c.Services)) 152 | j := 0 153 | 154 | for i := range c.Services { 155 | sc[j] = c.Services[i] 156 | j++ 157 | } 158 | 159 | return sc 160 | } 161 | -------------------------------------------------------------------------------- /birdwatcher/bird_test.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWriteBirdConfig(t *testing.T) { 14 | t.Parallel() 15 | 16 | t.Run("empty config", func(t *testing.T) { 17 | t.Parallel() 18 | 19 | // open tempfile 20 | tmpFile, err := os.CreateTemp(t.TempDir(), "bird_test") 21 | require.NoError(t, err) 22 | defer os.Remove(tmpFile.Name()) 23 | 24 | prefixes := make(PrefixCollection) 25 | prefixes["match_route"] = NewPrefixSet("match_route") 26 | 27 | // write bird config with empty prefix list 28 | err = writeBirdConfig(tmpFile.Name(), prefixes, false) 29 | require.NoError(t, err) 30 | 31 | // read data from temp file and compare it to file fixture 32 | data, err := os.ReadFile(tmpFile.Name()) 33 | require.NoError(t, err) 34 | 35 | fixture, err := os.ReadFile("testdata/bird/config_empty") 36 | require.NoError(t, err) 37 | 38 | assert.Equal(t, string(fixture), string(data)) 39 | }) 40 | 41 | t.Run("one prefixset", func(t *testing.T) { 42 | t.Parallel() 43 | 44 | // open tempfile 45 | tmpFile, err := os.CreateTemp(t.TempDir(), "bird_test") 46 | require.NoError(t, err) 47 | defer os.Remove(tmpFile.Name()) 48 | 49 | prefixes := make(PrefixCollection) 50 | prefixes["match_route"] = NewPrefixSet("match_route") 51 | 52 | for _, pref := range []string{"1.2.3.4/32", "2.3.4.5/26", "3.4.5.6/24", "4.5.6.7/21"} { 53 | _, prf, _ := net.ParseCIDR(pref) 54 | prefixes["match_route"].Add(*prf) 55 | } 56 | 57 | // write bird config to it 58 | err = writeBirdConfig(tmpFile.Name(), prefixes, false) 59 | require.NoError(t, err) 60 | 61 | // read data from temp file and compare it to file fixture 62 | data, err := os.ReadFile(tmpFile.Name()) 63 | require.NoError(t, err) 64 | 65 | fixture, err := os.ReadFile("testdata/bird/config") 66 | require.NoError(t, err) 67 | 68 | assert.Equal(t, string(fixture), string(data)) 69 | }) 70 | 71 | t.Run("one prefix, compat", func(t *testing.T) { 72 | t.Parallel() 73 | 74 | // open tempfile 75 | tmpFile, err := os.CreateTemp(t.TempDir(), "bird_test") 76 | require.NoError(t, err) 77 | defer os.Remove(tmpFile.Name()) 78 | 79 | prefixes := make(PrefixCollection) 80 | 81 | prefixes["other_function"] = NewPrefixSet("other_function") 82 | for _, pref := range []string{"5.6.7.8/32", "6.7.8.9/26", "7.8.9.10/24"} { 83 | _, prf, _ := net.ParseCIDR(pref) 84 | prefixes["other_function"].Add(*prf) 85 | } 86 | 87 | // write bird config to it 88 | err = writeBirdConfig(tmpFile.Name(), prefixes, true) 89 | require.NoError(t, err) 90 | 91 | // read data from temp file and compare it to file fixture 92 | data, err := os.ReadFile(tmpFile.Name()) 93 | require.NoError(t, err) 94 | 95 | fixture, err := os.ReadFile("testdata/bird/config_compat") 96 | require.NoError(t, err) 97 | 98 | assert.Equal(t, string(fixture), string(data)) 99 | }) 100 | } 101 | 102 | func TestPrefixPad(t *testing.T) { 103 | t.Parallel() 104 | 105 | prefixes := make([]net.IPNet, 4) 106 | 107 | for i, pref := range []string{"1.2.3.0/24", "2.3.4.0/24", "3.4.5.0/24", "3.4.5.0/26"} { 108 | _, prf, _ := net.ParseCIDR(pref) 109 | prefixes[i] = *prf 110 | } 111 | 112 | padded := prefixPad(prefixes) 113 | assert.Equal(t, "1.2.3.0/24,2.3.4.0/24,3.4.5.0/24,3.4.5.0/26", strings.Join(padded, "")) 114 | } 115 | 116 | func TestBirdCompareFiles(t *testing.T) { 117 | t.Parallel() 118 | 119 | // open 2 tempfiles 120 | tmpFileA, err := os.CreateTemp(t.TempDir(), "bird_test") 121 | require.NoError(t, err) 122 | defer os.Remove(tmpFileA.Name()) 123 | 124 | tmpFileB, err := os.CreateTemp(t.TempDir(), "bird_test") 125 | require.NoError(t, err) 126 | defer os.Remove(tmpFileB.Name()) 127 | 128 | // write same string to both files 129 | _, err = tmpFileA.WriteString("test") 130 | require.NoError(t, err) 131 | _, err = tmpFileB.WriteString("test") 132 | require.NoError(t, err) 133 | 134 | assert.True(t, compareFiles(tmpFileA.Name(), tmpFileB.Name())) 135 | 136 | // write something else to one file 137 | _, err = tmpFileB.WriteString("test123") 138 | require.NoError(t, err) 139 | 140 | assert.False(t, compareFiles(tmpFileA.Name(), tmpFileB.Name())) 141 | } 142 | -------------------------------------------------------------------------------- /birdwatcher/healthcheck_test.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/prometheus/client_golang/prometheus/testutil" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHealthCheck_addPrefix(t *testing.T) { 12 | t.Parallel() 13 | 14 | hc := HealthCheck{} 15 | assert.Nil(t, hc.prefixes) 16 | 17 | // adding a prefix should initialise the prefixcollection 18 | // and add the prefix under the right prefixset 19 | _, prefix, _ := net.ParseCIDR("1.2.3.0/24") 20 | hc.addPrefix(&ServiceCheck{name: "svc1", FunctionName: "foo"}, *prefix) 21 | assert.Len(t, hc.prefixes, 1) 22 | assert.Equal(t, *prefix, hc.prefixes["foo"].prefixes[0]) 23 | 24 | assert.InEpsilon(t, 1.0, testutil.ToFloat64(prefixStateMetric.WithLabelValues("svc1", "1.2.3.0/24")), 0.00001) 25 | 26 | _, prefix, _ = net.ParseCIDR("2.3.4.0/24") 27 | hc.addPrefix(&ServiceCheck{name: "svc2", FunctionName: "bar"}, *prefix) 28 | assert.Len(t, hc.prefixes, 2) 29 | assert.Equal(t, *prefix, hc.prefixes["bar"].prefixes[0]) 30 | 31 | assert.InEpsilon(t, 1.0, testutil.ToFloat64(prefixStateMetric.WithLabelValues("svc2", "2.3.4.0/24")), 0.00001) 32 | } 33 | 34 | func TestHealthCheck_removePrefix(t *testing.T) { 35 | t.Parallel() 36 | 37 | hc := HealthCheck{} 38 | assert.Nil(t, hc.prefixes) 39 | 40 | _, prefix, _ := net.ParseCIDR("1.2.3.0/24") 41 | 42 | svc1 := &ServiceCheck{name: "svc1", FunctionName: "foo"} 43 | hc.addPrefix(svc1, *prefix) 44 | assert.Len(t, hc.prefixes, 1) 45 | assert.Len(t, hc.prefixes["foo"].prefixes, 1) 46 | 47 | assert.InEpsilon(t, 1.0, testutil.ToFloat64(prefixStateMetric.WithLabelValues("svc1", "1.2.3.0/24")), 0.00001) 48 | 49 | // this should initialise the prefixset but won't remove any prefixes 50 | svc2 := &ServiceCheck{name: "svc2", FunctionName: "bar"} 51 | hc.removePrefix(svc2, *prefix) 52 | assert.Len(t, hc.prefixes, 2) 53 | assert.Len(t, hc.prefixes["foo"].prefixes, 1) 54 | assert.Empty(t, hc.prefixes["bar"].prefixes) 55 | 56 | assert.Empty(t, testutil.ToFloat64(prefixStateMetric.WithLabelValues("svc2", "1.2.3.0/24"))) 57 | 58 | // remove the prefix from the right prefixset 59 | hc.removePrefix(svc1, *prefix) 60 | assert.Empty(t, hc.prefixes["foo"].prefixes) 61 | 62 | assert.Empty(t, testutil.ToFloat64(prefixStateMetric.WithLabelValues("svc1", "1.2.3.0/24"))) 63 | } 64 | 65 | func TestHealthCheckDidReloadBefore(t *testing.T) { 66 | t.Parallel() 67 | 68 | hc := NewHealthCheck(Config{}) 69 | 70 | // expect both to fail 71 | assert.False(t, hc.didReloadBefore()) 72 | 73 | // should succeed now 74 | hc.reloadedBefore = true 75 | assert.True(t, hc.didReloadBefore()) 76 | 77 | hc.reloadedBefore = false 78 | 79 | // expect to fail again 80 | assert.False(t, hc.didReloadBefore()) 81 | } 82 | 83 | func TestHealthCheck_handleAction(t *testing.T) { 84 | t.Parallel() 85 | 86 | // empty healthcheck 87 | hc := HealthCheck{} 88 | assert.Nil(t, hc.prefixes) 89 | 90 | // create action with state up and 2 prefixes 91 | action := &Action{ 92 | State: ServiceStateUp, 93 | Prefixes: make([]net.IPNet, 2), 94 | } 95 | 96 | var prefix *net.IPNet 97 | _, prefix, _ = net.ParseCIDR("1.2.3.0/24") 98 | action.Prefixes[0] = *prefix 99 | _, prefix, _ = net.ParseCIDR("2.3.4.0/24") 100 | action.Prefixes[1] = *prefix 101 | action.Service = &ServiceCheck{ 102 | FunctionName: "test", 103 | } 104 | 105 | sc := make(chan string) 106 | go func() { 107 | // make sure to read the status channel to prevent blocking handleAction 108 | <-sc 109 | }() 110 | 111 | // handle service state up 112 | hc.handleAction(action, sc) 113 | 114 | if assert.Contains(t, hc.prefixes, "test") { 115 | assert.Len(t, hc.prefixes["test"].prefixes, 2) 116 | } 117 | 118 | // action switches to down for one of the prefixes 119 | action.State = ServiceStateDown 120 | action.Prefixes = action.Prefixes[1:] 121 | go func() { 122 | // make sure to read the status channel to prevent blocking handleAction 123 | <-sc 124 | }() 125 | hc.handleAction(action, sc) 126 | 127 | if assert.Contains(t, hc.prefixes, "test") { 128 | assert.Len(t, hc.prefixes["test"].prefixes, 1) 129 | } 130 | } 131 | 132 | func TestHealthCheck_statusUpdate(t *testing.T) { 133 | t.Parallel() 134 | 135 | // healthcheck with 2 empty services 136 | hc := HealthCheck{services: []*ServiceCheck{ 137 | {name: "foo"}, {name: "bar"}, 138 | }} 139 | 140 | assert.Equal(t, "all 2 service(s) down", hc.statusUpdate()) 141 | hc.services[0].state = ServiceStateUp 142 | assert.Equal(t, "service(s) bar down, 1 service(s) up", hc.statusUpdate()) 143 | hc.services[1].state = ServiceStateUp 144 | assert.Equal(t, "all 2 service(s) up", hc.statusUpdate()) 145 | } 146 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= 8 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 16 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 22 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 23 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 24 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 28 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 29 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 30 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 31 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 32 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 33 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 34 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 35 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 36 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 37 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 38 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 42 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 43 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 45 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 47 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 50 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 53 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main is the main runtime of the birdwatcher application 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/coreos/go-systemd/daemon" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | log "github.com/sirupsen/logrus" 17 | 18 | "github.com/skoef/birdwatcher/birdwatcher" 19 | ) 20 | 21 | const ( 22 | systemdStatusBufferSize = 32 23 | ) 24 | 25 | // variables filled in by goreleaser during release 26 | var ( 27 | version = "devel" 28 | commit = "none" 29 | // date = "unknown" 30 | ) 31 | 32 | //nolint:funlen // we should refactor this a bit 33 | func main() { 34 | // initialize logging 35 | log.SetOutput(os.Stdout) 36 | log.SetLevel(log.InfoLevel) 37 | 38 | var ( 39 | configFile = flag.String("config", "/etc/birdwatcher.conf", "path to config file") 40 | checkConfig = flag.Bool("check-config", false, "check config file and exit") 41 | debugFlag = flag.Bool("debug", false, "increase loglevel to debug") 42 | useSystemd = flag.Bool("systemd", false, "optimize behavior for running under systemd") 43 | versionFlag = flag.Bool("version", false, "show version and exit") 44 | ) 45 | 46 | flag.Parse() 47 | 48 | if *versionFlag { 49 | fmt.Printf("birdwatcher, %s (%s)\n", version, commit) 50 | 51 | return 52 | } 53 | 54 | log.Infof("starting birdwatcher, %s (%s)", version, commit) 55 | 56 | if *debugFlag { 57 | log.SetLevel(log.DebugLevel) 58 | } 59 | 60 | if *useSystemd { 61 | // if we're running under systemd, we don't need the timestamps 62 | // since journald will take care of those 63 | log.SetFormatter(&log.TextFormatter{DisableTimestamp: true}) 64 | } 65 | 66 | log.WithFields(log.Fields{ 67 | "configFile": *configFile, 68 | }).Debug("opening configuration file") 69 | 70 | var config birdwatcher.Config 71 | if err := birdwatcher.ReadConfig(&config, *configFile); err != nil { 72 | // return slightly different message when birdwatcher was invoked with -check-config 73 | if *checkConfig { 74 | fmt.Printf("Configuration file %s not OK: %s\n", *configFile, err) 75 | os.Exit(1) 76 | } 77 | 78 | log.Fatal(err.Error()) 79 | } 80 | 81 | if *checkConfig { 82 | fmt.Printf("Configuration file %s OK\n", *configFile) 83 | 84 | if *debugFlag { 85 | configJSON, err := json.MarshalIndent(config, "", " ") 86 | if err != nil { 87 | log.Fatal(err.Error()) 88 | } 89 | 90 | fmt.Println(string(configJSON)) 91 | } 92 | 93 | return 94 | } 95 | 96 | // enable prometheus 97 | // Expose /metrics HTTP endpoint using the created custom registry. 98 | if config.Prometheus.Enabled { 99 | go func() { 100 | if err := startPrometheus(config.Prometheus); err != nil { 101 | log.WithError(err).Fatal("could not start prometheus exporter") 102 | } 103 | }() 104 | } 105 | 106 | // start health checker 107 | hc := birdwatcher.NewHealthCheck(config) 108 | ready := make(chan bool) 109 | 110 | // create status update channel for systemd 111 | // give it a little buffer so the chances of it blocking the health check 112 | // is low 113 | sdStatus := make(chan string, systemdStatusBufferSize) 114 | go func() { 115 | // make sure we read from the sdStatus channel, regardless if we use 116 | // systemd integration or not to prevent the channel from blocking 117 | for update := range sdStatus { 118 | if *useSystemd { 119 | log.Debug("notifying systemd of new status") 120 | sdnotify("STATUS=" + update) 121 | } 122 | } 123 | }() 124 | 125 | go hc.Start(config.GetServices(), ready, sdStatus) 126 | // wait for all health services to have started 127 | <-ready 128 | 129 | if *useSystemd { 130 | log.Debug("notifying systemd birdwatcher is ready") 131 | sdnotify(daemon.SdNotifyReady) 132 | } 133 | 134 | // wait until interrupted 135 | signalCh := make(chan os.Signal, 1) 136 | signal.Notify(signalCh, os.Interrupt) 137 | signal.Notify(signalCh, syscall.SIGTERM, syscall.SIGQUIT) 138 | 139 | sig := <-signalCh 140 | log.WithFields(log.Fields{ 141 | "signal": sig, 142 | }).Info("signal received, stopping") 143 | 144 | if *useSystemd { 145 | log.Debug("notifying systemd birdwatcher is stopping") 146 | sdnotify(daemon.SdNotifyStopping) 147 | } 148 | 149 | hc.Stop() 150 | } 151 | 152 | // sdnotify is a little wrapper for daemon.SdNotify 153 | func sdnotify(msg string) { 154 | if ok, err := daemon.SdNotify(false, msg); ok && err != nil { 155 | log.WithError(err).Error("could not notify systemd") 156 | } 157 | } 158 | 159 | func startPrometheus(c birdwatcher.PrometheusConfig) error { 160 | log.WithFields(log.Fields{ 161 | "port": c.Port, 162 | "path": c.Path, 163 | }).Info("starting prometheus exporter") 164 | 165 | mux := http.NewServeMux() 166 | mux.Handle(c.Path, promhttp.Handler()) 167 | 168 | httpServer := &http.Server{ 169 | Addr: fmt.Sprintf("0.0.0.0:%d", c.Port), 170 | ReadTimeout: 5 * time.Second, 171 | WriteTimeout: 10 * time.Second, 172 | Handler: mux, 173 | } 174 | 175 | return httpServer.ListenAndServe() 176 | } 177 | -------------------------------------------------------------------------------- /birdwatcher/config_test.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfig(t *testing.T) { 12 | t.Parallel() 13 | 14 | // test check for valid file 15 | t.Run("config not found", func(t *testing.T) { 16 | t.Parallel() 17 | 18 | err := ReadConfig(&Config{}, "testdata/config/filedoesntexists") 19 | if assert.Error(t, err) { 20 | assert.Equal(t, "config file testdata/config/filedoesntexists not found", err.Error()) 21 | } 22 | }) 23 | 24 | // read invalid TOML from file and check if it gets detected 25 | t.Run("invalid toml", func(t *testing.T) { 26 | t.Parallel() 27 | 28 | err := ReadConfig(&Config{}, "testdata/config/invalidtoml") 29 | if assert.Error(t, err) { 30 | assert.Contains(t, err.Error(), "could not parse config") 31 | assert.Contains(t, err.Error(), "line 2, column 6") 32 | } 33 | }) 34 | 35 | // check for error when no services are defined 36 | t.Run("no services defined", func(t *testing.T) { 37 | t.Parallel() 38 | 39 | err := ReadConfig(&Config{}, "testdata/config/no_services") 40 | if assert.Error(t, err) { 41 | assert.Equal(t, "no services configured", err.Error()) 42 | } 43 | }) 44 | 45 | // check for error for service with no command 46 | t.Run("service no command", func(t *testing.T) { 47 | t.Parallel() 48 | 49 | err := ReadConfig(&Config{}, "testdata/config/service_nocommand") 50 | if assert.Error(t, err) { 51 | assert.Regexp(t, regexp.MustCompile("^service .+ has no command set"), err.Error()) 52 | } 53 | }) 54 | 55 | // check for error for service with no prefixes 56 | t.Run("service no prefixes", func(t *testing.T) { 57 | t.Parallel() 58 | 59 | err := ReadConfig(&Config{}, "testdata/config/service_noprefixes") 60 | if assert.Error(t, err) { 61 | assert.Regexp(t, regexp.MustCompile("^service .+ has no prefixes set"), err.Error()) 62 | } 63 | }) 64 | 65 | // check for error for service with invalid prefix 66 | t.Run("invalid prefix", func(t *testing.T) { 67 | t.Parallel() 68 | 69 | err := ReadConfig(&Config{}, "testdata/config/service_invalidprefix") 70 | if assert.Error(t, err) { 71 | assert.Regexp(t, regexp.MustCompile("^could not parse prefix for service"), err.Error()) 72 | } 73 | }) 74 | 75 | // check for error for service with duplicate prefix 76 | t.Run("duplicate prefix", func(t *testing.T) { 77 | t.Parallel() 78 | 79 | err := ReadConfig(&Config{}, "testdata/config/service_duplicateprefix") 80 | if assert.Error(t, err) { 81 | assert.Regexp(t, regexp.MustCompile("^duplicate prefix .+ found"), err.Error()) 82 | } 83 | }) 84 | 85 | // read minimal valid config and check defaults 86 | t.Run("minimal valid config", func(t *testing.T) { 87 | t.Parallel() 88 | 89 | testConf := Config{} 90 | 91 | err := ReadConfig(&testConf, "testdata/config/minimal") 92 | if !assert.NoError(t, err) { 93 | return 94 | } 95 | 96 | assert.Equal(t, defaultConfigFile, testConf.ConfigFile) 97 | assert.Equal(t, defaultReloadCommand, testConf.ReloadCommand) 98 | assert.False(t, testConf.Prometheus.Enabled) 99 | assert.Equal(t, defaultPrometheusPort, testConf.Prometheus.Port) 100 | assert.Equal(t, defaultPrometheusPath, testConf.Prometheus.Path) 101 | assert.Len(t, testConf.Services, 1) 102 | assert.Equal(t, "foo", testConf.Services["foo"].name) 103 | assert.Equal(t, defaultCheckInterval, testConf.Services["foo"].Interval) 104 | assert.Equal(t, defaultFunctionName, testConf.Services["foo"].FunctionName) 105 | assert.Equal(t, defaultServiceFail, testConf.Services["foo"].Fail) 106 | assert.Equal(t, defaultServiceRise, testConf.Services["foo"].Rise) 107 | assert.Equal(t, defaultServiceTimeout, testConf.Services["foo"].Timeout) 108 | 109 | if assert.Len(t, testConf.Services["foo"].prefixes, 1) { 110 | assert.Equal(t, "192.168.0.0/24", testConf.Services["foo"].prefixes[0].String()) 111 | } 112 | 113 | // check GetServices result 114 | svcs := testConf.GetServices() 115 | if assert.Len(t, svcs, 1) { 116 | assert.Equal(t, "foo", svcs[0].name) 117 | } 118 | }) 119 | 120 | // read overridden TOML file and check if overrides are picked up 121 | t.Run("all options overridden", func(t *testing.T) { 122 | t.Parallel() 123 | 124 | testConf := Config{} 125 | 126 | err := ReadConfig(&testConf, "testdata/config/overridden") 127 | if !assert.NoError(t, err) { 128 | return 129 | } 130 | 131 | assert.Equal(t, "/etc/birdwatcher.conf", testConf.ConfigFile) 132 | assert.Equal(t, "/sbin/birdc configure", testConf.ReloadCommand) 133 | assert.True(t, testConf.CompatBird213) 134 | 135 | assert.True(t, testConf.Prometheus.Enabled) 136 | assert.Equal(t, 1234, testConf.Prometheus.Port) 137 | assert.Equal(t, "/something", testConf.Prometheus.Path) 138 | assert.Equal(t, "foo_bar", testConf.Services["foo"].FunctionName) 139 | 140 | if assert.Len(t, testConf.Services["foo"].prefixes, 1) { 141 | assert.Equal(t, "192.168.0.0/24", testConf.Services["foo"].prefixes[0].String()) 142 | } 143 | 144 | if assert.Len(t, testConf.Services["bar"].prefixes, 2) { 145 | assert.Equal(t, "192.168.1.0/24", testConf.Services["bar"].prefixes[0].String()) 146 | assert.Equal(t, "fc00::/7", testConf.Services["bar"].prefixes[1].String()) 147 | } 148 | 149 | // check GetServices result 150 | svcs := testConf.GetServices() 151 | if assert.Len(t, svcs, 2) { 152 | // order of the services is not guaranteed 153 | for _, svc := range svcs { 154 | switch svc.name { 155 | case "foo": 156 | assert.Equal(t, 10, svc.Interval) 157 | assert.Equal(t, 20, svc.Rise) 158 | assert.Equal(t, 30, svc.Fail) 159 | assert.Equal(t, time.Second*40, svc.Timeout) 160 | case "bar": 161 | default: 162 | assert.Fail(t, "unexpected service name", "service name: %s", svc.name) 163 | } 164 | } 165 | } 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # birdwatcher 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/skoef/birdwatcher)](https://goreportcard.com/report/github.com/skoef/birdwatcher) 4 | 5 | > healthchecker for [BIRD](https://bird.network.cz/)-anycasted services 6 | 7 | :warning: **NOTE**: this version of birdwatcher is designed to work with BIRD version 2 and up. If you want to use birdwatcher with BIRD 1.x, refer to the `bird1` branch. 8 | 9 | This project is heavily influenced by [anycast-healthchecker](https://github.com/unixsurfer/anycast_healthchecker). If you want to know more about use cases of birdwatcher, please read their excellent documention about anycasted services and how a healthchecker can contribute to a more stable service availability. 10 | 11 | In a nutshell: birdwatcher periodically checks a specific service and tells BIRD which prefixes to announce or to withdraw when the service appears to be up or down respectively. 12 | 13 | ## Why birdwatcher 14 | 15 | When I found out about anycast-healthchecker (sadly only recently on [HaproxyConf 2019](https://www.haproxyconf.com/)), I figured this would solve the missing link between BIRD and whatever anycasted services I have running (mostly haproxy though). Currently however in anycast-healthchecker, it is not possible to specify multiple prefixes to a service. Some machines in these kind of setups are announcing _many_ `/32` and `/128` prefixes and I ended up specifiying so many services (one per prefix) that python crashed giving me a `too many open files` error. At first I tried to patch anycast-healthchecker but ended up writing something similar, hence birdwatcher. 16 | 17 | It is written in Go because I like it and running multiple threads is easy. 18 | 19 | ## Upgrading 20 | 21 | ### Upgrading from 1.0.0-beta2 to 1.0.0-beta3 22 | 23 | The notation of `timeout` for services changed from int (for number of seconds) to the format `time.ParseDuration` uses, such as "300ms", "-1.5h" or "2h45m". 24 | 25 | For example: `10` should become `"10s"` in your config files. 26 | 27 | ## Example usage 28 | 29 | This simple example configures a single service, runs `haproxy_check.sh` every second and manages 2 prefixes based on the exit code of the script: 30 | 31 | ```toml 32 | [services] 33 | [services."foo"] 34 | command = "/usr/bin/haproxy_check.sh" 35 | prefixes = ["192.168.0.0/24", "fc00::/7"] 36 | 37 | ``` 38 | 39 | Sample output in `/etc/bird/birdwatcher.conf` if `haproxy_check.sh` checks out would be: 40 | 41 | ``` 42 | # DO NOT EDIT MANUALLY 43 | function match_route() -> bool 44 | { 45 | return net ~ [ 46 | 192.168.0.0/24, 47 | fc00::/7 48 | ]; 49 | } 50 | ``` 51 | 52 | As soon as birdwatcher finds out haproxy is down, it will change the content in `/etc/bird/birdwatcher.conf` to: 53 | 54 | ``` 55 | # DO NOT EDIT MANUALLY 56 | function match_route() -> bool 57 | { 58 | return false; 59 | } 60 | ``` 61 | 62 | and reconfigures BIRD by given `reloadcommand`. Obviously, if you have multiple services being checked by birdwatcher, only the prefixes of that particular service would be removed from the list in `match_route`. 63 | 64 | Integration in BIRD is a matter of including `/etc/bird/birdwatcher.conf` (or whatever you configured at `configfile`) in the configuration for BIRD and use it in a protocol like this: 65 | 66 | ``` 67 | protocol bgp my_bgp_proto { 68 | local as 12345; 69 | neighbor 1.2.3.4 as 23435; 70 | ... 71 | ipv4 { 72 | ... 73 | export where match_route(); 74 | } 75 | ... 76 | ipv6 { 77 | ... 78 | export where match_route(); 79 | } 80 | } 81 | ``` 82 | 83 | ## Configuration 84 | 85 | ## **global** 86 | 87 | Configuration section for global options. 88 | 89 | | key | description | 90 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | 91 | | configfile | Path to configuration file that will be generated and should be included in the BIRD configuration. Defaults to **/etc/bird/birdwatcher.conf**. | 92 | | reloadcommand | Command to invoke to signal BIRD the configuration should be reloaded. Defaults to **/usr/sbin/birdc configure**. | 93 | | compatbird213 | To use birdwatcher with BIRD 2.13 or earlier, enable this flag. It will remove the function return types from the output | 94 | 95 | ## **[services]** 96 | 97 | Each service under this section can have the following settings: 98 | 99 | | key | description | 100 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 101 | | command | Command that will be periodically run to check if the service should be considered up or down. The result is based on the exit code: a non-zero exit codes makes birdwatcher decide the service is down, otherwise it's up. **Required** | 102 | | functionname | Specify the name of the function birdwatcher will generate. You can use this function name to use in your protocol export filter in BIRD. Defaults to **match_route**. | 103 | | interval | The interval in seconds at which birdwatcher will check the service. Defaults to **1** | 104 | | timeout | Time in which the check command should complete. Afterwards it will be handled as if the check command failed. Defaults to **10s**, format following that of [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration). | 105 | | fail | The amount of times the check command should fail before the service is considered to be down. Defaults to **1** | 106 | | rise | The amount of times the check command should succeed before the service is considered to be up. Defaults to **1** | 107 | | prefixes | Array of prefixes, mixed IPv4 and IPv6. At least 1 prefix is **required** per service | 108 | 109 | ## **[prometheus]** 110 | 111 | Configuration for the prometheus exporter 112 | 113 | | key | description | 114 | | ------- | ---------------------------------------------------------------------------- | 115 | | enabled | Boolean whether you want to export prometheus metrics. Defaults to **false** | 116 | | port | Port to export prometheus metrics on. Defaults to **9091** | 117 | | path | Path to the prometheus metrics. Defaults to **/metrics** | 118 | -------------------------------------------------------------------------------- /birdwatcher/healthcheck.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os/exec" 9 | "strings" 10 | "time" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promauto" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | const ( 18 | // size of the channels service checks push their events on 19 | actionsChannelSize = 16 20 | // timeout when reloading bird 21 | reloadTimeout = 10 * time.Second 22 | ) 23 | 24 | var prefixStateMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ 25 | Namespace: "birdwatcher", 26 | Subsystem: "prefix", 27 | Name: "state", 28 | Help: "Current health state per prefix", 29 | }, []string{"service", "prefix"}) 30 | 31 | // HealthCheck -- struct holding everything needed for the never-ending health 32 | // check loop 33 | type HealthCheck struct { 34 | stopped chan any 35 | actions chan *Action 36 | services []*ServiceCheck 37 | prefixes PrefixCollection 38 | Config Config 39 | reloadedBefore bool 40 | } 41 | 42 | // NewHealthCheck returns a HealthCheck with given configuration 43 | func NewHealthCheck(c Config) HealthCheck { 44 | h := HealthCheck{} 45 | h.Config = c 46 | 47 | return h 48 | } 49 | 50 | // Start starts the process of health checking the services and handling 51 | // Actions that come from them 52 | func (h *HealthCheck) Start(services []*ServiceCheck, ready chan<- bool, status chan string) { 53 | // copy reference to services 54 | h.services = services 55 | // create channel for service check to push there events on 56 | h.actions = make(chan *Action, actionsChannelSize) 57 | // create a channel to signal we're stopping 58 | h.stopped = make(chan any) 59 | 60 | // start each service and keep a pointer to the services 61 | // we'll need this later to stop them 62 | for _, s := range services { 63 | log.WithFields(log.Fields{ 64 | "service": s.Name(), 65 | }).Info("starting service check") 66 | 67 | go s.Start(&h.actions) 68 | } 69 | 70 | ready <- true 71 | 72 | // mean while process incoming actions from the channel 73 | for { 74 | select { 75 | case <-h.stopped: 76 | log.Debug("received stop signal") 77 | // we're done 78 | return 79 | case action := <-h.actions: 80 | log.WithFields(log.Fields{ 81 | "service": action.Service.name, 82 | "state": action.State, 83 | }).Debug("incoming action") 84 | 85 | h.handleAction(action, status) 86 | } 87 | } 88 | } 89 | 90 | func (h *HealthCheck) didReloadBefore() bool { 91 | return h.reloadedBefore 92 | } 93 | 94 | func (h *HealthCheck) handleAction(action *Action, status chan string) { 95 | for _, p := range action.Prefixes { 96 | switch action.State { 97 | case ServiceStateUp: 98 | h.addPrefix(action.Service, p) 99 | case ServiceStateDown: 100 | h.removePrefix(action.Service, p) 101 | default: 102 | log.WithFields(log.Fields{ 103 | "state": action.State, 104 | "service": action.Service.name, 105 | }).Warning("unhandled state received") 106 | 107 | return 108 | } 109 | } 110 | 111 | // gather data for a status update 112 | su := h.statusUpdate() 113 | log.WithField("status", su).Debug("status update") 114 | // send update over channel 115 | status <- su 116 | 117 | if err := h.applyConfig(h.Config, h.prefixes); err != nil { 118 | log.WithError(err).Error("could not apply BIRD config") 119 | } 120 | } 121 | 122 | // statusUpdate returns a string with a situational report on how many services 123 | // are configured up 124 | func (h *HealthCheck) statusUpdate() string { 125 | servicesDown := []string{} 126 | 127 | for _, s := range h.services { 128 | if s.IsUp() { 129 | continue 130 | } 131 | 132 | servicesDown = append(servicesDown, s.Name()) 133 | } 134 | 135 | allServices := len(h.services) 136 | 137 | var status string 138 | 139 | switch { 140 | case len(servicesDown) == 0: 141 | status = fmt.Sprintf("all %d service(s) up", allServices) 142 | case len(servicesDown) == allServices: 143 | status = fmt.Sprintf("all %d service(s) down", allServices) 144 | default: 145 | status = fmt.Sprintf("service(s) %s down, %d service(s) up", 146 | strings.Join(servicesDown, ","), allServices-len(servicesDown)) 147 | } 148 | 149 | return status 150 | } 151 | 152 | func (h *HealthCheck) applyConfig(config Config, prefixes PrefixCollection) error { 153 | cLog := log.WithFields(log.Fields{ 154 | "file": config.ConfigFile, 155 | }) 156 | 157 | // update bird config 158 | err := updateBirdConfig(config, prefixes) 159 | if err != nil { 160 | // if config did not change, we should still reload if we don't know the 161 | // state of BIRD 162 | if errors.Is(err, errConfigIdentical) { 163 | if h.didReloadBefore() { 164 | cLog.Warning("config did not change, not reloading") 165 | 166 | return nil 167 | } 168 | 169 | cLog.Info("config did not change, but reloading anyway") 170 | } else { 171 | // break on any other error 172 | cLog.WithError(err).Warning("error updating configuration") 173 | 174 | return err 175 | } 176 | } 177 | 178 | cLog = log.WithFields(log.Fields{ 179 | "command": config.ReloadCommand, 180 | }) 181 | cLog.Info("prefixes updated, reloading") 182 | 183 | // issue reload command, with some reasonable timeout 184 | ctx, cancel := context.WithTimeout(context.Background(), reloadTimeout) 185 | defer cancel() 186 | 187 | // split reload command into command/args assuming the first part is the command 188 | // and the rest are the arguments 189 | commandArgs := strings.Split(config.ReloadCommand, " ") 190 | 191 | // set up command execution within that context 192 | cmd := exec.CommandContext(ctx, commandArgs[0], commandArgs[1:]...) 193 | 194 | // get exit code of command 195 | output, err := cmd.Output() 196 | 197 | // We want to check the context error to see if the timeout was executed. 198 | // The error returned by cmd.Output() will be OS specific based on what 199 | // happens when a process is killed. 200 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 201 | cLog.WithField("timeout", reloadTimeout).Warning("reloading timed out") 202 | 203 | return ctx.Err() 204 | } 205 | 206 | if err != nil { 207 | cLog.WithError(err).WithField("output", output).Warning("reloading failed") 208 | } else { 209 | cLog.Debug("reloading succeeded") 210 | 211 | // mark successful reload 212 | h.reloadedBefore = true 213 | } 214 | 215 | return err 216 | } 217 | 218 | func (h *HealthCheck) addPrefix(svc *ServiceCheck, prefix net.IPNet) { 219 | h.ensurePrefixSet(svc.FunctionName) 220 | 221 | h.prefixes[svc.FunctionName].Add(prefix) 222 | prefixStateMetric.WithLabelValues(svc.Name(), prefix.String()).Set(1.0) 223 | } 224 | 225 | func (h *HealthCheck) removePrefix(svc *ServiceCheck, prefix net.IPNet) { 226 | h.ensurePrefixSet(svc.FunctionName) 227 | 228 | h.prefixes[svc.FunctionName].Remove(prefix) 229 | prefixStateMetric.WithLabelValues(svc.Name(), prefix.String()).Set(0.0) 230 | } 231 | 232 | func (h *HealthCheck) ensurePrefixSet(functionName string) { 233 | // make sure the top level map is prepared 234 | if h.prefixes == nil { 235 | h.prefixes = make(PrefixCollection) 236 | } 237 | 238 | // make sure a mapping for this function name exists 239 | if _, found := h.prefixes[functionName]; !found { 240 | h.prefixes[functionName] = NewPrefixSet(functionName) 241 | } 242 | } 243 | 244 | // Stop signals all servic checks to stop as well and then stops itself 245 | func (h *HealthCheck) Stop() { 246 | // signal each service to stop 247 | for _, s := range h.services { 248 | log.WithFields(log.Fields{ 249 | "service": s.Name(), 250 | }).Info("stopping service check") 251 | 252 | s.Stop() 253 | } 254 | 255 | h.stopped <- true 256 | } 257 | -------------------------------------------------------------------------------- /birdwatcher/servicecheck.go: -------------------------------------------------------------------------------- 1 | package birdwatcher 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promauto" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ( 18 | serviceInfoMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ 19 | Namespace: "birdwatcher", 20 | Subsystem: "service", 21 | Name: "info", 22 | Help: "Services and their configuration", 23 | }, []string{"service", "function_name", "command", "interval", "timeout", "rise", "fail"}) 24 | 25 | serviceCheckDuration = promauto.NewGaugeVec(prometheus.GaugeOpts{ 26 | Namespace: "birdwatcher", 27 | Subsystem: "service", 28 | Name: "check_duration", 29 | Help: "Service check duration in milliseconds", 30 | }, []string{"service"}) 31 | 32 | serviceStateMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ 33 | Namespace: "birdwatcher", 34 | Subsystem: "service", 35 | Name: "state", 36 | Help: "Current health state per service", 37 | }, []string{"service"}) 38 | 39 | serviceTransitionMetric = promauto.NewCounterVec(prometheus.CounterOpts{ 40 | Namespace: "birdwatcher", 41 | Subsystem: "service", 42 | Name: "transition_total", 43 | Help: "Number of transitions per service", 44 | }, []string{"service"}) 45 | 46 | serviceSuccessMetric = promauto.NewCounterVec(prometheus.CounterOpts{ 47 | Namespace: "birdwatcher", 48 | Subsystem: "service", 49 | Name: "success_total", 50 | Help: "Number of successful probes per service", 51 | }, []string{"service"}) 52 | 53 | serviceFailMetric = promauto.NewCounterVec(prometheus.CounterOpts{ 54 | Namespace: "birdwatcher", 55 | Subsystem: "service", 56 | Name: "fail_total", 57 | Help: "Number of failed probes per service", 58 | }, []string{"service"}) 59 | 60 | serviceTimeoutMetric = promauto.NewCounterVec(prometheus.CounterOpts{ 61 | Namespace: "birdwatcher", 62 | Subsystem: "service", 63 | Name: "timeout_total", 64 | Help: "Number of timed out probes per service", 65 | }, []string{"service"}) 66 | ) 67 | 68 | // ServiceState represents the state the service is considered to be in 69 | type ServiceState string 70 | 71 | const ( 72 | // ServiceStateDown considers the service to be down 73 | ServiceStateDown ServiceState = "down" 74 | // ServiceStateUp considers the service to be up 75 | ServiceStateUp ServiceState = "up" 76 | ) 77 | 78 | // ServiceCheck is the struct for holding all information and state about a 79 | // specific service health check 80 | type ServiceCheck struct { 81 | name string 82 | FunctionName string 83 | Command string 84 | Interval int 85 | Timeout time.Duration 86 | Fail int 87 | Rise int 88 | Prefixes []string 89 | //nolint:revive // these prefixes are converted into net.IPNet 90 | prefixes []net.IPNet 91 | state ServiceState 92 | disablePrefixCheck bool 93 | stopped chan any 94 | } 95 | 96 | // Start starts the process of health checking its service and sends actions to 97 | // the action channel when service state changes 98 | // 99 | //nolint:funlen // we should refactor this a bit 100 | func (s *ServiceCheck) Start(action *chan *Action) { 101 | s.stopped = make(chan any) 102 | ticker := time.NewTicker(time.Second * time.Duration(s.Interval)) 103 | 104 | var err error 105 | 106 | upCounter := 0 107 | downCounter := 0 108 | 109 | sLog := log.WithFields(log.Fields{ 110 | "service": s.name, 111 | "command": s.Command, 112 | }) 113 | 114 | // set service info metric 115 | serviceInfoMetric.With(prometheus.Labels{ 116 | "service": s.name, 117 | "function_name": s.FunctionName, 118 | "command": s.Command, 119 | "interval": strconv.Itoa(s.Interval), 120 | "timeout": s.Timeout.String(), 121 | "rise": strconv.Itoa(s.Rise), 122 | "fail": strconv.Itoa(s.Fail), 123 | }).Set(1.0) 124 | 125 | for { 126 | select { 127 | case <-s.stopped: 128 | sLog.Debug("received stop signal") 129 | // we're done 130 | return 131 | 132 | case <-ticker.C: 133 | beginCheck := time.Now() 134 | // perform check synchronously to prevent checks to queue 135 | err = s.performCheck() 136 | // keep track of the time it took for the check to perform 137 | serviceCheckDuration.WithLabelValues(s.name).Set(float64(time.Since(beginCheck))) 138 | 139 | // based on the check result, decide if we're going up or down 140 | // 141 | // check gave positive result 142 | if err == nil { 143 | // reset downCounter 144 | downCounter = 0 145 | 146 | // update success metric 147 | serviceSuccessMetric.WithLabelValues(s.name).Inc() 148 | 149 | sLog.Debug("check command exited without error") 150 | 151 | // are we up enough to consider service to be healthy 152 | if upCounter >= (s.Rise - 1) { 153 | if s.state != ServiceStateUp { 154 | sLog.WithFields(log.Fields{ 155 | "successes": upCounter, 156 | }).Info("service transitioning to up") 157 | 158 | // mark current state as up 159 | s.state = ServiceStateUp 160 | 161 | // update state metric 162 | serviceStateMetric.WithLabelValues(s.name).Set(1) 163 | // update transition metric 164 | serviceTransitionMetric.WithLabelValues(s.name).Inc() 165 | 166 | // send action on channel 167 | *action <- s.getAction() 168 | } 169 | } else { 170 | // or are we still in the process of coming up 171 | upCounter++ 172 | 173 | sLog.WithFields(log.Fields{ 174 | "successes": upCounter, 175 | }).Debug("service moving towards up") 176 | } 177 | } else { 178 | // check gave negative result 179 | // 180 | // reset upcounter 181 | upCounter = 0 182 | 183 | // update success metric 184 | serviceFailMetric.WithLabelValues(s.name).Inc() 185 | // if this was a timeout, increment that counter as well 186 | if errors.Is(err, context.DeadlineExceeded) { 187 | serviceTimeoutMetric.WithLabelValues(s.name).Inc() 188 | sLog.Debug("check command timed out") 189 | } else { 190 | sLog.Debug("check command failed") 191 | } 192 | 193 | // are we down long enough to consider service down 194 | if downCounter >= (s.Fail - 1) { 195 | if s.state != ServiceStateDown { 196 | sLog.WithFields(log.Fields{ 197 | "failures": downCounter, 198 | }).Info("service transitioning to down") 199 | 200 | // mark current state as down 201 | s.state = ServiceStateDown 202 | 203 | // update state metric 204 | serviceStateMetric.WithLabelValues(s.name).Set(0) 205 | // update transition metric 206 | serviceTransitionMetric.WithLabelValues(s.name).Inc() 207 | 208 | // send action on channel 209 | *action <- s.getAction() 210 | } 211 | } else { 212 | downCounter++ 213 | 214 | sLog.WithFields(log.Fields{ 215 | "failures": downCounter, 216 | }).Debug("service moving towards down") 217 | } 218 | } 219 | } 220 | } 221 | } 222 | 223 | // Stop stops the service check from running 224 | func (s *ServiceCheck) Stop() { 225 | s.stopped <- true 226 | 227 | log.WithFields(log.Fields{ 228 | "service": s.name, 229 | }).Debug("stopped service") 230 | } 231 | 232 | // Name returns the service check's name 233 | func (s *ServiceCheck) Name() string { 234 | return s.name 235 | } 236 | 237 | // IsUp returns whether the service is considered up by birdwatcher 238 | func (s *ServiceCheck) IsUp() bool { 239 | return (s.state == ServiceStateUp) 240 | } 241 | 242 | func (s *ServiceCheck) getAction() *Action { 243 | return &Action{ 244 | Service: s, 245 | State: s.state, 246 | Prefixes: s.prefixes, 247 | } 248 | } 249 | 250 | func (s *ServiceCheck) performCheck() error { 251 | sLog := log.WithFields(log.Fields{ 252 | "service": s.name, 253 | "command": s.Command, 254 | }) 255 | sLog.Debug("performing check") 256 | 257 | // create context that automatically times out 258 | ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) 259 | defer cancel() 260 | 261 | // split reload command into command/args assuming the first part is the command 262 | // and the rest are the arguments 263 | commandArgs := strings.Split(s.Command, " ") 264 | 265 | // set up command execution within that context 266 | cmd := exec.CommandContext(ctx, commandArgs[0], commandArgs[1:]...) 267 | 268 | // get exit code of command 269 | output, err := cmd.Output() 270 | 271 | // We want to check the context error to see if the timeout was executed. 272 | // The error returned by cmd.Output() will be OS specific based on what 273 | // happens when a process is killed. 274 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 275 | return ctx.Err() 276 | } 277 | 278 | if err != nil { 279 | sLog.WithError(err).WithField("output", output).Debug("check output") 280 | } 281 | 282 | return err 283 | } 284 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values (in comments). 3 | 4 | # Options for analysis running. 5 | run: 6 | # Number of operating system threads (`GOMAXPROCS`) that can execute golangci-lint simultaneously. 7 | # If it is explicitly set to 0 (i.e. not the default) then golangci-lint will automatically set the value to match Linux container CPU quota. 8 | # Default: the number of logical CPUs in the machine 9 | # concurrency: 4 10 | 11 | # Timeout for analysis, e.g. 30s, 5m. 12 | # Default: 1m 13 | timeout: 5m 14 | 15 | # Exit code when at least one issue was found. 16 | # Default: 1 17 | issues-exit-code: 1 18 | 19 | # Include test files or not. 20 | # Default: true 21 | tests: true 22 | 23 | # List of build tags, all linters use it. 24 | # Default: [] 25 | # build-tags: 26 | 27 | # If set, we pass it to "go list -mod={option}". From "go help modules": 28 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 29 | # automatic updating of go.mod described above. Instead, it fails when any changes 30 | # to go.mod are needed. This setting is most useful to check that go.mod does 31 | # not need updates, such as in a continuous integration and testing system. 32 | # If invoked with -mod=vendor, the go command assumes that the vendor 33 | # directory holds the correct copies of dependencies and ignores 34 | # the dependency descriptions in go.mod. 35 | # 36 | # Allowed values: readonly|vendor|mod 37 | # Default: "" 38 | modules-download-mode: "" 39 | 40 | # Allow multiple parallel golangci-lint instances running. 41 | # If false, golangci-lint acquires file lock on start. 42 | # Default: false 43 | allow-parallel-runners: true 44 | 45 | # Allow multiple golangci-lint instances running, but serialize them around a lock. 46 | # If false, golangci-lint exits with an error if it fails to acquire file lock on start. 47 | # Default: false 48 | allow-serial-runners: false 49 | 50 | # Define the Go version limit. 51 | # Mainly related to generics support since go1.18. 52 | # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.17 53 | # go: "1.21" 54 | 55 | # output configuration options 56 | output: 57 | # The formats used to render issues. 58 | # Formats: 59 | # - `colored-line-number` 60 | # - `line-number` 61 | # - `json` 62 | # - `colored-tab` 63 | # - `tab` 64 | # - `html` 65 | # - `checkstyle` 66 | # - `code-climate` 67 | # - `junit-xml` 68 | # - `github-actions` 69 | # - `teamcity` 70 | # - `sarif` 71 | # Output path can be either `stdout`, `stderr` or path to the file to write to. 72 | # 73 | # For the CLI flag (`--out-format`), multiple formats can be specified by separating them by comma. 74 | # The output can be specified for each of them by separating format name and path by colon symbol. 75 | # Example: "--out-format=checkstyle:report.xml,json:stdout,colored-line-number" 76 | # The CLI flag (`--out-format`) override the configuration file. 77 | # 78 | # Default: 79 | # formats: 80 | # - format: colored-line-number 81 | # path: stdout 82 | formats: 83 | - format: colored-line-number 84 | path: stdout 85 | 86 | # Print lines of code with issue. 87 | # Default: true 88 | print-issued-lines: true 89 | 90 | # Print linter name in the end of issue text. 91 | # Default: true 92 | print-linter-name: true 93 | 94 | # Make issues output unique by line. 95 | # Default: true 96 | uniq-by-line: true 97 | 98 | # Add a prefix to the output file references. 99 | # Default: "" 100 | path-prefix: "" 101 | 102 | # Sort results by the order defined in `sort-order`. 103 | # Default: false 104 | sort-results: true 105 | 106 | # Order to use when sorting results. 107 | # Require `sort-results` to `true`. 108 | # Possible values: `file`, `linter`, and `severity`. 109 | # 110 | # If the severity values are inside the following list, they are ordered in this order: 111 | # 1. error 112 | # 2. warning 113 | # 3. high 114 | # 4. medium 115 | # 5. low 116 | # Either they are sorted alphabetically. 117 | # 118 | # Default: ["file"] 119 | sort-order: 120 | - linter 121 | - severity 122 | - file # filepath, line, and column. 123 | 124 | # Show statistics per linter. 125 | # Default: false 126 | show-stats: false 127 | 128 | # All available settings of specific linters. 129 | linters-settings: 130 | ### Default linters 131 | errcheck: 132 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 133 | # Such cases aren't reported by default. 134 | # Default: false 135 | check-type-assertions: true 136 | 137 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. 138 | # Such cases aren't reported by default. 139 | # Default: false 140 | check-blank: false 141 | 142 | # To disable the errcheck built-in exclude list. 143 | # See `-excludeonly` option in https://github.com/kisielk/errcheck#excluding-functions for details. 144 | # Default: false 145 | disable-default-exclusions: false 146 | 147 | # List of functions to exclude from checking, where each entry is a single function to exclude. 148 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 149 | exclude-functions: 150 | - io/ioutil.ReadFile 151 | - io.Copy(*bytes.Buffer) 152 | - io.Copy(os.Stdout) 153 | - crypto/rand.Read 154 | 155 | gosimple: 156 | # Sxxxx checks in https://staticcheck.io/docs/configuration/options/#checks 157 | # Default: ["*"] 158 | checks: ["all"] 159 | 160 | govet: 161 | # Disable all analyzers. 162 | # Default: false 163 | disable-all: true 164 | # Enable analyzers by name. 165 | # (in addition to default: 166 | # appends, asmdecl, assign, atomic, bools, buildtag, cgocall, composites, copylocks, defers, directive, errorsas, 167 | # framepointer, httpresponse, ifaceassert, loopclosure, lostcancel, nilfunc, printf, shift, sigchanyzer, slog, 168 | # stdmethods, stringintconv, structtag, testinggoroutine, tests, timeformat, unmarshal, unreachable, unsafeptr, 169 | # unusedresult 170 | # ). 171 | # Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers. 172 | # Default: [] 173 | enable: 174 | - appends 175 | - asmdecl 176 | - assign 177 | - atomic 178 | - atomicalign 179 | - bools 180 | - buildtag 181 | - cgocall 182 | - composites 183 | - copylocks 184 | - deepequalerrors 185 | - defers 186 | - directive 187 | - errorsas 188 | - findcall 189 | - framepointer 190 | - httpresponse 191 | - ifaceassert 192 | - loopclosure 193 | - lostcancel 194 | - nilfunc 195 | - nilness 196 | - printf 197 | - reflectvaluecompare 198 | - shadow 199 | - shift 200 | - sigchanyzer 201 | - slog 202 | - sortslice 203 | - stdmethods 204 | - stringintconv 205 | - structtag 206 | - testinggoroutine 207 | - tests 208 | - unmarshal 209 | - unreachable 210 | - unsafeptr 211 | - unusedresult 212 | - unusedwrite 213 | 214 | # Enable all analyzers. 215 | # Default: false 216 | enable-all: false 217 | # Disable analyzers by name. 218 | # (in addition to default 219 | # atomicalign, deepequalerrors, fieldalignment, findcall, nilness, reflectvaluecompare, shadow, sortslice, 220 | # timeformat, unusedwrite 221 | # ). 222 | # Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers. 223 | # Default: [] 224 | disable: 225 | 226 | # Settings per analyzer. 227 | settings: 228 | # Analyzer name, run `go tool vet help` to see all analyzers. 229 | printf: 230 | # Comma-separated list of print function names to check (in addition to default, see `go tool vet help printf`). 231 | # Default: [] 232 | funcs: 233 | shadow: 234 | # Whether to be strict about shadowing; can be noisy. 235 | # Default: false 236 | strict: false 237 | unusedresult: 238 | # Comma-separated list of functions whose results must be used 239 | # (in addition to default: 240 | # context.WithCancel, context.WithDeadline, context.WithTimeout, context.WithValue, errors.New, fmt.Errorf, 241 | # fmt.Sprint, fmt.Sprintf, sort.Reverse 242 | # ). 243 | # Default: [] 244 | funcs: 245 | # Comma-separated list of names of methods of type func() string whose results must be used 246 | # (in addition to default Error,String) 247 | # Default: [] 248 | stringmethods: 249 | 250 | staticcheck: 251 | # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks 252 | # Default: ["*"] 253 | checks: ["all"] 254 | 255 | unused: 256 | # Mark all struct fields that have been written to as used. 257 | # Default: true 258 | field-writes-are-uses: true 259 | # Treat IncDec statement (e.g. `i++` or `i--`) as both read and write operation instead of just write. 260 | # Default: false 261 | post-statements-are-reads: false 262 | # Mark all exported fields as used. 263 | # Default: true 264 | exported-fields-are-used: true 265 | # Mark all function parameters as used. 266 | # Default: true 267 | parameters-are-used: true 268 | # Mark all local variables as used. 269 | # Default: true 270 | local-variables-are-used: true 271 | # Mark all identifiers inside generated files as used. 272 | # Default: true 273 | generated-is-used: true 274 | 275 | ### Additional linters 276 | asasalint: 277 | # To specify a set of function names to exclude. 278 | # The values are merged with the builtin exclusions. 279 | # The builtin exclusions can be disabled by setting `use-builtin-exclusions` to `false`. 280 | # Default: ["^(fmt|log|logger|t|)\.(Print|Fprint|Sprint|Fatal|Panic|Error|Warn|Warning|Info|Debug|Log)(|f|ln)$"] 281 | # exclude: 282 | # To enable/disable the asasalint builtin exclusions of function names. 283 | # See the default value of `exclude` to get the builtin exclusions. 284 | # Default: true 285 | use-builtin-exclusions: true 286 | # Ignore *_test.go files. 287 | # Default: false 288 | ignore-test: false 289 | 290 | bidichk: 291 | # The following configurations check for all mentioned invisible unicode runes. 292 | # All runes are enabled by default. 293 | left-to-right-embedding: true 294 | right-to-left-embedding: true 295 | pop-directional-formatting: true 296 | left-to-right-override: true 297 | right-to-left-override: true 298 | left-to-right-isolate: true 299 | right-to-left-isolate: true 300 | first-strong-isolate: true 301 | pop-directional-isolate: true 302 | 303 | copyloopvar: 304 | # Check all assigning the loop variable to another variable. 305 | # Default: false 306 | check-alias: true 307 | 308 | dogsled: 309 | # Checks assignments with too many blank identifiers. 310 | # Default: 2 311 | max-blank-identifiers: 3 312 | 313 | dupword: 314 | # Keywords for detecting duplicate words. 315 | # If this list is not empty, only the words defined in this list will be detected. 316 | # Default: [] 317 | keywords: 318 | - "the" 319 | - "and" 320 | - "a" 321 | # Keywords used to ignore detection. 322 | # Default: [] 323 | ignore: 324 | 325 | errchkjson: 326 | # With check-error-free-encoding set to true, errchkjson does warn about errors 327 | # from json encoding functions that are safe to be ignored, 328 | # because they are not possible to happen. 329 | # 330 | # if check-error-free-encoding is set to true and errcheck linter is enabled, 331 | # it is recommended to add the following exceptions to prevent from false positives: 332 | # 333 | # linters-settings: 334 | # errcheck: 335 | # exclude-functions: 336 | # - encoding/json.Marshal 337 | # - encoding/json.MarshalIndent 338 | # 339 | # Default: false 340 | check-error-free-encoding: false 341 | 342 | # Issue on struct encoding that doesn't have exported fields. 343 | # Default: false 344 | report-no-exported: false 345 | 346 | errorlint: 347 | # Check whether fmt.Errorf uses the %w verb for formatting errors. 348 | # See the https://github.com/polyfloyd/go-errorlint for caveats. 349 | # Default: true 350 | errorf: true 351 | # Permit more than 1 %w verb, valid per Go 1.20 (Requires errorf:true) 352 | # Default: true 353 | errorf-multi: true 354 | # Check for plain type assertions and type switches. 355 | # Default: true 356 | asserts: true 357 | # Check for plain error comparisons. 358 | # Default: true 359 | comparison: true 360 | # Allowed errors. 361 | # Default: [] 362 | allowed-errors: 363 | # Allowed error "wildcards". 364 | # Default: [] 365 | allowed-errors-wildcard: 366 | 367 | exhaustive: 368 | # Program elements to check for exhaustiveness. 369 | # Default: [ switch ] 370 | check: 371 | - switch 372 | - map 373 | # Check switch statements in generated files also. 374 | # Default: false 375 | check-generated: false 376 | # Presence of "default" case in switch statements satisfies exhaustiveness, 377 | # even if all enum members are not listed. 378 | # Default: false 379 | default-signifies-exhaustive: true 380 | # Enum members matching the supplied regex do not have to be listed in 381 | # switch statements to satisfy exhaustiveness. 382 | # Default: "" 383 | ignore-enum-members: "" 384 | # Enum types matching the supplied regex do not have to be listed in 385 | # switch statements to satisfy exhaustiveness. 386 | # Default: "" 387 | ignore-enum-types: "" 388 | # Consider enums only in package scopes, not in inner scopes. 389 | # Default: false 390 | package-scope-only: false 391 | # Only run exhaustive check on switches with "//exhaustive:enforce" comment. 392 | # Default: false 393 | explicit-exhaustive-switch: false 394 | # Only run exhaustive check on map literals with "//exhaustive:enforce" comment. 395 | # Default: false 396 | explicit-exhaustive-map: false 397 | # Switch statement requires default case even if exhaustive. 398 | # Default: false 399 | default-case-required: false 400 | 401 | funlen: 402 | # Checks the number of lines in a function. 403 | # If lower than 0, disable the check. 404 | # Default: 60 405 | lines: 80 406 | # Checks the number of statements in a function. 407 | # If lower than 0, disable the check. 408 | # Default: 40 409 | statements: 60 410 | # Ignore comments when counting lines. 411 | # Default false 412 | ignore-comments: true 413 | 414 | gci: 415 | # Section configuration to compare against. 416 | # Section names are case-insensitive and may contain parameters in (). 417 | # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`, 418 | # If `custom-order` is `true`, it follows the order of `sections` option. 419 | # Default: ["standard", "default"] 420 | sections: 421 | - standard # Standard section: captures all standard packages. 422 | - default # Default section: contains all imports that could not be matched to another section type. 423 | - prefix(gitlab.com/maxem) # Custom section: groups all imports with the specified Prefix. 424 | - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled. 425 | # Skip generated files. 426 | # Default: true 427 | skip-generated: true 428 | # Enable custom order of sections. 429 | # If `true`, make the section order the same as the order of `sections`. 430 | # Default: false 431 | custom-order: true 432 | # Drops lexical ordering for custom sections. 433 | # Default: false 434 | no-lex-order: false 435 | 436 | gocognit: 437 | # Minimal code complexity to report. 438 | # Default: 30 (but we recommend 10-20) 439 | min-complexity: 30 440 | 441 | goconst: 442 | # Minimal length of string constant. 443 | # Default: 3 444 | min-len: 2 445 | # Minimum occurrences of constant string count to trigger issue. 446 | # Default: 3 447 | min-occurrences: 3 448 | # Ignore test files. 449 | # Default: false 450 | ignore-tests: true 451 | # Look for existing constants matching the values. 452 | # Default: true 453 | match-constant: true 454 | # Search also for duplicated numbers. 455 | # Default: false 456 | numbers: false 457 | # Minimum value, only works with goconst.numbers 458 | # Default: 3 459 | min: 3 460 | # Maximum value, only works with goconst.numbers 461 | # Default: 3 462 | max: 3 463 | # Ignore when constant is not used as function argument. 464 | # Default: true 465 | ignore-calls: true 466 | # Exclude strings matching the given regular expression. 467 | # Default: "" 468 | ignore-strings: "%w" 469 | 470 | gocritic: 471 | # Disable all checks. 472 | # Default: false 473 | disable-all: true 474 | # Which checks should be enabled in addition to default checks; can't be combined with 'disabled-checks'. 475 | # By default, list of stable checks is used (https://go-critic.github.io/overview#checks-overview): 476 | # appendAssign, argOrder, assignOp, badCall, badCond, captLocal, caseOrder, codegenComment, commentFormatting, 477 | # defaultCaseOrder, deprecatedComment, dupArg, dupBranchBody, dupCase, dupSubExpr, elseif, exitAfterDefer, 478 | # flagDeref, flagName, ifElseChain, mapKey, newDeref, offBy1, regexpMust, singleCaseSwitch, sloppyLen, 479 | # sloppyTypeAssert, switchTrue, typeSwitchVar, underef, unlambda, unslice, valSwap, wrapperFunc 480 | # To see which checks are enabled run `GL_DEBUG=gocritic golangci-lint run --enable=gocritic`. 481 | enabled-checks: 482 | - appendAssign 483 | - appendCombine 484 | - argOrder 485 | - assignOp 486 | - badCall 487 | - badCond 488 | - badLock 489 | - badRegexp 490 | - badSorting 491 | - badSyncOnceFunc 492 | - boolExprSimplify 493 | - builtinShadow 494 | - builtinShadowDecl 495 | - captLocal 496 | - caseOrder 497 | - codegenComment 498 | - commentFormatting 499 | - commentedOutCode 500 | - commentedOutImport 501 | - defaultCaseOrder 502 | - deferInLoop 503 | - deferUnlambda 504 | - deprecatedComment 505 | - dupArg 506 | - dupBranchBody 507 | - dupCase 508 | - dupImport 509 | - dupSubExpr 510 | - dynamicFmtString 511 | - elseif 512 | - emptyDecl 513 | - emptyFallthrough 514 | - emptyStringTest 515 | - equalFold 516 | - evalOrder 517 | - exitAfterDefer 518 | - exposedSyncMutex 519 | - externalErrorReassign 520 | - filepathJoin 521 | - flagDeref 522 | - flagName 523 | - hexLiteral 524 | - httpNoBody 525 | - hugeParam 526 | - ifElseChain 527 | - importShadow 528 | - indexAlloc 529 | - initClause 530 | - mapKey 531 | - methodExprCall 532 | - nestingReduce 533 | - nilValReturn 534 | - octalLiteral 535 | - offBy1 536 | - paramTypeCombine 537 | - preferDecodeRune 538 | - preferFilepathJoin 539 | - preferFprint 540 | - preferStringWriter 541 | - preferWriteByte 542 | - rangeExprCopy 543 | - rangeValCopy 544 | - redundantSprint 545 | - regexpMust 546 | - regexpPattern 547 | - regexpSimplify 548 | - returnAfterHttpError 549 | - singleCaseSwitch 550 | - sliceClear 551 | - sloppyLen 552 | - sloppyReassign 553 | - sloppyTypeAssert 554 | - sortSlice 555 | - sprintfQuotedString 556 | - sqlQuery 557 | - stringConcatSimplify 558 | - stringXbytes 559 | - stringsCompare 560 | - switchTrue 561 | - syncMapLoadAndDelete 562 | - timeExprSimplify 563 | - todoCommentWithoutDetail 564 | - tooManyResultsChecker 565 | - truncateCmp 566 | - typeAssertChain 567 | - typeDefFirst 568 | - typeSwitchVar 569 | - typeUnparen 570 | - uncheckedInlineErr 571 | - underef 572 | - unlabelStmt 573 | - unlambda 574 | - unnecessaryBlock 575 | - unnecessaryDefer 576 | - unslice 577 | - valSwap 578 | - weakCond 579 | - wrapperFunc 580 | - yodaStyleExpr 581 | 582 | # Enable all checks. 583 | # Default: false 584 | enable-all: false 585 | # Which checks should be disabled; can't be combined with 'enabled-checks'. 586 | # Default: [] 587 | disabled-checks: 588 | 589 | # Enable multiple checks by tags in addition to default checks. 590 | # Run `GL_DEBUG=gocritic golangci-lint run --enable=gocritic` to see all tags and checks. 591 | # See https://github.com/go-critic/go-critic#usage -> section "Tags". 592 | # Default: [] 593 | enabled-tags: 594 | disabled-tags: 595 | 596 | # Settings passed to gocritic. 597 | # The settings key is the name of a supported gocritic checker. 598 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 599 | settings: 600 | # Must be valid enabled check name. 601 | captLocal: 602 | # Whether to restrict checker to params only. 603 | # Default: true 604 | paramsOnly: true 605 | commentedOutCode: 606 | # Min length of the comment that triggers a warning. 607 | # Default: 15 608 | minLength: 15 609 | elseif: 610 | # Whether to skip balanced if-else pairs. 611 | # Default: true 612 | skipBalanced: true 613 | hugeParam: 614 | # Size in bytes that makes the warning trigger. 615 | # Default: 80 616 | sizeThreshold: 512 617 | ifElseChain: 618 | # Min number of if-else blocks that makes the warning trigger. 619 | # Default: 2 620 | minThreshold: 2 621 | nestingReduce: 622 | # Min number of statements inside a branch to trigger a warning. 623 | # Default: 5 624 | bodyWidth: 5 625 | rangeExprCopy: 626 | # Size in bytes that makes the warning trigger. 627 | # Default: 512 628 | sizeThreshold: 512 629 | # Whether to check test functions 630 | # Default: true 631 | skipTestFuncs: true 632 | rangeValCopy: 633 | # Size in bytes that makes the warning trigger. 634 | # Default: 128 635 | sizeThreshold: 512 636 | # Whether to check test functions. 637 | # Default: true 638 | skipTestFuncs: true 639 | tooManyResultsChecker: 640 | # Maximum number of results. 641 | # Default: 5 642 | maxResults: 4 643 | truncateCmp: 644 | # Whether to skip int/uint/uintptr types. 645 | # Default: true 646 | skipArchDependent: true 647 | underef: 648 | # Whether to skip (*x).method() calls where x is a pointer receiver. 649 | # Default: true 650 | skipRecvDeref: true 651 | 652 | godox: 653 | # Report any comments starting with keywords, this is useful for TODO or FIXME comments that 654 | # might be left in the code accidentally and should be resolved before merging. 655 | # Default: ["TODO", "BUG", "FIXME"] 656 | keywords: 657 | - TODO 658 | - BUG 659 | - FIXME 660 | 661 | gofmt: 662 | # Simplify code: gofmt with `-s` option. 663 | # Default: true 664 | simplify: true 665 | # Apply the rewrite rules to the source before reformatting. 666 | # https://pkg.go.dev/cmd/gofmt 667 | # Default: [] 668 | rewrite-rules: 669 | 670 | gofumpt: 671 | # Module path which contains the source code being formatted. 672 | # Default: "" 673 | module-path: "" 674 | 675 | # Choose whether to use the extra rules. 676 | # Default: false 677 | extra-rules: false 678 | 679 | gosec: 680 | # To select a subset of rules to run. 681 | # Available rules: https://github.com/securego/gosec#available-rules 682 | # Default: [] - means include all rules 683 | includes: 684 | - G101 # Look for hard coded credentials 685 | - G102 # Bind to all interfaces 686 | - G103 # Audit the use of unsafe block 687 | - G104 # Audit errors not checked 688 | - G106 # Audit the use of ssh.InsecureIgnoreHostKey 689 | - G107 # Url provided to HTTP request as taint input 690 | - G108 # Profiling endpoint automatically exposed on /debug/pprof 691 | - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 692 | - G110 # Potential DoS vulnerability via decompression bomb 693 | - G111 # Potential directory traversal 694 | - G112 # Potential slowloris attack 695 | - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) 696 | - G114 # Use of net/http serve function that has no support for setting timeouts 697 | - G201 # SQL query construction using format string 698 | - G202 # SQL query construction using string concatenation 699 | - G203 # Use of unescaped data in HTML templates 700 | - G301 # Poor file permissions used when creating a directory 701 | - G302 # Poor file permissions used with chmod 702 | - G303 # Creating tempfile using a predictable path 703 | - G305 # File traversal when extracting zip/tar archive 704 | - G306 # Poor file permissions used when writing to a new file 705 | - G401 # Detect the usage of DES, RC4, MD5 or SHA1 706 | - G402 # Look for bad TLS connection settings 707 | - G403 # Ensure minimum RSA key length of 2048 bits 708 | - G404 # Insecure random number source (rand) 709 | - G405 # Detect the usage of DES or RC4 710 | - G406 # Detect the usage of MD4 or RIPEMD160 711 | - G501 # Import blocklist: crypto/md5 712 | - G502 # Import blocklist: crypto/des 713 | - G503 # Import blocklist: crypto/rc4 714 | - G504 # Import blocklist: net/http/cgi 715 | - G505 # Import blocklist: crypto/sha1 716 | - G506 # Import blocklist: golang.org/x/crypto/md4 717 | - G507 # Import blocklist: golang.org/x/crypto/ripemd160 718 | - G601 # Implicit memory aliasing of items from a range statement 719 | - G602 # Slice access out of bounds 720 | 721 | # To specify a set of rules to explicitly exclude. 722 | # Available rules: https://github.com/securego/gosec#available-rules 723 | # Default: [] 724 | excludes: 725 | - G115 # Potential integer overflow when converting between integer types 726 | - G204 # Audit use of command execution 727 | - G304 # File path provided as taint input 728 | - G307 # [RETIRED] Poor file permissions used when creating a file with os.Create 729 | 730 | 731 | # Exclude generated files 732 | # Default: false 733 | exclude-generated: true 734 | 735 | # Filter out the issues with a lower severity than the given value. 736 | # Valid options are: low, medium, high. 737 | # Default: low 738 | severity: low 739 | 740 | # Filter out the issues with a lower confidence than the given value. 741 | # Valid options are: low, medium, high. 742 | # Default: low 743 | confidence: low 744 | 745 | # Concurrency value. 746 | # Default: the number of logical CPUs usable by the current process. 747 | # concurrency: 748 | 749 | # To specify the configuration of rules. 750 | config: 751 | # Globals are applicable to all rules. 752 | global: 753 | # If true, ignore #nosec in comments (and an alternative as well). 754 | # Default: false 755 | nosec: false 756 | # Add an alternative comment prefix to #nosec (both will work at the same time). 757 | # Default: "" 758 | "#nosec": "" 759 | # Define whether nosec issues are counted as finding or not. 760 | # Default: false 761 | show-ignored: false 762 | # Audit mode enables addition checks that for normal code analysis might be too nosy. 763 | # Default: false 764 | audit: false 765 | G101: 766 | # Regexp pattern for variables and constants to find. 767 | # Default: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" 768 | pattern: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" 769 | # If true, complain about all cases (even with low entropy). 770 | # Default: false 771 | ignore_entropy: false 772 | # Maximum allowed entropy of the string. 773 | # Default: "80.0" 774 | entropy_threshold: "80.0" 775 | # Maximum allowed value of entropy/string length. 776 | # Is taken into account if entropy >= entropy_threshold/2. 777 | # Default: "3.0" 778 | per_char_threshold: "3.0" 779 | # Calculate entropy for first N chars of the string. 780 | # Default: "16" 781 | truncate: "16" 782 | # Additional functions to ignore while checking unhandled errors. 783 | # Following functions always ignored: 784 | # bytes.Buffer: 785 | # - Write 786 | # - WriteByte 787 | # - WriteRune 788 | # - WriteString 789 | # fmt: 790 | # - Print 791 | # - Printf 792 | # - Println 793 | # - Fprint 794 | # - Fprintf 795 | # - Fprintln 796 | # strings.Builder: 797 | # - Write 798 | # - WriteByte 799 | # - WriteRune 800 | # - WriteString 801 | # io.PipeWriter: 802 | # - CloseWithError 803 | # hash.Hash: 804 | # - Write 805 | # os: 806 | # - Unsetenv 807 | # Default: {} 808 | G104: 809 | fmt: 810 | - Fscanf 811 | G111: 812 | # Regexp pattern to find potential directory traversal. 813 | # Default: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" 814 | pattern: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" 815 | # Maximum allowed permissions mode for os.Mkdir and os.MkdirAll 816 | # Default: "0750" 817 | G301: "0750" 818 | # Maximum allowed permissions mode for os.OpenFile and os.Chmod 819 | # Default: "0600" 820 | G302: "0600" 821 | # Maximum allowed permissions mode for os.WriteFile and ioutil.WriteFile 822 | # Default: "0600" 823 | G306: "0600" 824 | 825 | inamedparam: 826 | # Skips check for interface methods with only a single parameter. 827 | # Default: false 828 | skip-single-param: true 829 | 830 | interfacebloat: 831 | # The maximum number of methods allowed for an interface. 832 | # Default: 10 833 | max: 15 834 | 835 | ireturn: 836 | # List of interfaces to allow. 837 | # Lists of the keywords and regular expressions matched to interface or package names can be used. 838 | # `allow` and `reject` settings cannot be used at the same time. 839 | # 840 | # Keywords: 841 | # - `empty` for `interface{}` 842 | # - `error` for errors 843 | # - `stdlib` for standard library 844 | # - `anon` for anonymous interfaces 845 | # - `generic` for generic interfaces added in go 1.18 846 | # 847 | # Default: [anon, error, empty, stdlib] 848 | allow: 849 | - anon 850 | - error 851 | - empty 852 | - stdlib 853 | - generic 854 | - github.com/nats-io/nats.go/jetstream 855 | 856 | # List of interfaces to reject. 857 | # Lists of the keywords and regular expressions matched to interface or package names can be used. 858 | # `allow` and `reject` settings cannot be used at the same time. 859 | # 860 | # Keywords: 861 | # - `empty` for `interface{}` 862 | # - `error` for errors 863 | # - `stdlib` for standard library 864 | # - `anon` for anonymous interfaces 865 | # - `generic` for generic interfaces added in go 1.18 866 | # 867 | # Default: [] 868 | reject: 869 | 870 | lll: 871 | # Max line length, lines longer will be reported. 872 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option. 873 | # Default: 120. 874 | line-length: 160 875 | # Tab width in spaces. 876 | # Default: 1 877 | tab-width: 1 878 | 879 | maintidx: 880 | # Show functions with maintainability index lower than N. 881 | # A high index indicates better maintainability (it's kind of the opposite of complexity). 882 | # Default: 20 883 | under: 20 884 | 885 | makezero: 886 | # Allow only slices initialized with a length of zero. 887 | # Default: false 888 | always: false 889 | 890 | misspell: 891 | # Correct spellings using locale preferences for US or UK. 892 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 893 | # Default is to use a neutral variety of English. 894 | locale: 895 | # Typos to ignore. 896 | # Should be in lower case. 897 | # Default: [] 898 | ignore-words: 899 | # Extra word corrections. 900 | # `typo` and `correction` should only contain letters. 901 | # The words are case-insensitive. 902 | # Default: [] 903 | extra-words: 904 | # Mode of the analysis: 905 | # - default: checks all the file content. 906 | # - restricted: checks only comments. 907 | # Default: "" 908 | mode: 909 | 910 | nakedret: 911 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 912 | # Default: 30 913 | max-func-lines: 0 914 | 915 | nilnil: 916 | # List of return types to check. 917 | # Default: ["ptr", "func", "iface", "map", "chan", "uintptr", "unsafeptr"] 918 | checked-types: 919 | - ptr 920 | - func 921 | - iface 922 | - map 923 | - chan 924 | - uintptr 925 | - unsafeptr 926 | 927 | nolintlint: 928 | # Disable to ensure that all nolint directives actually have an effect. 929 | # Default: false 930 | allow-unused: false 931 | # Exclude following linters from requiring an explanation. 932 | # Default: [] 933 | allow-no-explanation: [] 934 | # Enable to require an explanation of nonzero length after each nolint directive. 935 | # Default: false 936 | require-explanation: true 937 | # Enable to require nolint directives to mention the specific linter being suppressed. 938 | # Default: false 939 | require-specific: true 940 | 941 | nonamedreturns: 942 | # Report named error if it is assigned inside defer. 943 | # Default: false 944 | report-error-in-defer: false 945 | 946 | paralleltest: 947 | # Ignore missing calls to `t.Parallel()` and only report incorrect uses of it. 948 | # Default: false 949 | ignore-missing: false 950 | # Ignore missing calls to `t.Parallel()` in subtests. Top-level tests are 951 | # still required to have `t.Parallel`, but subtests are allowed to skip it. 952 | # Default: false 953 | ignore-missing-subtests: false 954 | 955 | perfsprint: 956 | # Optimizes even if it requires an int or uint type cast. 957 | # Default: true 958 | int-conversion: true 959 | # Optimizes into `err.Error()` even if it is only equivalent for non-nil errors. 960 | # Default: false 961 | err-error: false 962 | # Optimizes `fmt.Errorf`. 963 | # Default: true 964 | errorf: true 965 | # Optimizes `fmt.Sprintf` with only one argument. 966 | # Default: true 967 | sprintf1: true 968 | # Optimizes into strings concatenation. 969 | # Default: true 970 | strconcat: true 971 | 972 | prealloc: 973 | # IMPORTANT: we don't recommend using this linter before doing performance profiling. 974 | # For most programs usage of prealloc will be a premature optimization. 975 | 976 | # Report pre-allocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 977 | # Default: true 978 | simple: true 979 | # Report pre-allocation suggestions on range loops. 980 | # Default: true 981 | range-loops: true 982 | # Report pre-allocation suggestions on for loops. 983 | # Default: false 984 | for-loops: true 985 | 986 | predeclared: 987 | # Comma-separated list of predeclared identifiers to not report on. 988 | # Default: "" 989 | ignore: "" 990 | # Include method names and field names (i.e., qualified names) in checks. 991 | # Default: false 992 | q: false 993 | 994 | promlinter: 995 | # Promlinter cannot infer all metrics name in static analysis. 996 | # Enable strict mode will also include the errors caused by failing to parse the args. 997 | # Default: false 998 | strict: false 999 | # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage. 1000 | # Default: [] 1001 | disabled-linters: 1002 | 1003 | protogetter: 1004 | # Skip files generated by specified generators from the checking. 1005 | # Checks only the file's initial comment, which must follow the format: "// Code generated by ". 1006 | # Files generated by protoc-gen-go, protoc-gen-go-grpc, and protoc-gen-grpc-gateway are always excluded automatically. 1007 | # Default: [] 1008 | skip-generated-by: 1009 | # Skip files matching the specified glob pattern from the checking. 1010 | # Default: [] 1011 | skip-files: 1012 | - "*.pb.go" 1013 | - "*/vendor/*" 1014 | # Skip any generated files from the checking. 1015 | # Default: false 1016 | skip-any-generated: true 1017 | # Skip first argument of append function. 1018 | # Default: false 1019 | replace-first-arg-in-append: false 1020 | 1021 | revive: 1022 | # Maximum number of open files at the same time. 1023 | # See https://github.com/mgechev/revive#command-line-flags 1024 | # Defaults to unlimited. 1025 | # max-open-files: 1026 | 1027 | # When set to false, ignores files with "GENERATED" header, similar to golint. 1028 | # See https://github.com/mgechev/revive#available-rules for details. 1029 | # Default: false 1030 | ignore-generated-header: true 1031 | 1032 | # Sets the default severity. 1033 | # See https://github.com/mgechev/revive#configuration 1034 | # Default: warning 1035 | severity: warning 1036 | 1037 | # Enable all available rules. 1038 | # Default: false 1039 | enable-all-rules: false 1040 | 1041 | # Sets the default failure confidence. 1042 | # This means that linting errors with less than 0.8 confidence will be ignored. 1043 | # Default: 0.8 1044 | confidence: 0.8 1045 | 1046 | rules: 1047 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic 1048 | - name: atomic 1049 | severity: warning 1050 | disabled: false 1051 | exclude: [""] 1052 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports 1053 | - name: blank-imports 1054 | severity: warning 1055 | disabled: false 1056 | exclude: [""] 1057 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr 1058 | - name: bool-literal-in-expr 1059 | severity: warning 1060 | disabled: false 1061 | exclude: [""] 1062 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc 1063 | - name: call-to-gc 1064 | severity: warning 1065 | disabled: false 1066 | exclude: [""] 1067 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming 1068 | - name: confusing-naming 1069 | severity: warning 1070 | disabled: false 1071 | exclude: [""] 1072 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results 1073 | - name: confusing-results 1074 | severity: warning 1075 | disabled: false 1076 | exclude: [""] 1077 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr 1078 | - name: constant-logical-expr 1079 | severity: warning 1080 | disabled: false 1081 | exclude: [""] 1082 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument 1083 | - name: context-as-argument 1084 | severity: warning 1085 | disabled: false 1086 | exclude: [""] 1087 | arguments: 1088 | - allowTypesBefore: "*testing.T" 1089 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type 1090 | - name: context-keys-type 1091 | severity: warning 1092 | disabled: false 1093 | exclude: [""] 1094 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace 1095 | - name: datarace 1096 | severity: warning 1097 | disabled: false 1098 | exclude: [""] 1099 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit 1100 | - name: deep-exit 1101 | severity: warning 1102 | disabled: false 1103 | exclude: [""] 1104 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer 1105 | - name: defer 1106 | severity: warning 1107 | disabled: false 1108 | exclude: [""] 1109 | arguments: 1110 | - ["call-chain", "loop"] 1111 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports 1112 | - name: dot-imports 1113 | severity: warning 1114 | disabled: false 1115 | exclude: [""] 1116 | arguments: [] 1117 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block 1118 | - name: empty-block 1119 | severity: warning 1120 | disabled: false 1121 | exclude: [""] 1122 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines 1123 | - name: empty-lines 1124 | severity: warning 1125 | disabled: false 1126 | exclude: [""] 1127 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming 1128 | - name: error-naming 1129 | severity: warning 1130 | disabled: false 1131 | exclude: [""] 1132 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return 1133 | - name: error-return 1134 | severity: warning 1135 | disabled: false 1136 | exclude: [""] 1137 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings 1138 | - name: error-strings 1139 | severity: warning 1140 | disabled: false 1141 | exclude: [""] 1142 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf 1143 | - name: errorf 1144 | severity: warning 1145 | disabled: false 1146 | exclude: [""] 1147 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported 1148 | - name: exported 1149 | severity: warning 1150 | disabled: false 1151 | exclude: [""] 1152 | arguments: 1153 | - "checkPrivateReceivers" 1154 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter 1155 | - name: flag-parameter 1156 | severity: warning 1157 | disabled: false 1158 | exclude: [""] 1159 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return 1160 | - name: get-return 1161 | severity: warning 1162 | disabled: false 1163 | exclude: [""] 1164 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches 1165 | - name: identical-branches 1166 | severity: warning 1167 | disabled: false 1168 | exclude: [""] 1169 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return 1170 | - name: if-return 1171 | severity: warning 1172 | disabled: false 1173 | exclude: [""] 1174 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement 1175 | - name: increment-decrement 1176 | severity: warning 1177 | disabled: false 1178 | exclude: [""] 1179 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow 1180 | - name: indent-error-flow 1181 | severity: warning 1182 | disabled: false 1183 | exclude: [""] 1184 | arguments: 1185 | - "preserveScope" 1186 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter 1187 | - name: modifies-parameter 1188 | severity: warning 1189 | disabled: false 1190 | exclude: [""] 1191 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver 1192 | - name: modifies-value-receiver 1193 | severity: warning 1194 | disabled: false 1195 | exclude: [""] 1196 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs 1197 | - name: nested-structs 1198 | severity: warning 1199 | disabled: false 1200 | exclude: [""] 1201 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order 1202 | - name: optimize-operands-order 1203 | severity: warning 1204 | disabled: false 1205 | exclude: [""] 1206 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments 1207 | - name: package-comments 1208 | severity: warning 1209 | disabled: false 1210 | exclude: [""] 1211 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range 1212 | - name: range 1213 | severity: warning 1214 | disabled: false 1215 | exclude: [""] 1216 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure 1217 | - name: range-val-in-closure 1218 | severity: warning 1219 | disabled: false 1220 | exclude: [""] 1221 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming 1222 | - name: receiver-naming 1223 | severity: warning 1224 | disabled: false 1225 | exclude: [""] 1226 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id 1227 | - name: redefines-builtin-id 1228 | severity: warning 1229 | disabled: false 1230 | exclude: [""] 1231 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias 1232 | - name: redundant-import-alias 1233 | severity: warning 1234 | disabled: false 1235 | exclude: [""] 1236 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format 1237 | - name: string-format 1238 | severity: warning 1239 | disabled: false 1240 | exclude: [""] 1241 | arguments: 1242 | - - "core.WriteError[1].Message" 1243 | - "/^([^A-Z]|$)/" 1244 | - must not start with a capital letter 1245 | - - "fmt.Errorf[0]" 1246 | - '/(^|[^\.!?])$/' 1247 | - must not end in punctuation 1248 | - - panic 1249 | - '/^[^\n]*$/' 1250 | - must not contain line breaks 1251 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int 1252 | - name: string-of-int 1253 | severity: warning 1254 | disabled: false 1255 | exclude: [""] 1256 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal 1257 | - name: time-equal 1258 | severity: warning 1259 | disabled: false 1260 | exclude: [""] 1261 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming 1262 | - name: time-naming 1263 | severity: warning 1264 | disabled: false 1265 | exclude: [""] 1266 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion 1267 | - name: unconditional-recursion 1268 | severity: warning 1269 | disabled: false 1270 | exclude: [""] 1271 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming 1272 | - name: unexported-naming 1273 | severity: warning 1274 | disabled: false 1275 | exclude: [""] 1276 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return 1277 | - name: unexported-return 1278 | severity: warning 1279 | disabled: false 1280 | exclude: [""] 1281 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt 1282 | - name: unnecessary-stmt 1283 | severity: warning 1284 | disabled: false 1285 | exclude: [""] 1286 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code 1287 | - name: unreachable-code 1288 | severity: warning 1289 | disabled: false 1290 | exclude: [""] 1291 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter 1292 | - name: unused-parameter 1293 | severity: warning 1294 | disabled: false 1295 | exclude: [""] 1296 | arguments: 1297 | - allowRegex: "^_" 1298 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver 1299 | - name: unused-receiver 1300 | severity: warning 1301 | disabled: false 1302 | exclude: [""] 1303 | arguments: 1304 | - allowRegex: "^_" 1305 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any 1306 | - name: use-any 1307 | severity: warning 1308 | disabled: false 1309 | exclude: [""] 1310 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break 1311 | - name: useless-break 1312 | severity: warning 1313 | disabled: false 1314 | exclude: [""] 1315 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration 1316 | - name: var-declaration 1317 | severity: warning 1318 | disabled: false 1319 | exclude: [""] 1320 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming 1321 | - name: var-naming 1322 | severity: warning 1323 | disabled: false 1324 | exclude: [""] 1325 | arguments: 1326 | - ["ID"] # AllowList 1327 | - ["VM"] # DenyList 1328 | - - upperCaseConst: true # Extra parameter (upperCaseConst|skipPackageNameChecks) 1329 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value 1330 | - name: waitgroup-by-value 1331 | severity: warning 1332 | disabled: false 1333 | exclude: [""] 1334 | 1335 | rowserrcheck: 1336 | # database/sql is always checked 1337 | # Default: [] 1338 | packages: 1339 | 1340 | sloglint: 1341 | # Enforce using attributes only (overrides no-mixed-args, incompatible with kv-only). 1342 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#attributes-only 1343 | # Default: false 1344 | attr-only: true 1345 | # Enforce not using global loggers. 1346 | # Values: 1347 | # - "": disabled 1348 | # - "all": report all global loggers 1349 | # - "default": report only the default slog logger 1350 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global 1351 | # Default: "" 1352 | no-global: "all" 1353 | # Enforce using methods that accept a context. 1354 | # Values: 1355 | # - "": disabled 1356 | # - "all": report all contextless calls 1357 | # - "scope": report only if a context exists in the scope of the outermost function 1358 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only 1359 | # Default: "" 1360 | context: "scope" 1361 | # Enforce using static values for log messages. 1362 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#static-messages 1363 | # Default: false 1364 | static-msg: false 1365 | # Enforce using constants instead of raw keys. 1366 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-raw-keys 1367 | # Default: false 1368 | no-raw-keys: false 1369 | # Enforce a single key naming convention. 1370 | # Values: snake, kebab, camel, pascal 1371 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#key-naming-convention 1372 | # Default: "" 1373 | key-naming-case: camel 1374 | # Enforce not using specific keys. 1375 | # Default: [] 1376 | forbidden-keys: 1377 | - time 1378 | - level 1379 | - msg 1380 | - source 1381 | # Enforce putting arguments on separate lines. 1382 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#forbidden-keys 1383 | # Default: false 1384 | args-on-sep-lines: false 1385 | 1386 | spancheck: 1387 | # Checks to enable. 1388 | # Options include: 1389 | # - `end`: check that `span.End()` is called 1390 | # - `record-error`: check that `span.RecordError(err)` is called when an error is returned 1391 | # - `set-status`: check that `span.SetStatus(codes.Error, msg)` is called when an error is returned 1392 | # Default: ["end"] 1393 | checks: 1394 | - end 1395 | - record-error 1396 | - set-status 1397 | # A list of regexes for function signatures that silence `record-error` and `set-status` reports 1398 | # if found in the call path to a returned error. 1399 | # https://github.com/jjti/go-spancheck#ignore-check-signatures 1400 | # Default: [] 1401 | ignore-check-signatures: 1402 | # A list of regexes for additional function signatures that create spans. 1403 | # This is useful if you have a utility method to create spans. 1404 | # Each entry should be of the form `:`, where `telemetry-type` can be `opentelemetry` or `opencensus`. 1405 | # https://github.com/jjti/go-spancheck#extra-start-span-signatures 1406 | # Default: [] 1407 | extra-start-span-signatures: 1408 | 1409 | stylecheck: 1410 | # STxxxx checks in https://staticcheck.io/docs/configuration/options/#checks 1411 | # Default: ["*"] 1412 | checks: ["all"] 1413 | # https://staticcheck.io/docs/configuration/options/#dot_import_whitelist 1414 | # Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"] 1415 | dot-import-whitelist: 1416 | # https://staticcheck.io/docs/configuration/options/#initialisms 1417 | # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] 1418 | initialisms: 1419 | [ 1420 | "ACL", 1421 | "API", 1422 | "ASCII", 1423 | "CPU", 1424 | "CSS", 1425 | "DNS", 1426 | "EOF", 1427 | "GUID", 1428 | "HTML", 1429 | "HTTP", 1430 | "HTTPS", 1431 | "ID", 1432 | "IP", 1433 | "JSON", 1434 | "QPS", 1435 | "RAM", 1436 | "RPC", 1437 | "SLA", 1438 | "SMTP", 1439 | "SQL", 1440 | "SSH", 1441 | "TCP", 1442 | "TLS", 1443 | "TTL", 1444 | "UDP", 1445 | "UI", 1446 | "GID", 1447 | "UID", 1448 | "UUID", 1449 | "URI", 1450 | "URL", 1451 | "UTF8", 1452 | "VM", 1453 | "XML", 1454 | "XMPP", 1455 | "XSRF", 1456 | "XSS", 1457 | "SIP", 1458 | "RTP", 1459 | "AMQP", 1460 | "DB", 1461 | "TS", 1462 | ] 1463 | # https://staticcheck.io/docs/configuration/options/#http_status_code_whitelist 1464 | # Default: ["200", "400", "404", "500"] 1465 | http-status-code-whitelist: ["200", "400", "404", "500"] 1466 | 1467 | testifylint: 1468 | # Enable all checkers (https://github.com/Antonboom/testifylint#checkers). 1469 | # Default: false 1470 | # enable-all: false 1471 | # Disable checkers by name 1472 | # (in addition to default 1473 | # suite-thelper 1474 | # ). 1475 | # disable: 1476 | 1477 | # Disable all checkers (https://github.com/Antonboom/testifylint#checkers). 1478 | # Default: false 1479 | disable-all: true 1480 | # Enable checkers by name 1481 | # (in addition to default 1482 | # blank-import, bool-compare, compares, empty, error-is-as, error-nil, expected-actual, go-require, float-compare, 1483 | # formatter, len, negative-positive, nil-compare, require-error, suite-broken-parallel, suite-dont-use-pkg, 1484 | # suite-extra-assert-call, suite-subtest-run, useless-assert 1485 | # ). 1486 | enable: 1487 | - blank-import 1488 | - bool-compare 1489 | - compares 1490 | - empty 1491 | - error-is-as 1492 | - error-nil 1493 | - expected-actual 1494 | - float-compare 1495 | - go-require 1496 | - len 1497 | - negative-positive 1498 | - nil-compare 1499 | - suite-dont-use-pkg 1500 | - suite-extra-assert-call 1501 | - suite-thelper 1502 | - useless-assert 1503 | bool-compare: 1504 | # To ignore user defined types (over builtin bool). 1505 | # Default: false 1506 | ignore-custom-types: false 1507 | expected-actual: 1508 | # Regexp for expected variable name. 1509 | # Default: (^(exp(ected)?|want(ed)?)([A-Z]\w*)?$)|(^(\w*[a-z])?(Exp(ected)?|Want(ed)?)$) 1510 | pattern: (^(exp(ected)?|want(ed)?)([A-Z]\w*)?$)|(^(\w*[a-z])?(Exp(ected)?|Want(ed)?)$) 1511 | go-require: 1512 | # To ignore HTTP handlers (like http.HandlerFunc). 1513 | # Default: false 1514 | ignore-http-handlers: false 1515 | require-error: 1516 | # Regexp for assertions to analyze. If defined, then only matched error assertions will be reported. 1517 | # Default: "" 1518 | fn-pattern: "" 1519 | suite-extra-assert-call: 1520 | # To require or remove extra Assert() call? 1521 | # Default: remove 1522 | mode: remove 1523 | 1524 | thelper: 1525 | test: 1526 | # Check *testing.T is first param (or after context.Context) of helper function. 1527 | # Default: true 1528 | first: true 1529 | # Check *testing.T param has name t. 1530 | # Default: true 1531 | name: true 1532 | # Check t.Helper() begins helper function. 1533 | # Default: true 1534 | begin: true 1535 | benchmark: 1536 | # Check *testing.B is first param (or after context.Context) of helper function. 1537 | # Default: true 1538 | first: true 1539 | # Check *testing.B param has name b. 1540 | # Default: true 1541 | name: true 1542 | # Check b.Helper() begins helper function. 1543 | # Default: true 1544 | begin: true 1545 | tb: 1546 | # Check *testing.TB is first param (or after context.Context) of helper function. 1547 | # Default: true 1548 | first: true 1549 | # Check *testing.TB param has name tb. 1550 | # Default: true 1551 | name: true 1552 | # Check tb.Helper() begins helper function. 1553 | # Default: true 1554 | begin: true 1555 | fuzz: 1556 | # Check *testing.F is first param (or after context.Context) of helper function. 1557 | # Default: true 1558 | first: true 1559 | # Check *testing.F param has name f. 1560 | # Default: true 1561 | name: true 1562 | # Check f.Helper() begins helper function. 1563 | # Default: true 1564 | begin: true 1565 | 1566 | usestdlibvars: 1567 | # Suggest the use of http.MethodXX. 1568 | # Default: true 1569 | http-method: true 1570 | # Suggest the use of http.StatusXX. 1571 | # Default: true 1572 | http-status-code: true 1573 | # Suggest the use of time.Weekday.String(). 1574 | # Default: true 1575 | time-weekday: true 1576 | # Suggest the use of time.Month.String(). 1577 | # Default: false 1578 | time-month: true 1579 | # Suggest the use of time.Layout. 1580 | # Default: false 1581 | time-layout: true 1582 | # Suggest the use of crypto.Hash.String(). 1583 | # Default: false 1584 | crypto-hash: true 1585 | # Suggest the use of rpc.DefaultXXPath. 1586 | # Default: false 1587 | default-rpc-path: true 1588 | # Suggest the use of sql.LevelXX.String(). 1589 | # Default: false 1590 | sql-isolation-level: true 1591 | # Suggest the use of tls.SignatureScheme.String(). 1592 | # Default: false 1593 | tls-signature-scheme: true 1594 | # Suggest the use of constant.Kind.String(). 1595 | # Default: false 1596 | constant-kind: true 1597 | 1598 | unconvert: 1599 | # Remove conversions that force intermediate rounding. 1600 | # Default: false 1601 | fast-math: false 1602 | # Be more conservative (experimental). 1603 | # Default: false 1604 | safe: false 1605 | 1606 | unparam: 1607 | # Inspect exported functions. 1608 | # 1609 | # Set to true if no external program/library imports your code. 1610 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 1611 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 1612 | # with golangci-lint call it on a directory with the changed file. 1613 | # 1614 | # Default: false 1615 | check-exported: false 1616 | 1617 | wsl: 1618 | # Do strict checking when assigning from append (x = append(x, y)). 1619 | # If this is set to true - the append call must append either a variable 1620 | # assigned, called or used on the line above. 1621 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#strict-append 1622 | # Default: true 1623 | strict-append: true 1624 | 1625 | # Allows assignments to be cuddled with variables used in calls on 1626 | # line above and calls to be cuddled with assignments of variables 1627 | # used in call on line above. 1628 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-call 1629 | # Default: true 1630 | allow-assign-and-call: true 1631 | 1632 | # Allows assignments to be cuddled with anything. 1633 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-anything 1634 | # Default: false 1635 | allow-assign-and-anything: false 1636 | 1637 | # Allows cuddling to assignments even if they span over multiple lines. 1638 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-multiline-assign 1639 | # Default: true 1640 | allow-multiline-assign: true 1641 | 1642 | # If the number of lines in a case block is equal to or lager than this number, 1643 | # the case *must* end white a newline. 1644 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-case-trailing-whitespace 1645 | # Default: 0 1646 | force-case-trailing-whitespace: 0 1647 | 1648 | # Allow blocks to end with comments. 1649 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-trailing-comment 1650 | # Default: false 1651 | allow-trailing-comment: false 1652 | 1653 | # Allow multiple comments in the beginning of a block separated with newline. 1654 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-separated-leading-comment 1655 | # Default: false 1656 | allow-separated-leading-comment: false 1657 | 1658 | # Allow multiple var/declaration statements to be cuddled. 1659 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-cuddle-declarations 1660 | # Default: false 1661 | allow-cuddle-declarations: false 1662 | 1663 | # A list of call idents that everything can be cuddled with. 1664 | # Defaults: [ "Lock", "RLock" ] 1665 | allow-cuddle-with-calls: ["Lock", "RLock"] 1666 | 1667 | # AllowCuddleWithRHS is a list of right hand side variables that is allowed 1668 | # to be cuddled with anything. 1669 | # Defaults: [ "Unlock", "RUnlock" ] 1670 | allow-cuddle-with-rhs: ["Unlock", "RUnlock"] 1671 | 1672 | # Causes an error when an if-statement that checks an error variable doesn't 1673 | # cuddle with the assignment of that variable. 1674 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-err-cuddling 1675 | # Default: false 1676 | force-err-cuddling: true 1677 | 1678 | # When force-err-cuddling is enabled this is a list of names 1679 | # used for error variables to check for in the conditional. 1680 | # Default: [ "err" ] 1681 | error-variable-names: ["err"] 1682 | 1683 | # Causes an error if a short declaration (:=) cuddles with anything other than 1684 | # another short declaration. 1685 | # This logic overrides force-err-cuddling among others. 1686 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-short-decl-cuddling 1687 | # Default: false 1688 | force-short-decl-cuddling: false 1689 | 1690 | linters: 1691 | # Disable all linters. 1692 | # Default: false 1693 | disable-all: true 1694 | # Enable specific linter 1695 | # https://golangci-lint.run/usage/linters/#enabled-by-default 1696 | enable: 1697 | ### Default linters 1698 | - errcheck # Errcheck is a program for checking for unchecked errors in Go code. 1699 | - gosimple # Linter for Go source code that specializes in simplifying code. 1700 | - govet # Vet examines Go source code and reports suspicious constructs. 1701 | - ineffassign # Detects when assignments to existing variables are not used. 1702 | - staticcheck # It's a set of rules from staticcheck. 1703 | - unused # Checks Go code for unused constants, variables, functions and types. 1704 | ### Additional linters 1705 | - asasalint # Check for pass []any as any in variadic func(...any). 1706 | - asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name. 1707 | - bidichk # Checks for dangerous unicode character sequences. 1708 | - bodyclose # Checks whether HTTP response body is closed successfully. 1709 | - canonicalheader # Canonicalheader checks whether net/http.Header uses canonical header. 1710 | - containedctx # Containedctx is a linter that detects struct contained context.Context field. 1711 | - contextcheck # Check whether the function uses a non-inherited context. 1712 | - copyloopvar # Copyloopvar is a linter detects places where loop variables are copied. 1713 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()). 1714 | - dupword # Checks for duplicate words in the source code. 1715 | - durationcheck # Check for two durations multiplied together. 1716 | - errchkjson # Checks types passed to the json encoding functions. 1717 | - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. 1718 | - errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 1719 | - exhaustive # Check exhaustiveness of enum switch statements. 1720 | - fatcontext # Detects nested contexts in loops. 1721 | - funlen # Tool for detection of long functions. 1722 | - gci # Gci controls Go package import order and makes it always deterministic. 1723 | - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. 1724 | - gochecksumtype # Run exhaustiveness checks on Go "sum types". 1725 | - gocognit # Computes and checks the cognitive complexity of functions. 1726 | - goconst # Finds repeated strings that could be replaced by a constant. 1727 | - gocritic # Provides diagnostics that check for bugs, performance and style issues. 1728 | - godox # Tool for detection of FIXME, TODO and other comment keywords. 1729 | - gofmt # Gofmt checks whether code was gofmt-ed. 1730 | - gofumpt # Gofumpt checks whether code was gofumpt-ed. 1731 | - goprintffuncname # Checks that printf-like functions are named with f at the end. 1732 | - gosec # Inspects source code for security problems. 1733 | - inamedparam # Reports interfaces with unnamed method parameters. 1734 | - interfacebloat # A linter that checks the number of methods inside an interface. 1735 | - intrange # Intrange is a linter to find places where for loops could make use of an integer range. 1736 | - ireturn # Accept Interfaces, Return Concrete Types. 1737 | - lll # Reports long lines. 1738 | - maintidx # Maintidx measures the maintainability index of each function. 1739 | - makezero # Finds slice declarations with non-zero initial length. 1740 | - mirror # Reports wrong mirror patterns of bytes/strings usage. 1741 | - misspell # Finds commonly misspelled English words. 1742 | - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). 1743 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil. 1744 | - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. 1745 | - noctx # Finds sending http request without context.Context. 1746 | - nolintlint # Reports ill-formed or insufficient nolint directives. 1747 | - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. 1748 | - paralleltest # Detects missing usage of t.Parallel() method in your Go test. 1749 | - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. 1750 | - prealloc # Finds slice declarations that could potentially be pre-allocated. 1751 | - predeclared # Find code that shadows one of Go's predeclared identifiers. 1752 | - promlinter # Check Prometheus metrics naming via promlint. 1753 | - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. 1754 | - rowserrcheck # Checks whether Rows.Err of rows is checked successfully. 1755 | - sloglint # Ensure consistent code style when using log/slog. 1756 | - spancheck # Checks for mistakes with OpenTelemetry/Census spans. 1757 | - sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. 1758 | - stylecheck # Stylecheck is a replacement for golint. 1759 | - testifylint # Checks usage of github.com/stretchr/testify. 1760 | - thelper # Thelper detects tests helpers which is not start with t.Helper() method. 1761 | - tparallel # Tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. 1762 | - unconvert # Remove unnecessary type conversions. 1763 | - unparam # Reports unused function parameters. 1764 | - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. 1765 | - wsl # Add or remove empty lines. 1766 | 1767 | # Enable all available linters. 1768 | # Default: false 1769 | # enable-all: false 1770 | # Disable specific linter 1771 | # https://golangci-lint.run/usage/linters/#disabled-by-default 1772 | # disable: 1773 | 1774 | # Enable presets. 1775 | # https://golangci-lint.run/usage/linters 1776 | # Default: [] 1777 | # presets: 1778 | 1779 | # Enable only fast linters from enabled linters set (first run won't be fast) 1780 | # Default: false 1781 | fast: false 1782 | 1783 | issues: 1784 | # List of regexps of issue texts to exclude. 1785 | # 1786 | # But independently of this option we use default exclude patterns, 1787 | # it can be disabled by `exclude-use-default: false`. 1788 | # To list all excluded by default patterns execute `golangci-lint run --help` 1789 | # 1790 | # Default: https://golangci-lint.run/usage/false-positives/#default-exclusions 1791 | # exclude: 1792 | 1793 | # Excluding configuration per-path, per-linter, per-text and per-source 1794 | exclude-rules: 1795 | # Exclude some linters from running on tests files. 1796 | - path: _test\.go 1797 | linters: 1798 | - errcheck 1799 | - dupl 1800 | - funlen 1801 | - gocognit 1802 | - gosec 1803 | - lll 1804 | - maintidx 1805 | - nlreturn 1806 | - revive 1807 | - wsl 1808 | 1809 | # Run some linter only for test files by excluding its issues for everything else. 1810 | - path-except: _test\.go 1811 | linters: 1812 | - paralleltest 1813 | - testifylint 1814 | - thelper 1815 | - tparallel 1816 | 1817 | # Exclude known linters from partially hard-vendored code, 1818 | # which is impossible to exclude via `nolint` comments. 1819 | # `/` will be replaced by current OS file path separator to properly work on Windows. 1820 | # - path: internal/hmac/ 1821 | # text: "weak cryptographic primitive" 1822 | # linters: 1823 | # - gosec 1824 | 1825 | # Exclude some `staticcheck` messages. 1826 | # - linters: 1827 | # - staticcheck 1828 | # text: "SA9003:" 1829 | 1830 | # Exclude `lll` issues for long lines with `go:generate`. 1831 | - linters: 1832 | - lll 1833 | source: "^//go:generate " 1834 | 1835 | # Exclude govet shadow for err variable. 1836 | - text: 'shadow: declaration of "err" shadows declaration at' 1837 | linters: [govet] 1838 | 1839 | # Independently of option `exclude` we use default exclude patterns, 1840 | # it can be disabled by this option. 1841 | # To list all excluded by default patterns execute `golangci-lint run --help`. 1842 | # Default: true 1843 | exclude-use-default: false 1844 | 1845 | # If set to true, `exclude` and `exclude-rules` regular expressions become case-sensitive. 1846 | # Default: false 1847 | exclude-case-sensitive: false 1848 | 1849 | # Which dirs to exclude: issues from them won't be reported. 1850 | # Can use regexp here: `generated.*`, regexp is applied on full path, 1851 | # including the path prefix if one is set. 1852 | # Default dirs are skipped independently of this option's value (see exclude-dirs-use-default). 1853 | # "/" will be replaced by current OS file path separator to properly work on Windows. 1854 | # Default: [] 1855 | exclude-dirs: 1856 | - mocks 1857 | 1858 | # Enables exclude of directories: 1859 | # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 1860 | # Default: true 1861 | exclude-dirs-use-default: true 1862 | 1863 | # Which files to exclude: they will be analyzed, but issues from them won't be reported. 1864 | # There is no need to include all autogenerated files, 1865 | # we confidently recognize autogenerated files. 1866 | # If it's not, please let us know. 1867 | # "/" will be replaced by current OS file path separator to properly work on Windows. 1868 | # Default: [] 1869 | # exclude-files: 1870 | 1871 | # Mode of the generated files analysis. 1872 | # 1873 | # - `strict`: sources are excluded by following strictly the Go generated file convention. 1874 | # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` 1875 | # This line must appear before the first non-comment, non-blank text in the file. 1876 | # https://go.dev/s/generatedcode 1877 | # - `lax`: sources are excluded if they contain lines `autogenerated file`, `code generated`, `do not edit`, etc. 1878 | # - `disable`: disable the generated files exclusion. 1879 | # 1880 | # Default: lax 1881 | exclude-generated: lax 1882 | 1883 | # The list of ids of default excludes to include or disable. 1884 | # https://golangci-lint.run/usage/false-positives/#default-exclusions 1885 | # Default: [] 1886 | # include: 1887 | 1888 | # Maximum issues count per one linter. 1889 | # Set to 0 to disable. 1890 | # Default: 50 1891 | max-issues-per-linter: 0 1892 | 1893 | # Maximum count of issues with the same text. 1894 | # Set to 0 to disable. 1895 | # Default: 3 1896 | max-same-issues: 0 1897 | 1898 | # Show only new issues: if there are unstaged changes or untracked files, 1899 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 1900 | # It's a super-useful option for integration of golangci-lint into existing large codebase. 1901 | # It's not practical to fix all existing issues at the moment of integration: 1902 | # much better don't allow issues in new code. 1903 | # 1904 | # Default: false 1905 | new: false 1906 | 1907 | # Show only new issues created after git revision `REV`. 1908 | # Default: "" 1909 | # new-from-rev: 1910 | 1911 | # Show only new issues created in git patch with set file path. 1912 | # Default: "" 1913 | # new-from-patch: 1914 | 1915 | # Show issues in any part of update files (requires new-from-rev or new-from-patch). 1916 | # Default: false 1917 | whole-files: false 1918 | 1919 | # Fix found issues (if it's supported by the linter). 1920 | # Default: false 1921 | fix: true 1922 | 1923 | severity: 1924 | # Set the default severity for issues. 1925 | # 1926 | # If severity rules are defined and the issues do not match or no severity is provided to the rule 1927 | # this will be the default severity applied. 1928 | # Severities should match the supported severity names of the selected out format. 1929 | # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity 1930 | # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel 1931 | # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message 1932 | # - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance 1933 | # 1934 | # `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...) 1935 | # 1936 | # Default: "" 1937 | default-severity: warning 1938 | 1939 | # If set to true `severity-rules` regular expressions become case-sensitive. 1940 | # Default: false 1941 | case-sensitive: false 1942 | 1943 | # When a list of severity rules are provided, severity information will be added to lint issues. 1944 | # Severity rules have the same filtering capability as exclude rules 1945 | # except you are allowed to specify one matcher per severity rule. 1946 | # 1947 | # `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...) 1948 | # 1949 | # Only affects out formats that support setting severity information. 1950 | # 1951 | # Default: [] 1952 | rules: 1953 | - linters: 1954 | - funlen 1955 | - gocognit 1956 | - lll 1957 | - paralleltest 1958 | - wsl 1959 | severity: info 1960 | - linters: 1961 | - errcheck 1962 | - gosec 1963 | - gosimple 1964 | - govet 1965 | - ineffassign 1966 | - staticcheck 1967 | - unused 1968 | severity: error 1969 | --------------------------------------------------------------------------------