diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f1e0a18 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index df957db..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build -on: [push] -jobs: - - build: - name: Build - runs-on: ubuntu-latest - steps: - - - name: Set up Go 1.13 - uses: actions/setup-go@v1 - with: - go-version: 1.13 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v1 - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Build - run: go build -v . diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..6ec9a11 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,27 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Build + run: make build + + - name: Test + run: make test diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..367a3a2 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,69 @@ +name: golangci-lint +on: + push: + branches: + - master + - main + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` + # option. + # pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + # Require: The version of golangci-lint to use. + # + # When `install-mode` is `binary` (default) the value can be v1.2 or + # v1.2.3 or `latest` to use the latest version. + # + # When `install-mode` is `goinstall` the value can be v1.2.3, + # `latest`, or the hash of a commit. + version: 'latest' + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of + # the repository. + # + # The location of the configuration file can be changed by using + # `--config=`. + # + # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default + # value is `false`. + # only-new-issues: true + + # Optional: if set to true, then all caching functionality will be + # completely disabled, takes precedence over all other caching + # options. + # skip-cache: true + + # Optional: if set to true, then the action won't cache or restore + # ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true, then the action won't cache or restore + # ~/.cache/go-build. + # skip-build-cache: true + + # Optional: The mode to install golangci-lint. It can be 'binary' or + # 'goinstall'. + # install-mode: "goinstall" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 001a4f9..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Lint -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - fmt: - name: fmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: check - uses: grandcolline/golang-github-actions@v1.1.0 - with: - run: fmt - token: ${{ secrets.GITHUB_TOKEN }} - - vet: - name: vet - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: check - uses: grandcolline/golang-github-actions@v1.1.0 - with: - run: vet - token: ${{ secrets.GITHUB_TOKEN }} - - lint: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: check - uses: grandcolline/golang-github-actions@v1.1.0 - with: - run: lint - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 8aa5e93..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Test - -on: - push: - branches: - - '*' - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.14 - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Run tests - run: go test \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..1b16655 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,49 @@ +run: + timeout: 5m + +linters: + enable: + - asciicheck + - bidichk + - errorlint + - gocritic + # - gofmt # we are using gofumpt instead + - gofumpt + - goimports + - makezero + - misspell + - nolintlint + - perfsprint + - prealloc + - testifylint + - unconvert + - usestdlibvars + - wastedassign + - wrapcheck + +linters-settings: + goimports: + # A comma-separated list of prefixes, which, if set, checks import paths + # with the given prefixes are grouped after 3rd-party packages. + # Default: "" + local-prefixes: "github.com/inexio/go-monitoringplugin" + + nolintlint: + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + revive: + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming + - name: receiver-naming + disabled: true + +issues: + exclude-rules: + - path: _test\.go + linters: + - wrapcheck diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..86b3327 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +TEST_ARGS= + +test: + go test ${TEST_ARGS} ./... + +build: + go build -ldflags="-s -w" ./ + +clean: + rm -f check_wg diff --git a/README.md b/README.md index e73735f..44a7836 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,71 @@ # go-monitoringplugin - [![Go Report Card](https://goreportcard.com/badge/github.com/inexio/go-monitoringplugin)](https://goreportcard.com/report/github.com/inexio/go-monitoringplugin) [![GitHub license](https://img.shields.io/badge/license-BSD-blue.svg)](https://github.com/inexio/go-monitoringplugin/blob/master/LICENSE) [![GoDoc doc](https://img.shields.io/badge/godoc-reference-blue)](https://godoc.org/github.com/inexio/go-monitoringplugin) + ## Description -Golang package for writing monitoring check plugins for [nagios](https://www.nagios.org/), [icinga2](https://icinga.com/), [zabbix](https://www.zabbix.com/), [checkmk](https://checkmk.com/), etc. + +Golang package for writing monitoring check plugins for +[nagios](https://www.nagios.org/), [icinga2](https://icinga.com/), +[zabbix](https://www.zabbix.com/), [checkmk](https://checkmk.com/), etc. + The package complies with the [Monitoring Plugins Development Guidelines](https://www.monitoring-plugins.org/doc/guidelines.html). ## Example / Usage - package main - - import ( - monitoringplugin "github.com/inexio/go-monitoringplugin" - ) - - func main() { - //Creating response with a default ok message that will be displayed when the checks exits with status ok - response := monitoringplugin.NewResponse("everything checked!") - - //Set output delimiter (default is \n) - //response.SetOutputDelimiter(" / ") - - //updating check plugin status and adding message to the ouput (status only changes if the new status is worse than the current one) - response.UpdateStatus(monitoringplugin.OK, "something is ok!") //check status stays ok - response.UpdateStatus(monitoringplugin.CRITICAL, "something else is critical!") //check status updates to critical - response.UpdateStatus(monitoringplugin.WARNING, "something else is warning!") //check status stays critical, but message will be added to the output - - - //adding performance data - err := response.AddPerformanceDataPoint(monitoringplugin.NewPerformanceDataPoint("response_time", 10, "s").SetWarn(10).SetCrit(20).SetMin(0)) - if err != nil { - //error handling - } - err = response.AddPerformanceDataPoint(monitoringplugin.NewPerformanceDataPoint("memory_usage", 50, "%").SetWarn(80).SetCrit(90).SetMin(0).SetMax(100)) - if err != nil { - //error handling - } - - response.OutputAndExit() - /* exits program with exit code 2 and outputs: - CRITICAL: something is ok! - something else is critical! - something else is warning! | 'response_time'=10s;10;20;0; 'memory_usage'=50%;80;90;0;100 - */ + +``` go +package main + +import "github.com/inexio/go-monitoringplugin/v2" + +func main() { + // Creating response with a default ok message, that will be displayed when + // the checks exits with status ok. + response := monitoringplugin.NewResponse("everything checked!") + + // Set output delimiter (default is \n) + // response.SetOutputDelimiter(" / ") + + // Updating check plugin status and adding message to the output (status only + // changes if the new status is worse than the current one). + + // check status stays ok + response.UpdateStatus(monitoringplugin.OK, "something is ok!") + // check status updates to critical + response.UpdateStatus(monitoringplugin.CRITICAL, + "something else is critical!") + // check status stays critical, but message will be added to the output + response.UpdateStatus(monitoringplugin.WARNING, "something else is warning!") + + // adding performance data + p1 := monitoringplugin.NewPerformanceDataPoint("response_time", 10). + SetUnit("s").SetMin(0) + p1.NewThresholds(0, 10, 0, 20) + if err := response.AddPerformanceDataPoint(p1); err != nil { + // error handling + } + + p2 := monitoringplugin.NewPerformanceDataPoint("memory_usage", 50.6). + SetUnit("%").SetMin(0).SetMax(100) + p2.NewThresholds(0, 80, 0, 90) + if err := response.AddPerformanceDataPoint(p2); err != nil { + // error handling + } + + err = response.AddPerformanceDataPoint( + monitoringplugin.NewPerformanceDataPoint("memory_usage", 50.6). + SetUnit("%").SetMin(0).SetMax(100). + SetThresholds(monitoringplugin.NewThresholds(0, 80.0, 0, 90.0))) + if err != nil { + // error handling } + + response.OutputAndExit() + /* exits program with exit code 2 and outputs: + CRITICAL: something is ok! + something else is critical! + something else is warning! | 'response_time'=10s;10;20;0; 'memory_usage'=50%;80;90;0;100 + */ +} +``` diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..6d6815c --- /dev/null +++ b/doc_test.go @@ -0,0 +1,48 @@ +package monitoringplugin_test + +import ( + "fmt" + + "github.com/inexio/go-monitoringplugin/v2" +) + +func Example_basicUsage() { + // Creating response with a default ok message, that will be displayed when + // the checks exits with status ok. + response := monitoringplugin.NewResponse("everything checked!") + + // Set output delimiter (default is \n) + // response.SetOutputDelimiter(" / ") + + // Updating check plugin status and adding message to the output (status only + // changes if the new status is worse than the current one). + + // check status stays ok + response.UpdateStatus(monitoringplugin.OK, "something is ok!") + // check status updates to critical + response.UpdateStatus(monitoringplugin.CRITICAL, + "something else is critical!") + // check status stays critical, but message will be added to the output + response.UpdateStatus(monitoringplugin.WARNING, "something else is warning!") + + // adding performance data + p1 := monitoringplugin.NewPerformanceDataPoint("response_time", 10). + SetUnit("s").SetMin(0) + p1.NewThresholds(0, 10, 0, 20) + if err := response.AddPerformanceDataPoint(p1); err != nil { + // error handling + } + + p2 := monitoringplugin.NewPerformanceDataPoint("memory_usage", 50.6). + SetUnit("%").SetMin(0).SetMax(100) + p2.NewThresholds(0, 80, 0, 90) + if err := response.AddPerformanceDataPoint(p2); err != nil { + // error handling + } + + fmt.Println(response.GetInfo().RawOutput) + // Output: + // CRITICAL: something else is critical! + // something else is warning! + // something is ok! | 'response_time'=10s;10;20;0; 'memory_usage'=50.6%;80;90;0;100 +} diff --git a/go.mod b/go.mod index 340ed76..ddad263 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ -module github.com/inexio/go-monitoringplugin +module github.com/inexio/go-monitoringplugin/v2 -go 1.14 +go 1.22 + +require github.com/stretchr/testify v1.9.0 require ( - github.com/pkg/errors v0.8.1 - github.com/stretchr/testify v1.6.1 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 48d262e..60ce688 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,10 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/performance_data.go b/performance_data.go index cb5a1b9..319ccc9 100644 --- a/performance_data.go +++ b/performance_data.go @@ -2,207 +2,207 @@ package monitoringplugin import ( "bytes" + "cmp" "encoding/json" + "errors" "fmt" - "github.com/pkg/errors" - "math/big" "regexp" - "strconv" ) +func newPerformanceDataPointKey(metric, label string) performanceDataPointKey { + return performanceDataPointKey{Metric: metric, Label: label} +} + type performanceDataPointKey struct { Metric string `json:"metric"` Label string `json:"label,omitempty"` } +func (self *performanceDataPointKey) String() string { + if self.Label == "" { + return self.Metric + } + return self.Metric + " and label " + self.Label +} + +func newPerformanceData() performanceData { + return performanceData{ + keys: make(map[performanceDataPointKey]int), + } +} + // performanceData is a map where all performanceDataPoints are stored. // It assigns labels (string) to performanceDataPoints. -type performanceData map[performanceDataPointKey]PerformanceDataPoint - -/* -add adds a PerformanceDataPoint to the performanceData Map. -The function checks if a PerformanceDataPoint is valid and if there is already another PerformanceDataPoint with the -same metric in the performanceData map. -Usage: - err := performanceData.add(NewPerformanceDataPoint("temperature", 32, "°C").SetWarn(35).SetCrit(40)) - if err != nil { - ... - } -*/ -func (p *performanceData) add(point *PerformanceDataPoint) error { +type performanceData struct { + keys map[performanceDataPointKey]int + points []anyDataPoint +} + +type anyDataPoint interface { + Validate() error + HasThresholds() bool + CheckThresholds() int + Name() string + + key() performanceDataPointKey + output(jsonLabel bool) []byte +} + +// add adds a PerformanceDataPoint to the performanceData Map. The function +// checks if a PerformanceDataPoint is valid and if there is already another +// PerformanceDataPoint with the same metric in the performanceData map. +// +// Usage: +// +// err := performanceData.add(NewPerformanceDataPoint("temperature", 32, "°C").SetWarn(35).SetCrit(40)) +// if err != nil { +// ... +// } +func (p *performanceData) add(point anyDataPoint) error { if err := point.Validate(); err != nil { - return errors.Wrap(err, "given performance data point is not valid") + return fmt.Errorf("given performance data point is not valid: %w", err) } - key := performanceDataPointKey{point.Metric, point.Label} - if _, ok := (*p)[key]; ok { - return fmt.Errorf("a performance data point with the metric '%s' does already exist", func(key performanceDataPointKey) string { - res := key.Metric - if key.Label != "" { - res += " and label " + key.Label - } - return res - }(key)) + key := point.key() + if _, ok := p.keys[key]; ok { + return fmt.Errorf( + "a performance data point with the metric '%s' does already exist", key) } - (*p)[key] = *point + p.keys[key] = len(p.points) + p.points = append(p.points, point) return nil } -// getInfo returns all information for performance data. -func (p performanceData) getInfo() []PerformanceDataPoint { - var info []PerformanceDataPoint - for _, pd := range p { - info = append(info, pd) +func (p *performanceData) point(key performanceDataPointKey) anyDataPoint { + if i, ok := p.keys[key]; ok { + return p.points[i] } - return info + return nil +} + +// getInfo returns all information for performance data. +func (p *performanceData) getInfo() []anyDataPoint { + return p.points +} + +// NewPerformanceDataPoint creates a new PerformanceDataPoint. Metric and value +// are mandatory but are not checked at this point, the performanceDatePoint's +// validation is checked later when it is added to the performanceData list in +// the function performanceData.add(*PerformanceDataPoint). +// +// It is possible to directly add thresholds, min and max values with the +// functions SetThresholds(Thresholds), SetMin(int) and SetMax(int). +// +// Usage: +// +// PerformanceDataPoint := NewPerformanceDataPoint("memory_usage", 55).SetUnit("%") +func NewPerformanceDataPoint[T cmp.Ordered](metric string, value T, +) *PerformanceDataPoint[T] { + return &PerformanceDataPoint[T]{Metric: metric, Value: value} } // PerformanceDataPoint contains all information of one PerformanceDataPoint. -type PerformanceDataPoint struct { - Metric string `json:"metric" xml:"metric"` - Label string `json:"label" xml:"label"` - Value interface{} `json:"value" xml:"value"` - Unit string `json:"unit" xml:"unit"` - Thresholds Thresholds `json:"thresholds" xml:"thresholds"` - Min interface{} `json:"min" xml:"min"` - Max interface{} `json:"max" xml:"max"` +type PerformanceDataPoint[T cmp.Ordered] struct { + Metric string `json:"metric" xml:"metric"` + Label string `json:"label" xml:"label"` + Value T `json:"value" xml:"value"` + Unit string `json:"unit" xml:"unit"` + Thresholds Thresholds[T] `json:"thresholds" xml:"thresholds"` + Min T `json:"min" xml:"min"` + Max T `json:"max" xml:"max"` + + hasMin, hasMax bool } -/* -Validate validates a PerformanceDataPoint. -This function is used to check if a PerformanceDataPoint is compatible with the documentation from -'http://nagios-plugins.org/doc/guidelines.html'(valid name and unit, value is inside the range of min and max etc.) -*/ -func (p *PerformanceDataPoint) Validate() error { +var ( + reInvalidMetricLabel = regexp.MustCompile("([='])") + reInvalidUnit = regexp.MustCompile("([0-9;'\"])") +) + +// Validate validates a PerformanceDataPoint. This function is used to check if +// a PerformanceDataPoint is compatible with the documentation from +// [Monitoring Plugins Development Guidelines](https://www.monitoring-plugins.org/doc/guidelines.html) +// (valid name and unit, value is inside the range of min and max etc.) +func (p *PerformanceDataPoint[T]) Validate() error { if p.Metric == "" { return errors.New("data point metric cannot be an empty string") } - match, err := regexp.MatchString("([='])", p.Metric) - if err != nil { - return errors.Wrap(err, "error during regex match") - } - if match { + if reInvalidMetricLabel.MatchString(p.Metric) { return errors.New("metric contains invalid character") } - match, err = regexp.MatchString("([='])", p.Label) - if err != nil { - return errors.Wrap(err, "error during regex match") - } - if match { + if reInvalidMetricLabel.MatchString(p.Label) { return errors.New("metric contains invalid character") } - match, err = regexp.MatchString("([0-9;'\"])", p.Unit) - if err != nil { - return errors.Wrap(err, "error during regex match") - } - if match { + if reInvalidUnit.MatchString(p.Unit) { return errors.New("unit can not contain numbers, semicolon or quotes") } - var min, max, value big.Float - _, _, err = value.Parse(fmt.Sprint(p.Value), 10) - if err != nil { - return errors.Wrap(err, "can't parse value") + if p.hasMin && cmp.Compare(p.Min, p.Value) == 1 { + return errors.New("value cannot be smaller than min") } - if p.Min != nil { - _, _, err = min.Parse(fmt.Sprint(p.Min), 10) - if err != nil { - return errors.Wrap(err, "can't parse min") - } - switch min.Cmp(&value) { - case 1: - return errors.New("value cannot be smaller than min") - default: - } - } - if p.Max != nil { - _, _, err = max.Parse(fmt.Sprint(p.Max), 10) - if err != nil { - return errors.Wrap(err, "can't parse max") - } - switch max.Cmp(&value) { - case -1: - return errors.New("value cannot be larger than max") - default: - } + if p.hasMax && cmp.Compare(p.Max, p.Value) == -1 { + return errors.New("value cannot be larger than max") } - if p.Min != nil && p.Max != nil { - switch min.Cmp(&max) { - case 1: - return errors.New("min cannot be larger than max") - default: - } + + if p.hasMin && p.hasMax && cmp.Compare(p.Min, p.Max) == 1 { + return errors.New("min cannot be larger than max") } - if !p.Thresholds.IsEmpty() { - err = p.Thresholds.Validate() - if err != nil { - return errors.Wrap(err, "thresholds are invalid") + if p.HasThresholds() { + if err := p.Thresholds.Validate(); err != nil { + return fmt.Errorf("thresholds are invalid: %w", err) } } - return nil } -/* -NewPerformanceDataPoint creates a new PerformanceDataPoint. Metric and value are mandatory but are not checked at this -point, the performanceDatePoint's validation is checked later when it is added to the performanceData list in the -function performanceData.add(*PerformanceDataPoint). -It is possible to directly add thresholds, min and max values with the functions SetThresholds(Thresholds), -SetMin(int) and SetMax(int). -Usage: - PerformanceDataPoint := NewPerformanceDataPoint("memory_usage", 55).SetUnit("%") -*/ -func NewPerformanceDataPoint(metric string, value interface{}) *PerformanceDataPoint { - return &PerformanceDataPoint{ - Metric: metric, - Value: value, - } +func (p *PerformanceDataPoint[T]) key() performanceDataPointKey { + return newPerformanceDataPointKey(p.Metric, p.Label) } // SetUnit sets the unit of the performance data point -func (p *PerformanceDataPoint) SetUnit(unit string) *PerformanceDataPoint { +func (p *PerformanceDataPoint[T]) SetUnit(unit string) *PerformanceDataPoint[T] { p.Unit = unit return p } // SetMin sets minimum value. -func (p *PerformanceDataPoint) SetMin(min interface{}) *PerformanceDataPoint { - p.Min = min +func (p *PerformanceDataPoint[T]) SetMin(min T) *PerformanceDataPoint[T] { + p.Min, p.hasMin = min, true return p } // SetMax sets maximum value. -func (p *PerformanceDataPoint) SetMax(max interface{}) *PerformanceDataPoint { - p.Max = max +func (p *PerformanceDataPoint[T]) SetMax(max T) *PerformanceDataPoint[T] { + p.Max, p.hasMax = max, true return p } // SetLabel adds a tag to the performance data point // If one tag is added more than once, the value before will be overwritten -func (p *PerformanceDataPoint) SetLabel(label string) *PerformanceDataPoint { +func (p *PerformanceDataPoint[T]) SetLabel(label string, +) *PerformanceDataPoint[T] { p.Label = label return p } // SetThresholds sets the thresholds for the performance data point -func (p *PerformanceDataPoint) SetThresholds(thresholds Thresholds) *PerformanceDataPoint { +func (p *PerformanceDataPoint[T]) SetThresholds(thresholds Thresholds[T], +) *PerformanceDataPoint[T] { p.Thresholds = thresholds return p } -// This function returns the PerformanceDataPoint in the specified format that will be returned by the check plugin. -func (p *PerformanceDataPoint) output(jsonLabel bool) []byte { +// This function returns the PerformanceDataPoint in the specified format that +// will be returned by the check plugin. +func (p *PerformanceDataPoint[T]) output(jsonLabel bool) []byte { var buffer bytes.Buffer if jsonLabel { buffer.WriteByte('\'') - key := performanceDataPointKey{ - Metric: p.Metric, - Label: p.Label, - } + key := performanceDataPointKey{Metric: p.Metric, Label: p.Label} jsonKey, _ := json.Marshal(key) buffer.Write(jsonKey) buffer.WriteByte('\'') @@ -217,16 +217,10 @@ func (p *PerformanceDataPoint) output(jsonLabel bool) []byte { } buffer.WriteByte('=') - switch p.Value.(type) { - case float64: - buffer.WriteString(strconv.FormatFloat(p.Value.(float64), 'f', -1, 64)) - default: - buffer.WriteString(fmt.Sprint(p.Value)) - } - + buffer.WriteString(fmt.Sprint(p.Value)) buffer.WriteString(p.Unit) - if !p.Thresholds.IsEmpty() || p.Max != nil || p.Min != nil { + if p.HasThresholds() || p.hasMax || p.hasMin { buffer.WriteByte(';') if p.Thresholds.HasWarning() { buffer.WriteString(p.Thresholds.getWarning()) @@ -236,24 +230,42 @@ func (p *PerformanceDataPoint) output(jsonLabel bool) []byte { buffer.WriteString(p.Thresholds.getCritical()) } buffer.WriteByte(';') - if p.Min != nil { - switch min := p.Min.(type) { - case float64: - buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64)) - default: - buffer.WriteString(fmt.Sprint(min)) - } + if p.hasMin { + buffer.WriteString(fmt.Sprint(p.Min)) } buffer.WriteByte(';') - if p.Max != nil { - switch max := p.Max.(type) { - case float64: - buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64)) - default: - buffer.WriteString(fmt.Sprint(max)) - } + if p.hasMax { + buffer.WriteString(fmt.Sprint(p.Max)) } } - return buffer.Bytes() } + +// HasThresholds checks if the thresholds are not empty. +func (p *PerformanceDataPoint[T]) HasThresholds() bool { + return !p.Thresholds.IsEmpty() +} + +// Name returns a human-readable name suitable for [Response.UpdateStatus]. +func (p *PerformanceDataPoint[T]) Name() string { + if p.Label == "" { + return p.Metric + } + return p.Metric + " (" + p.Label + ")" +} + +// CheckThresholds checks if [Value] is violating the thresholds. See +// [Thresholds.CheckValue]. +func (p *PerformanceDataPoint[T]) CheckThresholds() int { + return p.Thresholds.CheckValue(p.Value) +} + +// NewThresholds is a wrapper, which creates [NewThresholds] with same type as +// [Value], [SetThresholds] it and returns pointer to [Thresholds]. +func (p *PerformanceDataPoint[T]) NewThresholds(warningMin, warningMax, + criticalMin, criticalMax T, +) *Thresholds[T] { + th := NewThresholds(warningMin, warningMax, criticalMin, criticalMax) + p.SetThresholds(th) + return &p.Thresholds +} diff --git a/performance_data_test.go b/performance_data_test.go index 19460da..fbc6673 100644 --- a/performance_data_test.go +++ b/performance_data_test.go @@ -2,289 +2,243 @@ package monitoringplugin import ( "fmt" - "regexp" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPerformanceDataPointCreation(t *testing.T) { - metric := "testMetric" - var value float64 = 10 + const metric = "testMetric" + const value = float64(10) p := NewPerformanceDataPoint(metric, value) + assert.Implements(t, (*anyDataPoint)(nil), p) - if p.Metric != metric || p.Value != value { - t.Error("the created PerfomanceDataPoint NewPerformanceDataPoint") - } + require.Equal(t, metric, p.Metric) + //nolint:testifylint // float-compare safe here + require.Equal(t, value, p.Value) - unit := "%" + const unit = "%" p.SetUnit(unit) - if p.Unit != unit { - t.Error("SetUnit failed") - } + require.Equal(t, unit, p.Unit, "SetUnit failed") - label := "testLabel" + const label = "testLabel" p.SetLabel(label) - if p.Label != label { - t.Error("SetLabel failed") - } + require.Equal(t, label, p.Label, "SetLabel failed") - var min float64 + const min = float64(0) p.SetMin(min) - if p.Min != min || p.Min == nil { - t.Error("SetMin failed") - } + //nolint:testifylint // float-compare safe here + require.Equal(t, min, p.Min, "SetMin failed") + require.True(t, p.hasMin, "SetMin failed") - var max float64 = 100 + const max = float64(100) p.SetMax(max) - if p.Max != max || p.Max == nil { - t.Error("SetMax failed") - } + //nolint:testifylint // float-compare safe here + require.Equal(t, max, p.Max, "SetMax failed") + require.True(t, p.hasMax, "SetMax failed") - thresholds := Thresholds{ - WarningMin: 0, - WarningMax: 10, - CriticalMin: 0, - CriticalMax: 20, - } - p.SetThresholds(thresholds) - if p.Thresholds != thresholds { - t.Error("SetThresholds failed") - } - return + assert.False(t, p.HasThresholds()) + th := p.NewThresholds(0, 10, 0, 20) + assert.Same(t, &p.Thresholds, th, "NewThresholds failed") + assert.True(t, p.HasThresholds()) + + p.SetThresholds(*th) + require.Equal(t, *th, p.Thresholds, "SetThresholds failed") } func TestPerformanceDataPoint_validate(t *testing.T) { p := NewPerformanceDataPoint("metric", 10).SetMin(0).SetMax(100) - if err := p.Validate(); err != nil { - t.Error("valid performance data point resulted in an error: " + err.Error()) - } + require.NoError(t, p.Validate(), + "valid performance data point resulted in an error") - //empty metric + // empty metric p = NewPerformanceDataPoint("", 10) - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: empty metric)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: empty metric)") - //invalid metric + // invalid metric p = NewPerformanceDataPoint("metric=", 10) - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid metric, contains =)") - } - p = NewPerformanceDataPoint("'metric'", 10) + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid metric, contains =)") - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid metric, contains single quotes)") - } + p = NewPerformanceDataPoint("'metric'", 10) + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid metric, contains single quotes)") - //invalid unit + // invalid unit p = NewPerformanceDataPoint("metric", 10).SetUnit("unit1") - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid unit, contains numbers)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid unit, contains numbers)") + p = NewPerformanceDataPoint("metric", 10).SetUnit("unit;") - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid unit, contains semicolon)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid unit, contains semicolon)") + p = NewPerformanceDataPoint("metric", 10).SetUnit("unit'") - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid unit, contains single quotes)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid unit, contains single quotes)") + p = NewPerformanceDataPoint("metric", 10).SetUnit("unit\"") - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: invalid unit, contains double quotes)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: invalid unit, contains double quotes)") - //value < min + // value < min p = NewPerformanceDataPoint("metric", 10).SetMin(50) - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: value < min)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: value < min)") - //value > max + // value > max p = NewPerformanceDataPoint("metric", 10).SetMax(5) - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: value < min)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: value < min)") - //min > max + // min > max p = NewPerformanceDataPoint("metric", 10).SetMin(10).SetMax(5) - if err := p.Validate(); err == nil { - t.Error("invalid performance data did not return an error (case: max < min)") - } + require.Error(t, p.Validate(), + "invalid performance data did not return an error (case: max < min)") } func TestPerformanceDataPoint_output(t *testing.T) { - label := "metric" - value := 10.0 - unit := "s" - warn := 40.0 - crit := 50.0 - min := 0.0 - max := 60.0 + const label = "metric" + const value = float64(10.0) + const unit = "s" + const warn = float64(40.0) + const crit = float64(50.0) + const min = float64(0.0) + const max = float64(60.0) p := NewPerformanceDataPoint(label, value) regex := fmt.Sprintf("'%s'=%g", label, value) - match, err := regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") p.SetUnit(unit) regex = fmt.Sprintf("'%s'=%g%s", label, value, unit) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") p.SetMax(max) regex = fmt.Sprintf("'%s'=%g%s;;;;%g", label, value, unit, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } - - p.SetThresholds(NewThresholds(nil, warn, nil, crit)) - regex = fmt.Sprintf("'%s'=%g%s;~:%g;~:%g;;%g", label, value, unit, warn, crit, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } - - p.SetThresholds(NewThresholds(0, nil, -10, nil)) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") + + p.NewThresholds(0, warn, 0, crit). + UseWarning(false, true).UseCritical(false, true) + regex = fmt.Sprintf("'%s'=%g%s;~:%g;~:%g;;%g", label, value, unit, warn, crit, + max) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") + + p.NewThresholds(0, 0, -10, 0). + UseWarning(true, false).UseCritical(true, false) regex = fmt.Sprintf("'%s'=%g%s;%d:;%d:;;%g", label, value, unit, 0, -10, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") - p.SetThresholds(NewThresholds(5, 10, 3, 11)) - regex = fmt.Sprintf("'%s'=%g%s;%d:%d;%d:%d;;%g", label, value, unit, 5, 10, 3, 11, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + p.NewThresholds(5, 10, 3, 11) + regex = fmt.Sprintf("'%s'=%g%s;%d:%d;%d:%d;;%g", label, value, unit, 5, 10, 3, + 11, max) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") - p.SetThresholds(NewThresholds(0, warn, 0, crit)) - regex = fmt.Sprintf("'%s'=%g%s;%g;%g;;%g", label, value, unit, warn, crit, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + p.NewThresholds(0, warn, 0, crit) + regex = fmt.Sprintf("'%s'=%g%s;%g;%g;;%g", label, value, unit, warn, crit, + max) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") p.SetMin(min) - regex = fmt.Sprintf("'%s'=%g%s;%g;%g;%g;%g", label, value, unit, warn, crit, min, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + regex = fmt.Sprintf("'%s'=%g%s;%g;%g;%g;%g", label, value, unit, warn, crit, + min, max) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") - regex = fmt.Sprintf(`'{"metric":"%s"}'=%g%s;%g;%g;%g;%g`, label, value, unit, warn, crit, min, max) - match, err = regexp.Match(regex, p.output(true)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } + regex = fmt.Sprintf(`'{"metric":"%s"}'=%g%s;%g;%g;%g;%g`, label, value, unit, + warn, crit, min, max) + require.Contains(t, string(p.output(true)), regex, + "output string did not match regex") tag := "tag" p.SetLabel(tag) - regex = fmt.Sprintf(`'{"metric":"%s","label":"%s"}'=%g%s;%g;%g;%g;%g`, label, tag, value, unit, warn, crit, min, max) - match, err = regexp.Match(regex, p.output(true)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } - - regex = fmt.Sprintf(`'%s_%s'=%g%s;%g;%g;%g;%g`, label, tag, value, unit, warn, crit, min, max) - match, err = regexp.Match(regex, p.output(false)) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output string did not match regex") - } - + regex = fmt.Sprintf(`'{"metric":"%s","label":"%s"}'=%g%s;%g;%g;%g;%g`, + label, tag, value, unit, warn, crit, min, max) + require.Contains(t, string(p.output(true)), regex, + "output string did not match regex") + + regex = fmt.Sprintf(`'%s_%s'=%g%s;%g;%g;%g;%g`, label, tag, value, unit, warn, + crit, min, max) + require.Contains(t, string(p.output(false)), regex, + "output string did not match regex") } func TestPerformanceData_add(t *testing.T) { - perfData := make(performanceData) + perfData := newPerformanceData() - //valid perfdata point - err := perfData.add(NewPerformanceDataPoint("metric", 10)) - if err != nil { - t.Error("adding a valid performance data point resulted in an error") - return - } + key := newPerformanceDataPointKey("metric", "") + assert.Nil(t, perfData.point(key)) - if _, ok := perfData[performanceDataPointKey{"metric", ""}]; !ok { - t.Error("performance data point was not added to the map of performance data points") - } + // valid perfdata point + require.NoError(t, perfData.add(NewPerformanceDataPoint("metric", 10)), + "adding a valid performance data point resulted in an error") - err = perfData.add(NewPerformanceDataPoint("metric", 10)) - if err == nil { - t.Error("there was no error when adding a performance data point with a metric, that already exists in performance data") - } + point := perfData.point(key) + require.NotNil(t, point, + "performance data point was not added to the map of performance data points") + assert.Equal(t, key, point.key()) - err = perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag1")) - if err != nil { - t.Error("adding a valid performance data point resulted in an error") - return - } + require.Error(t, perfData.add(NewPerformanceDataPoint("metric", 10)), + "there was no error when adding a performance data point with a metric, that already exists in performance data") - err = perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag2")) - if err != nil { - t.Error("adding a valid performance data point resulted in an error") - return - } + require.NoError(t, + perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag1")), + "adding a valid performance data point resulted in an error") - err = perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag1")) - if err == nil { - t.Error("there was no error when adding a performance data point with a metric and tag, that already exists in performance data") - } + require.NoError(t, + perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag2")), + "adding a valid performance data point resulted in an error") + + require.Error(t, + perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag1")), + "there was no error when adding a performance data point with a metric and tag, that already exists in performance data") } func TestResponse_SetPerformanceDataJsonLabel(t *testing.T) { - perfData := make(performanceData) + perfData := newPerformanceData() + + // valid perfdata point + require.NoError(t, perfData.add(NewPerformanceDataPoint("metric", 10)), + "adding a valid performance data point resulted in an error") + + key := newPerformanceDataPointKey("metric", "") + point := perfData.point(key) + require.Equal(t, key, point.key(), + "performance data point was not added to the map of performance data points") + + require.Error(t, perfData.add(NewPerformanceDataPoint("metric", 10)), + "there was no error when adding a performance data point with a metric, that already exists in performance data") +} - //valid perfdata point - err := perfData.add(NewPerformanceDataPoint("metric", 10)) - if err != nil { - t.Error("adding a valid performance data point resulted in an error") - return +func TestPerformanceData_keepOrder(t *testing.T) { + pointKeys := [...]performanceDataPointKey{ + {"metric", ""}, + {"metric", "tag1"}, + {"metric", "tag2"}, } - if _, ok := perfData[performanceDataPointKey{"metric", ""}]; !ok { - t.Error("performance data point was not added to the map of performance data points") + perfData := newPerformanceData() + wantKeys := make([]performanceDataPointKey, 0, len(pointKeys)) + for i := range pointKeys { + key := &pointKeys[i] + require.NoError(t, perfData.add( + NewPerformanceDataPoint(key.Metric, 10).SetLabel(key.Label))) + wantKeys = append(wantKeys, newPerformanceDataPointKey( + key.Metric, key.Label)) } - err = perfData.add(NewPerformanceDataPoint("metric", 10)) - if err == nil { - t.Error("there was no error when adding a performance data point with a metric, that already exists in performance data") + gotKeys := make([]performanceDataPointKey, 0, len(pointKeys)) + for _, p := range perfData.getInfo() { + gotKeys = append(gotKeys, p.key()) } + assert.Equal(t, wantKeys, gotKeys, "wrong order of data points") } diff --git a/response.go b/response.go index 41f7664..a811eea 100644 --- a/response.go +++ b/response.go @@ -1,54 +1,67 @@ -//Package monitoringplugin provides types for writing monitoring check plugins for nagios, icinga2, zabbix, etc +// Package monitoringplugin provides types for writing monitoring check plugins +// for nagios, icinga2, zabbix, etc. package monitoringplugin import ( "bytes" + "cmp" + "errors" "fmt" - "github.com/pkg/errors" "os" - "sort" + "slices" "strings" ) const ( - // OK check plugin status = OK - OK = 0 - // WARNING check plugin status = WARNING - WARNING = 1 - // CRITICAL check plugin status = CRITICAL - CRITICAL = 2 - // UNKNOWN check plugin status = UNKNOWN - UNKNOWN = 3 + OK = iota // OK check plugin status = OK + WARNING // WARNING check plugin status = WARNING + CRITICAL // CRITICAL check plugin status = CRITICAL + UNKNOWN // UNKNOWN check plugin status = UNKNOWN ) -// InvalidCharacterBehavior specifies how the monitoringplugin should behave if an invalid character is found in the -// output message. Does not affect invalid characters in the performance data. +// InvalidCharacterBehavior specifies how the monitoringplugin should behave if +// an invalid character is found in the output message. Does not affect invalid +// characters in the performance data. type InvalidCharacterBehavior int const ( // InvalidCharacterRemove removes invalid character in the output message. InvalidCharacterRemove InvalidCharacterBehavior = iota + 1 - // InvalidCharacterReplace replaces invalid character in the output message with another character. - // Only valid if replace character is set + // InvalidCharacterReplace replaces invalid character in the output message + // with another character. Only valid if replace character is set InvalidCharacterReplace - // InvalidCharacterRemoveMessage removes the message with the invalid character. - // StatusCode of the message will still be set. + // InvalidCharacterRemoveMessage removes the message with the invalid + // character. StatusCode of the message will still be set. InvalidCharacterRemoveMessage - // InvalidCharacterReplaceWithError replaces the whole message with an error message if an invalid character is found. + // InvalidCharacterReplaceWithError replaces the whole message with an error + // message if an invalid character is found. InvalidCharacterReplaceWithError - // InvalidCharacterReplaceWithErrorAndSetUNKNOWN replaces the whole message with an error message if an invalid character is found. - // Also sets the status code to UNKNOWN. + // InvalidCharacterReplaceWithErrorAndSetUNKNOWN replaces the whole message + // with an error message if an invalid character is found. Also sets the + // status code to UNKNOWN. InvalidCharacterReplaceWithErrorAndSetUNKNOWN ) -// OutputMessage represents a message of the response. It contains a message and a status code. -type OutputMessage struct { - Status int `yaml:"status" json:"status" xml:"status"` - Message string `yaml:"message" json:"message" xml:"message"` +// NewResponse creates a new Response and sets the default OK message to the +// given string. The default OK message will be displayed together with the +// other output messages, but only if the status is still OK when the check +// exits. +func NewResponse(defaultOkMessage string) *Response { + resp := &Response{ + statusCode: OK, + defaultOkMessage: defaultOkMessage, + performanceData: newPerformanceData(), + printPerformanceData: true, + sortOutputMessagesByStatus: true, + invalidCharacterBehaviour: InvalidCharacterRemove, + } + resp.OutputDelimiterMultiline() + return resp } // Response is the main type that is responsible for the check plugin Response. -// It stores the current status code, output messages, performance data and the output message delimiter. +// It stores the current status code, output messages, performance data and the +// output message delimiter. type Response struct { statusCode int defaultOkMessage string @@ -62,62 +75,47 @@ type Response struct { invalidCharacterReplaceChar string } -/* -NewResponse creates a new Response and sets the default OK message to the given string. -The default OK message will be displayed together with the other output messages, but only -if the status is still OK when the check exits. -*/ -func NewResponse(defaultOkMessage string) *Response { - response := &Response{ - statusCode: OK, - defaultOkMessage: defaultOkMessage, - outputDelimiter: "\n", - printPerformanceData: true, - sortOutputMessagesByStatus: true, - invalidCharacterBehaviour: InvalidCharacterRemove, - } - response.performanceData = make(performanceData) - return response +// OutputMessage represents a message of the response. It contains a message and +// a status code. +type OutputMessage struct { + Status int `yaml:"status" json:"status" xml:"status"` + Message string `yaml:"message" json:"message" xml:"message"` } -/* -AddPerformanceDataPoint adds a PerformanceDataPoint to the performanceData map, -using performanceData.add(*PerformanceDataPoint). -Usage: - err := Response.AddPerformanceDataPoint(NewPerformanceDataPoint("temperature", 32, "°C").SetWarn(35).SetCrit(40)) - if err != nil { - ... - } -*/ -func (r *Response) AddPerformanceDataPoint(point *PerformanceDataPoint) error { - err := r.performanceData.add(point) - if err != nil { - return errors.Wrap(err, "failed to add performance data point") - } +// WithDefaultOkMessage changes the default OK message, see [NewResponse]. +func (r *Response) WithDefaultOkMessage(defaultOkMessage string) *Response { + r.defaultOkMessage = defaultOkMessage + return r +} - if !point.Thresholds.IsEmpty() { - name := point.Metric - if point.Label != "" { - name += " (" + point.Label + ")" - } - err = r.CheckThresholds(point.Thresholds, point.Value, name) - if err != nil { - return errors.Wrap(err, "failed to check thresholds") - } +// AddPerformanceDataPoint adds a PerformanceDataPoint to the performanceData +// map, using performanceData.add(*PerformanceDataPoint). +// +// Usage: +// +// err := Response.AddPerformanceDataPoint(NewPerformanceDataPoint("temperature", 32, "°C").SetWarn(35).SetCrit(40)) +// if err != nil { +// ... +// } +func (r *Response) AddPerformanceDataPoint(point anyDataPoint) error { + if err := r.performanceData.add(point); err != nil { + return fmt.Errorf("failed to add performance data point: %w", err) + } + if point.HasThresholds() { + r.CheckThresholds(point) } - return nil } -/* -UpdateStatus updates the exit status of the Response and adds a statusMessage to the outputMessages that -will be displayed when the check exits. -See updateStatusCode(int) for a detailed description of the algorithm that is used to update the status code. -*/ +// UpdateStatus updates the exit status of the Response and adds a statusMessage +// to the outputMessages that will be displayed when the check exits. See +// updateStatusCode(int) for a detailed description of the algorithm that is +// used to update the status code. func (r *Response) UpdateStatus(statusCode int, statusMessage string) { r.updateStatusCode(statusCode) if statusMessage != "" { - r.outputMessages = append(r.outputMessages, OutputMessage{statusCode, statusMessage}) + r.outputMessages = append(r.outputMessages, + OutputMessage{statusCode, statusMessage}) } } @@ -131,10 +129,12 @@ func (r *Response) SetPerformanceDataJSONLabel(jsonLabel bool) { r.performanceDataJSONLabel = jsonLabel } -// SetInvalidCharacterBehavior sets the desired behavior if an invalid character is found in a message. -// Default is InvalidCharacterRemove. -// replaceCharacter is only necessary if InvalidCharacterReplace is set. -func (r *Response) SetInvalidCharacterBehavior(behavior InvalidCharacterBehavior, replaceCharacter string) error { +// SetInvalidCharacterBehavior sets the desired behavior if an invalid character +// is found in a message. Default is InvalidCharacterRemove. replaceCharacter is +// only necessary if InvalidCharacterReplace is set. +func (r *Response) SetInvalidCharacterBehavior( + behavior InvalidCharacterBehavior, replaceCharacter string, +) error { switch behavior { case InvalidCharacterReplace: if replaceCharacter == "" { @@ -150,29 +150,35 @@ func (r *Response) SetInvalidCharacterBehavior(behavior InvalidCharacterBehavior return nil } -/* -This function updates the statusCode of the Response. The status code is mapped to a state like this: -0 = OK -1 = WARNING -2 = CRITICAL -3 = UNKNOWN -Everything else is also mapped to UNKNOWN. - -UpdateStatus uses the following algorithm to update the exit status: -CRITICAL > UNKNOWN > WARNING > OK -Everything "left" from the current status code is seen as worse than the current one. -If the function wants to set a status code, it will only update it if the new status code is "left" of the current one. -Example: - //current status code = 1 - Response.updateStatusCode(0) //nothing changes - Response.updateStatusCode(2) //status code changes to CRITICAL (=2) - - //now current status code = 2 - Response.updateStatusCode(3) //nothing changes, because CRITICAL is worse than UNKNOWN - -*/ +// This function updates the statusCode of the Response. The status code is +// mapped to a state like this: +// +// 0 = OK +// 1 = WARNING +// 2 = CRITICAL +// 3 = UNKNOWN +// +// Everything else is also mapped to UNKNOWN. +// +// UpdateStatus uses the following algorithm to update the exit status: +// +// CRITICAL > UNKNOWN > WARNING > OK +// +// Everything "left" from the current status code is seen as worse than the +// current one. If the function wants to set a status code, it will only update +// it if the new status code is "left" of the current one. +// +// Example: +// +// //current status code = 1 +// Response.updateStatusCode(0) //nothing changes +// Response.updateStatusCode(2) //status code changes to CRITICAL (=2) +// +// //now current status code = 2 +// Response.updateStatusCode(3) //nothing changes, because CRITICAL is worse than UNKNOWN func (r *Response) updateStatusCode(statusCode int) { - if r.statusCode == CRITICAL { //critical is the worst status code; if its critical, do not change anything + if r.statusCode == CRITICAL { + // critical is the worst status code; if its critical, do not change anything return } if statusCode == CRITICAL { @@ -187,24 +193,33 @@ func (r *Response) updateStatusCode(statusCode int) { } } -// UpdateStatusIf calls UpdateStatus(statusCode, statusMessage) if the given condition is true. -func (r *Response) UpdateStatusIf(condition bool, statusCode int, statusMessage string) bool { +// UpdateStatusIf calls UpdateStatus(statusCode, statusMessage) if the given +// condition is true. +func (r *Response) UpdateStatusIf(condition bool, statusCode int, + statusMessage string, +) bool { if condition { r.UpdateStatus(statusCode, statusMessage) } return condition } -// UpdateStatusIfNot calls UpdateStatus(statusCode, statusMessage) if the given condition is false. -func (r *Response) UpdateStatusIfNot(condition bool, statusCode int, statusMessage string) bool { +// UpdateStatusIfNot calls UpdateStatus(statusCode, statusMessage) if the given +// condition is false. +func (r *Response) UpdateStatusIfNot(condition bool, statusCode int, + statusMessage string, +) bool { if !condition { r.UpdateStatus(statusCode, statusMessage) } return !condition } -// UpdateStatusOnError calls UpdateStatus(statusCode, statusMessage) if the given error is not nil. -func (r *Response) UpdateStatusOnError(err error, statusCode int, statusMessage string, includeErrorMessage bool) bool { +// UpdateStatusOnError calls UpdateStatus(statusCode, statusMessage) if the +// given error is not nil. +func (r *Response) UpdateStatusOnError(err error, statusCode int, + statusMessage string, includeErrorMessage bool, +) bool { x := err != nil if x { msg := statusMessage @@ -220,40 +235,43 @@ func (r *Response) UpdateStatusOnError(err error, statusCode int, statusMessage return x } -/* -SetOutputDelimiter is used to set the delimiter that is used to separate the outputMessages that will be displayed when -the check plugin exits. The default value is a linebreak (\n) -It can be set to any string. -Example: - Response.SetOutputDelimiter(" / ") - //this results in the output having the following format: - //OK: defaultOkMessage / outputMessage1 / outputMessage2 / outputMessage3 | performanceData -*/ +// SetOutputDelimiter is used to set the delimiter that is used to separate the +// outputMessages that will be displayed when the check plugin exits. The +// default value is a linebreak (\n). It can be set to any string. +// +// Example: +// +// Response.SetOutputDelimiter(" / ") +// //this results in the output having the following format: +// //OK: defaultOkMessage / outputMessage1 / outputMessage2 / outputMessage3 | performanceData func (r *Response) SetOutputDelimiter(delimiter string) { r.outputDelimiter = delimiter } -// OutputDelimiterMultiline sets the outputDelimiter to "\n". (See Response.SetOutputDelimiter(string)) +// OutputDelimiterMultiline sets the outputDelimiter to "\n". See +// [Response.SetOutputDelimiter] func (r *Response) OutputDelimiterMultiline() { r.SetOutputDelimiter("\n") } -// PrintPerformanceData activates or deactivates printing performance data +// PrintPerformanceData activates or deactivates printing performance data. func (r *Response) PrintPerformanceData(b bool) { r.printPerformanceData = b } -// SortOutputMessagesByStatus sorts the output messages according to their status. +// SortOutputMessagesByStatus sorts the output messages according to their +// status. func (r *Response) SortOutputMessagesByStatus(b bool) { r.sortOutputMessagesByStatus = b } -// This function returns the output that will be returned by the check plugin as a string. +// This function returns the output that will be returned by the check plugin as +// a string. func (r *Response) outputString() string { return string(r.output()) } -// This function returns the output that will be returned by the check plugin. +// This function returns the output that will be returned by the check plugin. func (r *Response) output() []byte { var buffer bytes.Buffer buffer.WriteString(StatusCode2Text(r.statusCode)) @@ -274,7 +292,7 @@ func (r *Response) output() []byte { if r.printPerformanceData { firstPoint := true - for _, perfDataPoint := range r.performanceData { + for _, perfDataPoint := range r.performanceData.getInfo() { if firstPoint { buffer.WriteString(" | ") firstPoint = false @@ -291,7 +309,8 @@ func (r *Response) validate() { if strings.Contains(r.defaultOkMessage, "|") { switch r.invalidCharacterBehaviour { case InvalidCharacterReplace: - r.defaultOkMessage = strings.ReplaceAll(r.defaultOkMessage, "|", r.invalidCharacterReplaceChar) + r.defaultOkMessage = strings.ReplaceAll(r.defaultOkMessage, "|", + r.invalidCharacterReplaceChar) case InvalidCharacterRemoveMessage: r.defaultOkMessage = "" case InvalidCharacterReplaceWithError: @@ -357,35 +376,38 @@ out: } func (r *Response) sortMessagesByStatus() { - sort.Slice(r.outputMessages, func(i, j int) bool { - if r.outputMessages[i].Status == CRITICAL { - return true + slices.SortStableFunc(r.outputMessages, func(a, b OutputMessage) int { + if a.Status == CRITICAL && b.Status != CRITICAL { + return -1 + } else if a.Status != CRITICAL && b.Status == CRITICAL { + return 1 } - return r.outputMessages[i].Status > r.outputMessages[j].Status + return cmp.Compare(b.Status, a.Status) }) } -/* -OutputAndExit generates the output string and prints it to stdout. -After that the check plugin exits with the current exit code. -Example: - Response := NewResponse("everything checked!") - defer Response.OutputAndExit() - - //check plugin logic... -*/ +// OutputAndExit generates the output string and prints it to stdout. After that +// the check plugin exits with the current exit code. +// +// Example: +// +// Response := NewResponse("everything checked!") +// defer Response.OutputAndExit() +// +// //check plugin logic... func (r *Response) OutputAndExit() { r.validate() fmt.Println(r.outputString()) os.Exit(r.statusCode) } -// ResponseInfo has all available information for a response. It also contains the RawOutput. +// ResponseInfo has all available information for a response. It also contains +// the RawOutput. type ResponseInfo struct { - StatusCode int `yaml:"status_code" json:"status_code" xml:"status_code"` - PerformanceData []PerformanceDataPoint `yaml:"performance_data" json:"performance_data" xml:"performance_data"` - RawOutput string `yaml:"raw_output" json:"raw_output" xml:"raw_output"` - Messages []OutputMessage `yaml:"messages" json:"messages" xml:"messages"` + StatusCode int `yaml:"status_code" json:"status_code" xml:"status_code"` + PerformanceData []anyDataPoint `yaml:"performance_data" json:"performance_data" xml:"performance_data"` + RawOutput string `yaml:"raw_output" json:"raw_output" xml:"raw_output"` + Messages []OutputMessage `yaml:"messages" json:"messages" xml:"messages"` } // GetInfo returns all information for a response. @@ -399,22 +421,17 @@ func (r *Response) GetInfo() ResponseInfo { } } -// CheckThresholds checks if the value exceeds the given thresholds and updates the response -func (r *Response) CheckThresholds(thresholds Thresholds, value interface{}, name string) error { - res, err := thresholds.CheckValue(value) - if err != nil { - return errors.Wrap(err, "failed to check value against threshold") - } - if res != OK { - r.UpdateStatus(res, name+" is outside of "+StatusCode2Text(res)+" threshold") +// CheckThresholds checks if the value exceeds the given thresholds and updates +// the response. +func (r *Response) CheckThresholds(point anyDataPoint) { + if res := point.CheckThresholds(); res != OK { + r.UpdateStatus(res, point.Name()+" is outside of "+StatusCode2Text(res)+ + " threshold") } - return nil } -/* -String2StatusCode returns the status code for a string. -OK -> 1, WARNING -> 2, CRITICAL -> 3, UNKNOWN and everything else -> 4 (case insensitive) -*/ +// String2StatusCode returns the status code for a string. +// OK -> 1, WARNING -> 2, CRITICAL -> 3, UNKNOWN and everything else -> 4 (case insensitive) func String2StatusCode(s string) int { switch { case strings.EqualFold("OK", s): diff --git a/response_test.go b/response_test.go index c30d386..ecfdba6 100644 --- a/response_test.go +++ b/response_test.go @@ -2,118 +2,103 @@ package monitoringplugin import ( "bytes" - "github.com/stretchr/testify/assert" "os" "os/exec" - "regexp" - "strconv" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestOKResponse(t *testing.T) { - defaultMessage := "OKTest" + const defaultMessage = "OKTest" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse(defaultMessage) r.OutputAndExit() } + cmd := exec.Command(os.Args[0], "-test.run=TestOKResponse") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=1") var outputB bytes.Buffer cmd.Stdout = &outputB - err := cmd.Run() - - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - t.Error("OkResponse is expected to return exit status 0, but exited with exit code " + strconv.Itoa(exitError.ExitCode())) - } else { - t.Error("cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: " + err.Error()) - } - return + if err := cmd.Run(); err != nil { + var exitError *exec.ExitError + require.ErrorAs(t, err, &exitError, + "cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: %s", + err) + require.Equal(t, 0, exitError.ExitCode(), + "OkResponse is expected to return exit status 0, but exited with exit code %v", + exitError.ExitCode()) } output := outputB.String() - match, err := regexp.MatchString("^OK: "+defaultMessage+"\n$", output) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("ok result output message did not match to the expected regex") - } - - return + assert.Equal(t, "OK: "+defaultMessage+"\n", output, + "ok result output message did not match to the expected regex") } func TestWARNINGResponse(t *testing.T) { failureResponse(t, 1) - return } func TestCRITICALResponse(t *testing.T) { failureResponse(t, 2) - return } func TestUNKNOWNResponse(t *testing.T) { failureResponse(t, 3) - return } func TestStatusHierarchy(t *testing.T) { r := NewResponse("") - if r.statusCode != OK { - t.Error("status code is supposed to be OK when a new Response is created") - } + require.Equal(t, OK, r.statusCode, + "status code is supposed to be OK when a new Response is created") r.UpdateStatus(WARNING, "") - if r.statusCode != WARNING { - t.Error("status code did not update from OK to WARNING after UpdateStatus(WARNING) is called!") - } + require.Equal(t, WARNING, r.statusCode, + "status code did not update from OK to WARNING after UpdateStatus(WARNING) is called!") r.UpdateStatus(OK, "") - if r.statusCode != WARNING { - t.Error("status code did change from WARNING to " + strconv.Itoa(r.statusCode) + " after UpdateStatus(OK) was called! The function should not affect the status code, because WARNING is worse than OK") - } + require.Equal(t, WARNING, r.statusCode, + "status code did change from WARNING to %v after UpdateStatus(OK) was called! The function should not affect the status code, because WARNING is worse than OK", + r.statusCode) r.UpdateStatus(CRITICAL, "") - if r.statusCode != CRITICAL { - t.Error("status code did not update from WARNING to CRITICAL after UpdateStatus(WARNING) is called!") - } + require.Equal(t, CRITICAL, r.statusCode, + "status code did not update from WARNING to CRITICAL after UpdateStatus(WARNING) is called!") r.UpdateStatus(OK, "") - if r.statusCode != CRITICAL { - t.Error("status code did change from CRITICAL to " + strconv.Itoa(r.statusCode) + " after UpdateStatus(OK) was called! The function should not affect the status code, because CRITICAL is worse than OK") - } + require.Equal(t, CRITICAL, r.statusCode, + "status code did change from CRITICAL to %v after UpdateStatus(OK) was called! The function should not affect the status code, because CRITICAL is worse than OK", + r.statusCode) r.UpdateStatus(WARNING, "") - if r.statusCode != CRITICAL { - t.Error("status code did change from CRITICAL to " + strconv.Itoa(r.statusCode) + " after UpdateStatus(WARNING) was called! The function should not affect the status code, because CRITICAL is worse than WARNING") - } + require.Equal(t, CRITICAL, r.statusCode, + "status code did change from CRITICAL to %v after UpdateStatus(WARNING) was called! The function should not affect the status code, because CRITICAL is worse than WARNING", + r.statusCode) r.UpdateStatus(UNKNOWN, "") - if r.statusCode != CRITICAL { - t.Error("status code did change from CRITICAL to " + strconv.Itoa(r.statusCode) + " after UpdateStatus(UNKNOWN) was called! The function should not affect the status code, because CRITICAL is worse than UNKNOWN") - } + require.Equal(t, CRITICAL, r.statusCode, + "status code did change from CRITICAL to %v after UpdateStatus(UNKNOWN) was called! The function should not affect the status code, because CRITICAL is worse than UNKNOWN", + r.statusCode) r = NewResponse("") r.UpdateStatus(UNKNOWN, "") - if r.statusCode != UNKNOWN { - t.Error("status code did not update from OK to UNKNOWN after UpdateStatus(UNKNOWN) is called!") - } + require.Equal(t, UNKNOWN, r.statusCode, + "status code did not update from OK to UNKNOWN after UpdateStatus(UNKNOWN) is called!") r.UpdateStatus(WARNING, "") - if r.statusCode != UNKNOWN { - t.Error("status code did change from UNKNOWN to " + strconv.Itoa(r.statusCode) + " after UpdateStatus(WARNING) was called! The function should not affect the status code, because UNKNOWN is worse than WARNING") - } + require.Equal(t, UNKNOWN, r.statusCode, + "status code did change from UNKNOWN to %v after UpdateStatus(WARNING) was called! The function should not affect the status code, because UNKNOWN is worse than WARNING", + r.statusCode) r.UpdateStatus(CRITICAL, "") - if r.statusCode != CRITICAL { - t.Error("status code is did not change from UNKNOWN to CRITICAL after UpdateStatus(CRITICAL) was called! The function should affect the status code, because CRITICAL is worse than UNKNOWN") - } + require.Equal(t, CRITICAL, r.statusCode, + "status code is did not change from UNKNOWN to CRITICAL after UpdateStatus(CRITICAL) was called! The function should affect the status code, because CRITICAL is worse than UNKNOWN") } func TestOutputMessages(t *testing.T) { - defaultMessage := "default" + const defaultMessage = "default" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse(defaultMessage) r.UpdateStatus(0, "message1") @@ -121,8 +106,8 @@ func TestOutputMessages(t *testing.T) { r.UpdateStatus(0, "message3") r.UpdateStatus(0, "message4") r.OutputAndExit() - return } + if os.Getenv("EXECUTE_PLUGIN") == "2" { r := NewResponse(defaultMessage) r.UpdateStatus(1, "message1") @@ -131,201 +116,177 @@ func TestOutputMessages(t *testing.T) { r.UpdateStatus(0, "message4") r.SetOutputDelimiter(" / ") r.OutputAndExit() - return } + cmd := exec.Command(os.Args[0], "-test.run=TestOutputMessages") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=1") var outputB bytes.Buffer cmd.Stdout = &outputB - err := cmd.Run() - - if err != nil { - t.Error("an error occurred during cmd.Run(), but the Response was expected to exit with exit code 0") - return - } + require.NoError(t, cmd.Run(), + "an error occurred during cmd.Run(), but the Response was expected to exit with exit code 0") output := outputB.String() - - match, err := regexp.MatchString("^OK: "+defaultMessage+"\nmessage1\nmessage2\nmessage3\nmessage4\n$", output) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error("output did not match to the expected regex") - } + require.Equal(t, + "OK: "+defaultMessage+"\nmessage1\nmessage2\nmessage3\nmessage4\n", + output, "output did not match to the expected regex") cmd = exec.Command(os.Args[0], "-test.run=TestOutputMessages") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=2") var outputB2 bytes.Buffer cmd.Stdout = &outputB2 - err = cmd.Run() - - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if exitError.ExitCode() != 1 { - t.Error("the command is expected to return exit status 1, but exited with exit code " + strconv.Itoa(exitError.ExitCode())) - } - } else { - t.Errorf("cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: " + err.Error()) - } - } else { - t.Error("the command exited with exitcode 0 but is expected to exit with exitcode 1") - } - output = outputB2.String() - match, err = regexp.MatchString("^WARNING: message1 / message2 / message3 / message4\n$", output) - if err != nil { - t.Error(err.Error()) - } + err := cmd.Run() + require.Error(t, err, + "the command exited with exitcode 0 but is expected to exit with exitcode 1") + var exitError *exec.ExitError + require.ErrorAs(t, err, &exitError, + "cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: %v", + err.Error()) + require.Equal(t, 1, exitError.ExitCode(), + "the command is expected to return exit status 1, but exited with exit code %v", + exitError.ExitCode()) - if !match { - t.Error("output did not match to the expected regex") - } + output = outputB2.String() + require.Equal(t, "WARNING: message1 / message2 / message3 / message4\n", + output, "output did not match to the expected regex") } func TestResponse_UpdateStatusIf(t *testing.T) { r := NewResponse("") r.UpdateStatusIf(false, 1, "") - assert.True(t, r.statusCode == 0) + assert.Equal(t, 0, r.statusCode) r.UpdateStatusIf(true, 1, "") - assert.True(t, r.statusCode == 1) + assert.Equal(t, 1, r.statusCode) } func TestResponse_UpdateStatusIfNot(t *testing.T) { r := NewResponse("") r.UpdateStatusIfNot(true, 1, "") - assert.True(t, r.statusCode == 0) + assert.Equal(t, 0, r.statusCode) r.UpdateStatusIfNot(false, 1, "") - assert.True(t, r.statusCode == 1) + assert.Equal(t, 1, r.statusCode) } func TestString2StatusCode(t *testing.T) { - assert.True(t, String2StatusCode("ok") == 0) - assert.True(t, String2StatusCode("OK") == 0) - assert.True(t, String2StatusCode("Ok") == 0) - assert.True(t, String2StatusCode("warning") == 1) - assert.True(t, String2StatusCode("WARNING") == 1) - assert.True(t, String2StatusCode("Warning") == 1) - assert.True(t, String2StatusCode("critical") == 2) - assert.True(t, String2StatusCode("CRITICAL") == 2) - assert.True(t, String2StatusCode("Critical") == 2) - assert.True(t, String2StatusCode("unknown") == 3) - assert.True(t, String2StatusCode("UNKNOWN") == 3) - assert.True(t, String2StatusCode("Unknown") == 3) + assert.Equal(t, 0, String2StatusCode("ok")) + assert.Equal(t, 0, String2StatusCode("OK")) + assert.Equal(t, 0, String2StatusCode("Ok")) + assert.Equal(t, 1, String2StatusCode("warning")) + assert.Equal(t, 1, String2StatusCode("WARNING")) + assert.Equal(t, 1, String2StatusCode("Warning")) + assert.Equal(t, 2, String2StatusCode("critical")) + assert.Equal(t, 2, String2StatusCode("CRITICAL")) + assert.Equal(t, 2, String2StatusCode("Critical")) + assert.Equal(t, 3, String2StatusCode("unknown")) + assert.Equal(t, 3, String2StatusCode("UNKNOWN")) + assert.Equal(t, 3, String2StatusCode("Unknown")) } func TestOutputPerformanceData(t *testing.T) { p1 := NewPerformanceDataPoint("label1", 10). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p1.NewThresholds(0, 80, 0, 90) + p2 := NewPerformanceDataPoint("label2", 20). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p2.NewThresholds(0, 80, 0, 90) + p3 := NewPerformanceDataPoint("label3", 30). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p3.NewThresholds(0, 80, 0, 90) - defaultMessage := "OKTest" + const defaultMessage = "OKTest" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse(defaultMessage) - err := r.AddPerformanceDataPoint(p1) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p1); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } - err = r.AddPerformanceDataPoint(p2) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p2); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } - err = r.AddPerformanceDataPoint(p3) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p3); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } r.OutputAndExit() } + cmd := exec.Command(os.Args[0], "-test.run=TestOutputPerformanceData") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=1") var outputB bytes.Buffer cmd.Stdout = &outputB - err := cmd.Run() - if err != nil { - t.Error("cmd.Run() returned an exitcode != 0, but exit code 0 was expected") - } + require.NoError(t, cmd.Run(), + "cmd.Run() returned an exitcode != 0, but exit code 0 was expected") output := outputB.String() - if !strings.HasPrefix(output, "OK: "+defaultMessage+" | ") { - t.Error("output did not match the expected regex") - } + require.True(t, strings.HasPrefix(output, "OK: "+defaultMessage+" | "), + "output did not match the expected regex") } func TestOutputPerformanceDataThresholdsExceeded(t *testing.T) { p1 := NewPerformanceDataPoint("label1", 10). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p1.NewThresholds(0, 80, 0, 90) + p2 := NewPerformanceDataPoint("label2", 20). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p2.NewThresholds(0, 80, 0, 90) + p3 := NewPerformanceDataPoint("label3", 85). SetUnit("%"). SetMin(0). - SetMax(100). - SetThresholds( - NewThresholds(0, 80, 0, 90)) + SetMax(100) + p3.NewThresholds(0, 80, 0, 90) - defaultMessage := "OKTest" + const defaultMessage = "OKTest" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse(defaultMessage) - err := r.AddPerformanceDataPoint(p1) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p1); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } - err = r.AddPerformanceDataPoint(p2) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p2); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } - err = r.AddPerformanceDataPoint(p3) - if err != nil { - r.UpdateStatus(3, "error during add performance data point") + if err := r.AddPerformanceDataPoint(p3); err != nil { + r.UpdateStatus(UNKNOWN, "error during add performance data point") } r.OutputAndExit() } + cmd := exec.Command(os.Args[0], "-test.run=TestOutputPerformanceDataThresholdsExceeded") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=1") var outputB bytes.Buffer cmd.Stdout = &outputB err := cmd.Run() - if err == nil { - t.Error("cmd.Run() returned an exitcode = 0, but exit code 1 was expected") - } else if err.Error() != "exit status 1" { - t.Error("cmd.Run() returned an exitcode != 1, but exit code 1 was expected") - } + require.Error(t, err, + "cmd.Run() returned an exitcode = 0, but exit code 1 was expected") + var exitError *exec.ExitError + require.ErrorAs(t, err, &exitError) + require.Equal(t, 1, exitError.ExitCode(), + "cmd.Run() returned an exitcode != 1, but exit code 1 was expected") output := outputB.String() - if !strings.HasPrefix(output, "WARNING: label3 is outside of WARNING threshold | ") { - t.Error("output did not match the expected regex") - } + require.True(t, + strings.HasPrefix(output, + "WARNING: label3 is outside of WARNING threshold | "), + "output did not match the expected regex") } func failureResponse(t *testing.T, exitCode int) { + require.NotEqual(t, 0, + "exitcode in failureResponse function cannot be 0, because it is not meant to be used for a successful cmd") + var status string switch exitCode { - case 0: - t.Error("exitcode in failureResponse function cannot be 0, because it is not meant to be used for a successful cmd") - return case 1: status = "WARNING" case 2: @@ -340,34 +301,28 @@ func failureResponse(t *testing.T, exitCode int) { r.UpdateStatus(exitCode, message) r.OutputAndExit() } + cmd := exec.Command(os.Args[0], "-test.run=Test"+status+"Response") cmd.Env = append(os.Environ(), "EXECUTE_PLUGIN=1") var outputB bytes.Buffer cmd.Stdout = &outputB err := cmd.Run() - - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if exitError.ExitCode() != exitCode { - t.Error(status + " Response is expected to return exit status " + strconv.Itoa(exitCode) + ", but exited with exit code " + strconv.Itoa(exitError.ExitCode())) - } - } else { - t.Errorf("cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: " + err.Error()) - } - } else { - t.Error("the command exited with exitcode 0 but is expected to exit with exitcode " + strconv.Itoa(exitCode)) - } + require.Error(t, err, + "the command exited with exitcode 0 but is expected to exit with exitcode %v", + exitCode) + + var exitError *exec.ExitError + require.ErrorAs(t, err, &exitError, + "cmd.Run() Command resulted in an error that can not be converted to exec.ExitEror! error: %s", + err.Error()) + require.Equal(t, exitCode, exitError.ExitCode(), + "%v Response is expected to return exit status %v, but exited with exit code %v", + status, exitCode, exitError.ExitCode()) output := outputB.String() - match, err := regexp.MatchString("^"+status+": "+message+"\n$", output) - if err != nil { - t.Error(err.Error()) - } - if !match { - t.Error(status + " result output message did not match to the expected regex") - } - return + require.Equal(t, status+": "+message+"\n", output, + "%s result output message did not match to the expected regex", status) } func TestResponse_SortOutputMessagesByStatus(t *testing.T) { @@ -380,94 +335,105 @@ func TestResponse_SortOutputMessagesByStatus(t *testing.T) { r.UpdateStatus(CRITICAL, "message6") r.UpdateStatus(UNKNOWN, "message7") r.UpdateStatus(OK, "message8") + r.validate() - for x, message := range r.outputMessages { - for _, m := range r.outputMessages[x:] { - assert.True(t, message.Status >= m.Status || message.Status == CRITICAL, "sorting did not work") - } - } + assert.Equal(t, []OutputMessage{ + {CRITICAL, "message4"}, + {CRITICAL, "message6"}, + {UNKNOWN, "message3"}, + {UNKNOWN, "message7"}, + {WARNING, "message2"}, + {WARNING, "message5"}, + {OK, "message1"}, + {OK, "message8"}, + }, r.outputMessages, "sorting did not work") } func TestResponse_InvalidCharacter(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "WARNING: test") + assert.Equal(t, "WARNING: test", r.GetInfo().RawOutput) } func TestResponse_InvalidCharacterReplace(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|2") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplace, "-") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterReplace, "-")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "OK: checked\ntest-2") + assert.Equal(t, "OK: checked\ntest-2", r.GetInfo().RawOutput) } func TestResponse_InvalidCharacterReplaceError(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplace, "") - assert.Error(t, err) + assert.Error(t, r.SetInvalidCharacterBehavior(InvalidCharacterReplace, "")) } func TestResponse_InvalidCharacterRemoveMessage(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|") - err := r.SetInvalidCharacterBehavior(InvalidCharacterRemoveMessage, "") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterRemoveMessage, "")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "OK: checked") + assert.Equal(t, "OK: checked", r.GetInfo().RawOutput) } func TestResponse_InvalidCharacterReplaceWithError(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithError, "") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterReplaceWithError, "")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "WARNING: output message contains invalid character") + assert.Equal(t, "WARNING: output message contains invalid character", + r.GetInfo().RawOutput) } -func TestResponse_InvalidCharacterReplaceWithErrorMultipleMessages(t *testing.T) { +func TestResponse_InvalidCharacterReplaceWithErrorMultipleMessages( + t *testing.T, +) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") r.UpdateStatus(WARNING, "test|2") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithError, "") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterReplaceWithError, "")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "WARNING: output message contains invalid character") + assert.Equal(t, "WARNING: output message contains invalid character", + r.GetInfo().RawOutput) } func TestResponse_InvalidCharacterReplaceWithErrorAndSetUnknown(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithErrorAndSetUNKNOWN, "") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterReplaceWithErrorAndSetUNKNOWN, "")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "UNKNOWN: output message contains invalid character") + assert.Equal(t, "UNKNOWN: output message contains invalid character", + r.GetInfo().RawOutput) } -func TestResponse_InvalidCharacterReplaceWithErrorAndSetUnknownMultipleMessages(t *testing.T) { +func TestResponse_InvalidCharacterReplaceWithErrorAndSetUnknownMultipleMessages( + t *testing.T, +) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") r.UpdateStatus(WARNING, "test|2") - err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithErrorAndSetUNKNOWN, "") - assert.NoError(t, err) + require.NoError(t, r.SetInvalidCharacterBehavior( + InvalidCharacterReplaceWithErrorAndSetUNKNOWN, "")) r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "UNKNOWN: output message contains invalid character") + assert.Equal(t, "UNKNOWN: output message contains invalid character", + r.GetInfo().RawOutput) } func TestResponse_InvalidCharacterDefaultMessage(t *testing.T) { r := NewResponse("test|") r.validate() - res := r.GetInfo() - assert.True(t, res.RawOutput == "OK: test") + assert.Equal(t, "OK: test", r.GetInfo().RawOutput) +} + +func TestResponse_WithDefaultOkMessage(t *testing.T) { + const msg2 = "message2" + r := NewResponse("default message1").WithDefaultOkMessage(msg2) + assert.Equal(t, msg2, r.defaultOkMessage) } diff --git a/thresholds.go b/thresholds.go index 71ded75..1483bf2 100644 --- a/thresholds.go +++ b/thresholds.go @@ -1,200 +1,129 @@ package monitoringplugin import ( + "cmp" + "errors" "fmt" - "github.com/pkg/errors" - "math/big" - "strconv" + "strings" ) -// Thresholds contains all threshold values -type Thresholds struct { - WarningMin interface{} `json:"warningMin" xml:"warningMin"` - WarningMax interface{} `json:"warningMax" xml:"warningMax"` - CriticalMin interface{} `json:"criticalMin" xml:"criticalMin"` - CriticalMax interface{} `json:"criticalMax" xml:"criticalMax"` +// Thresholds contains all threshold values. +type Thresholds[T cmp.Ordered] struct { + WarningMin T `json:"warningMin" xml:"warningMin"` + WarningMax T `json:"warningMax" xml:"warningMax"` + CriticalMin T `json:"criticalMin" xml:"criticalMin"` + CriticalMax T `json:"criticalMax" xml:"criticalMax"` + + hasWarnMin, hasWarnMax bool + hasCritMin, hasCritMax bool } -// NewThresholds creates a new threshold -func NewThresholds(warningMin, warningMax, criticalMin, criticalMax interface{}) Thresholds { - return Thresholds{ +// NewThresholds creates a new threshold. +func NewThresholds[T cmp.Ordered](warningMin, warningMax, criticalMin, + criticalMax T, +) Thresholds[T] { + return Thresholds[T]{ WarningMin: warningMin, WarningMax: warningMax, CriticalMin: criticalMin, CriticalMax: criticalMax, + + hasWarnMin: true, + hasWarnMax: true, + hasCritMin: true, + hasCritMax: true, } } -// Validate checks if the Thresholds contains some invalid combination of warning and critical values -func (c *Thresholds) Validate() error { - if c.WarningMin != nil && c.WarningMax != nil { - var min, max big.Float - _, _, err := min.Parse(fmt.Sprint(c.WarningMin), 10) - if err != nil { - return errors.Wrap(err, "can't parse warning min") - } - _, _, err = max.Parse(fmt.Sprint(c.WarningMax), 10) - if err != nil { - return errors.Wrap(err, "can't parse warning max") - } - - if res := min.Cmp(&max); res == 1 { - return errors.New("warning min and max are invalid") - } - } +// UseWarning configures how to use WarningMin and WarningMax. +func (c *Thresholds[T]) UseWarning(useMin, useMax bool) *Thresholds[T] { + c.hasWarnMin, c.hasWarnMax = useMin, useMax + return c +} - if c.CriticalMin != nil && c.CriticalMax != nil { - var min, max big.Float - _, _, err := min.Parse(fmt.Sprint(c.CriticalMin), 10) - if err != nil { - return errors.Wrap(err, "can't parse critical min") - } - _, _, err = max.Parse(fmt.Sprint(c.CriticalMax), 10) - if err != nil { - return errors.Wrap(err, "can't parse critical max") - } +// UseCritical configures how to use CriticalMin and CriticalMax. +func (c *Thresholds[T]) UseCritical(useMin, useMax bool) *Thresholds[T] { + c.hasCritMin, c.hasCritMax = useMin, useMax + return c +} - if res := min.Cmp(&max); res == 1 { - return errors.New("critical min and max are invalid") - } +// Validate checks if the Thresholds contains some invalid combination of +// warning and critical values. +func (c *Thresholds[T]) Validate() error { + if c.hasWarnMin && c.hasWarnMax && cmp.Compare(c.WarningMin, c.WarningMax) == 1 { + return errors.New("warning min and max are invalid") } - if c.CriticalMin != nil && c.WarningMin != nil { - var wMin, cMin big.Float - _, _, err := wMin.Parse(fmt.Sprint(c.WarningMin), 10) - if err != nil { - return errors.Wrap(err, "can't parse warning min") - } - _, _, err = cMin.Parse(fmt.Sprint(c.CriticalMin), 10) - if err != nil { - return errors.Wrap(err, "can't parse critical min") - } - - if res := cMin.Cmp(&wMin); res == 1 { - return errors.New("critical and warning min are invalid") - } + if c.hasCritMin && c.hasCritMax && cmp.Compare(c.CriticalMin, c.CriticalMax) == 1 { + return errors.New("critical min and max are invalid") } - if c.WarningMax != nil && c.CriticalMax != nil { - var wMax, cMax big.Float - _, _, err := wMax.Parse(fmt.Sprint(c.WarningMax), 10) - if err != nil { - return errors.Wrap(err, "can't parse warning min") - } - _, _, err = cMax.Parse(fmt.Sprint(c.CriticalMax), 10) - if err != nil { - return errors.Wrap(err, "can't parse critical min") - } - - if res := cMax.Cmp(&wMax); res == -1 { - return errors.New("critical and warning max are invalid") - } + if c.hasCritMin && c.hasWarnMin && cmp.Compare(c.CriticalMin, c.WarningMin) == 1 { + return errors.New("critical and warning min are invalid") } + if c.hasWarnMax && c.hasCritMax && cmp.Compare(c.CriticalMax, c.WarningMax) == -1 { + return errors.New("critical and warning max are invalid") + } return nil } -// HasWarning checks if a warning threshold is set -func (c *Thresholds) HasWarning() bool { - return c.WarningMax != nil || c.WarningMin != nil +// HasWarning checks if a warning threshold is set. +func (c *Thresholds[T]) HasWarning() bool { + return c.hasWarnMax || c.hasWarnMin } -// HasCritical checks if a critical threshold is set -func (c *Thresholds) HasCritical() bool { - return c.CriticalMax != nil || c.CriticalMin != nil +// HasCritical checks if a critical threshold is set. +func (c *Thresholds[T]) HasCritical() bool { + return c.hasCritMax || c.hasCritMin } -// IsEmpty checks if the thresholds are empty -func (c *Thresholds) IsEmpty() bool { - return c.WarningMin == nil && c.WarningMax == nil && c.CriticalMin == nil && c.CriticalMax == nil +// IsEmpty checks if the thresholds are empty. +func (c *Thresholds[T]) IsEmpty() bool { + return !c.HasWarning() && !c.HasCritical() } -// CheckValue checks if the input is violating the thresholds -func (c *Thresholds) CheckValue(v interface{}) (int, error) { - var value, wMin, wMax, cMin, cMax big.Float - _, _, err := value.Parse(fmt.Sprint(v), 10) - if err != nil { - return 0, errors.Wrap(err, "value can't be parsed") - } - if c.CriticalMin != nil { - _, _, err := cMin.Parse(fmt.Sprint(c.CriticalMin), 10) - if err != nil { - return 0, errors.Wrap(err, "critical min can't be parsed") - } - if cMin.Cmp(&value) == 1 { - return CRITICAL, nil - } - } - if c.CriticalMax != nil { - _, _, err := cMax.Parse(fmt.Sprint(c.CriticalMax), 10) - if err != nil { - return 0, errors.Wrap(err, "critical max can't be parsed") - } - if cMax.Cmp(&value) == -1 { - return CRITICAL, nil - } - } - if c.WarningMin != nil { - _, _, err := wMin.Parse(fmt.Sprint(c.WarningMin), 10) - if err != nil { - return 0, errors.Wrap(err, "warning min can't be parsed") - } - if wMin.Cmp(&value) == 1 { - return WARNING, nil - } - } - if c.WarningMax != nil { - _, _, err := wMax.Parse(fmt.Sprint(c.WarningMax), 10) - if err != nil { - return 0, errors.Wrap(err, "warning max can't be parsed") - } - if wMax.Cmp(&value) == -1 { - return WARNING, nil - } +// CheckValue checks if the input is violating the thresholds. +func (c *Thresholds[T]) CheckValue(value T) int { + switch { + case c.hasCritMin && cmp.Compare(c.CriticalMin, value) == 1: + return CRITICAL + case c.hasCritMax && cmp.Compare(c.CriticalMax, value) == -1: + return CRITICAL + case c.hasWarnMin && cmp.Compare(c.WarningMin, value) == 1: + return WARNING + case c.hasWarnMax && cmp.Compare(c.WarningMax, value) == -1: + return WARNING } - return OK, nil + return OK } -func (c *Thresholds) getWarning() string { - return getRange(c.WarningMin, c.WarningMax) +func (c *Thresholds[T]) getWarning() string { + return getRange(c.WarningMin, c.WarningMax, c.hasWarnMin, c.hasWarnMax) } -func (c *Thresholds) getCritical() string { - return getRange(c.CriticalMin, c.CriticalMax) +func (c *Thresholds[T]) getCritical() string { + return getRange(c.CriticalMin, c.CriticalMax, c.hasCritMin, c.hasCritMax) } -func getRange(min, max interface{}) string { - if min == nil && max == nil { +func getRange[T cmp.Ordered](min, max T, hasMin, hasMax bool) string { + if !hasMin && !hasMax { return "" } - var res string - - if min != nil { - var minString string - switch m := min.(type) { - case float64: - minString = strconv.FormatFloat(m, 'f', -1, 64) - default: - minString = fmt.Sprint(m) - } - if minString != "0" || max == nil { - res += minString + ":" + var b strings.Builder + if hasMin { + minString := fmt.Sprint(min) + if minString != "0" || !hasMax { + b.WriteString(minString) + b.WriteString(":") } } else { - res += "~:" + b.WriteString("~:") } - if max != nil { - var maxString string - switch m := max.(type) { - case float64: - maxString = strconv.FormatFloat(m, 'f', -1, 64) - default: - maxString = fmt.Sprint(m) - } - res += maxString + if hasMax { + b.WriteString(fmt.Sprint(max)) } - - return res + return b.String() } diff --git a/thresholds_test.go b/thresholds_test.go index 08266ad..cffe0c8 100644 --- a/thresholds_test.go +++ b/thresholds_test.go @@ -1,112 +1,53 @@ package monitoringplugin import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestValidateThresholds(t *testing.T) { - th1 := Thresholds{ - WarningMin: 5, - WarningMax: 10, - CriticalMin: 3, - CriticalMax: 12, - } - assert.NoError(t, th1.Validate()) + th1 := NewThresholds(5, 10, 3, 12) + require.NoError(t, th1.Validate()) - th2 := Thresholds{ - WarningMin: 0, - WarningMax: 10, - CriticalMin: 0, - CriticalMax: 12, - } - assert.NoError(t, th2.Validate()) + th2 := NewThresholds(0, 10, 0, 12) + require.NoError(t, th2.Validate()) - th3 := Thresholds{} - assert.NoError(t, th3.Validate()) + th3 := Thresholds[int]{} + require.NoError(t, th3.Validate()) - th4 := Thresholds{ - WarningMax: 3, - } - assert.NoError(t, th4.Validate()) + th4 := NewThresholds(0, 3, 0, 0) + require.NoError(t, + th4.UseWarning(false, true).UseCritical(false, false).Validate()) - th5 := Thresholds{ - WarningMin: 2, - WarningMax: 1, - } - assert.Error(t, th5.Validate()) + th5 := NewThresholds(2, 1, 0, 0) + require.Error(t, th5.UseCritical(false, false).Validate()) - th6 := Thresholds{ - CriticalMin: 2, - CriticalMax: 1, - } - assert.Error(t, th6.Validate()) + th6 := NewThresholds(0, 0, 2, 1) + require.Error(t, th6.UseWarning(false, false).Validate()) - th7 := Thresholds{ - WarningMin: 1, - CriticalMin: 2, - } - assert.Error(t, th7.Validate()) + th7 := NewThresholds(1, 0, 2, 0) + require.Error(t, + th7.UseWarning(true, false).UseCritical(true, false).Validate()) - th8 := Thresholds{ - WarningMax: 2, - CriticalMax: 1, - } - assert.Error(t, th8.Validate()) + th8 := NewThresholds(0, 2, 0, 1) + require.Error(t, + th8.UseWarning(false, true).UseCritical(false, true).Validate()) } func TestCheckThresholds(t *testing.T) { - th1 := Thresholds{ - WarningMin: 5, - WarningMax: 10, - CriticalMin: 3, - CriticalMax: 12, - } - - res, err := th1.CheckValue(6) - assert.NoError(t, err) - assert.Equal(t, OK, res) - - res, err = th1.CheckValue(5) - assert.NoError(t, err) - assert.Equal(t, OK, res) - - res, err = th1.CheckValue(10) - assert.NoError(t, err) - assert.Equal(t, OK, res) - - res, err = th1.CheckValue(4) - assert.NoError(t, err) - assert.Equal(t, WARNING, res) - - res, err = th1.CheckValue(11) - assert.NoError(t, err) - assert.Equal(t, WARNING, res) - - res, err = th1.CheckValue(3) - assert.NoError(t, err) - assert.Equal(t, WARNING, res) - - res, err = th1.CheckValue(12) - assert.NoError(t, err) - assert.Equal(t, WARNING, res) - - res, err = th1.CheckValue(2) - assert.NoError(t, err) - assert.Equal(t, CRITICAL, res) - - res, err = th1.CheckValue(13) - assert.NoError(t, err) - assert.Equal(t, CRITICAL, res) - - th2 := Thresholds{ - WarningMin: 5, - WarningMax: 10, - CriticalMin: 5, - CriticalMax: 12, - } - - res, err = th2.CheckValue(4) - assert.NoError(t, err) - assert.Equal(t, CRITICAL, res) + th1 := NewThresholds(5, 10, 3, 12) + assert.Equal(t, OK, th1.CheckValue(6)) + assert.Equal(t, OK, th1.CheckValue(5)) + assert.Equal(t, OK, th1.CheckValue(10)) + assert.Equal(t, WARNING, th1.CheckValue(4)) + assert.Equal(t, WARNING, th1.CheckValue(11)) + assert.Equal(t, WARNING, th1.CheckValue(3)) + assert.Equal(t, WARNING, th1.CheckValue(12)) + assert.Equal(t, CRITICAL, th1.CheckValue(2)) + assert.Equal(t, CRITICAL, th1.CheckValue(13)) + + th2 := NewThresholds(5, 10, 5, 12) + assert.Equal(t, CRITICAL, th2.CheckValue(4)) }