pax_global_header00006660000000000000000000000064141521150060014505gustar00rootroot0000000000000052 comment=acfe1d74d8b1362243b8dc39076bde2f40434669 go-monitoringplugin-1.0.13/000077500000000000000000000000001415211500600155765ustar00rootroot00000000000000go-monitoringplugin-1.0.13/.github/000077500000000000000000000000001415211500600171365ustar00rootroot00000000000000go-monitoringplugin-1.0.13/.github/workflows/000077500000000000000000000000001415211500600211735ustar00rootroot00000000000000go-monitoringplugin-1.0.13/.github/workflows/build.yml000066400000000000000000000010631415211500600230150ustar00rootroot00000000000000name: 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 . go-monitoringplugin-1.0.13/.github/workflows/lint.yml000066400000000000000000000014701415211500600226660ustar00rootroot00000000000000name: 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 }} go-monitoringplugin-1.0.13/.github/workflows/test.yml000066400000000000000000000011311415211500600226710ustar00rootroot00000000000000name: 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 testgo-monitoringplugin-1.0.13/LICENSE000066400000000000000000000024441415211500600166070ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2019, inexio All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-monitoringplugin-1.0.13/README.md000066400000000000000000000044021415211500600170550ustar00rootroot00000000000000# 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. 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-monitoringplugin-1.0.13/go.mod000066400000000000000000000002051415211500600167010ustar00rootroot00000000000000module github.com/inexio/go-monitoringplugin go 1.14 require ( github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.6.1 ) go-monitoringplugin-1.0.13/go.sum000066400000000000000000000022411415211500600167300ustar00rootroot00000000000000github.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/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= 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= go-monitoringplugin-1.0.13/performance_data.go000066400000000000000000000156771415211500600214370ustar00rootroot00000000000000package monitoringplugin import ( "bytes" "encoding/json" "fmt" "github.com/pkg/errors" "math/big" "regexp" "strconv" ) type performanceDataPointKey struct { Metric string `json:"metric"` Label string `json:"label,omitempty"` } // 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 { if err := point.Validate(); err != nil { return errors.Wrap(err, "given performance data point is not valid") } 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)) } (*p)[key] = *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) } return info } // 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"` } /* 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 { 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 { 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 { 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 { 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.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.Min != nil && p.Max != nil { switch min.Cmp(&max) { case 1: return errors.New("min cannot be larger than max") default: } } if !p.Thresholds.IsEmpty() { err = p.Thresholds.Validate() if err != nil { return errors.Wrap(err, "thresholds are invalid") } } 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, } } // SetUnit sets the unit of the performance data point func (p *PerformanceDataPoint) SetUnit(unit string) *PerformanceDataPoint { p.Unit = unit return p } // SetMin sets minimum value. func (p *PerformanceDataPoint) SetMin(min interface{}) *PerformanceDataPoint { p.Min = min return p } // SetMax sets maximum value. func (p *PerformanceDataPoint) SetMax(max interface{}) *PerformanceDataPoint { p.Max = max 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 { p.Label = label return p } // SetThresholds sets the thresholds for the performance data point func (p *PerformanceDataPoint) SetThresholds(thresholds Thresholds) *PerformanceDataPoint { 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 { var buffer bytes.Buffer if jsonLabel { buffer.WriteByte('\'') key := performanceDataPointKey{ Metric: p.Metric, Label: p.Label, } jsonKey, _ := json.Marshal(key) buffer.Write(jsonKey) buffer.WriteByte('\'') } else { buffer.WriteByte('\'') buffer.WriteString(p.Metric) if p.Label != "" { buffer.WriteByte('_') buffer.WriteString(p.Label) } buffer.WriteByte('\'') } 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(p.Unit) if !p.Thresholds.IsEmpty() || p.Max != nil || p.Min != nil { buffer.WriteByte(';') if p.Thresholds.HasWarning() { buffer.WriteString(p.Thresholds.getWarning()) } buffer.WriteByte(';') if p.Thresholds.HasCritical() { 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)) } } 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)) } } } return buffer.Bytes() } go-monitoringplugin-1.0.13/performance_data_test.go000066400000000000000000000177361415211500600224740ustar00rootroot00000000000000package monitoringplugin import ( "fmt" "regexp" "testing" ) func TestPerformanceDataPointCreation(t *testing.T) { metric := "testMetric" var value float64 = 10 p := NewPerformanceDataPoint(metric, value) if p.Metric != metric || p.Value != value { t.Error("the created PerfomanceDataPoint NewPerformanceDataPoint") } unit := "%" p.SetUnit(unit) if p.Unit != unit { t.Error("SetUnit failed") } label := "testLabel" p.SetLabel(label) if p.Label != label { t.Error("SetLabel failed") } var min float64 p.SetMin(min) if p.Min != min || p.Min == nil { t.Error("SetMin failed") } var max float64 = 100 p.SetMax(max) if p.Max != max || p.Max == nil { t.Error("SetMax failed") } thresholds := Thresholds{ WarningMin: 0, WarningMax: 10, CriticalMin: 0, CriticalMax: 20, } p.SetThresholds(thresholds) if p.Thresholds != thresholds { t.Error("SetThresholds failed") } return } 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()) } //empty metric p = NewPerformanceDataPoint("", 10) if err := p.Validate(); err == nil { t.Error("invalid performance data did not return an error (case: empty 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) if err := p.Validate(); err == nil { t.Error("invalid performance data did not return an error (case: invalid metric, contains single quotes)") } //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)") } 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)") } 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)") } 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)") } //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)") } //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)") } //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)") } } 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 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") } 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") } 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)) 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") } 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.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.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(`'{"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") } 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") } } func TestPerformanceData_add(t *testing.T) { perfData := make(performanceData) //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 } if _, ok := perfData[performanceDataPointKey{"metric", ""}]; !ok { t.Error("performance data point was not added to the map of performance data points") } 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") } err = perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag1")) if err != nil { t.Error("adding a valid performance data point resulted in an error") return } err = perfData.add(NewPerformanceDataPoint("metric", 10).SetLabel("tag2")) if err != nil { t.Error("adding a valid performance data point resulted in an error") return } 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") } } func TestResponse_SetPerformanceDataJsonLabel(t *testing.T) { perfData := make(performanceData) //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 } if _, ok := perfData[performanceDataPointKey{"metric", ""}]; !ok { t.Error("performance data point was not added to the map of performance data points") } 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") } } go-monitoringplugin-1.0.13/response.go000066400000000000000000000333761415211500600177770ustar00rootroot00000000000000//Package monitoringplugin provides types for writing monitoring check plugins for nagios, icinga2, zabbix, etc package monitoringplugin import ( "bytes" "fmt" "github.com/pkg/errors" "os" "sort" "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 ) // 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 // 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 // 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"` } // 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. type Response struct { statusCode int defaultOkMessage string outputMessages []OutputMessage performanceData performanceData outputDelimiter string performanceDataJSONLabel bool printPerformanceData bool sortOutputMessagesByStatus bool invalidCharacterBehaviour InvalidCharacterBehavior 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 } /* 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") } 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") } } 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. */ func (r *Response) UpdateStatus(statusCode int, statusMessage string) { r.updateStatusCode(statusCode) if statusMessage != "" { r.outputMessages = append(r.outputMessages, OutputMessage{statusCode, statusMessage}) } } // GetStatusCode returns the current status code. func (r *Response) GetStatusCode() int { return r.statusCode } // SetPerformanceDataJSONLabel updates the JSON metric. 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 { switch behavior { case InvalidCharacterReplace: if replaceCharacter == "" { return errors.New("empty replace character set") } r.invalidCharacterReplaceChar = replaceCharacter fallthrough case InvalidCharacterRemove, InvalidCharacterRemoveMessage, InvalidCharacterReplaceWithError, InvalidCharacterReplaceWithErrorAndSetUNKNOWN: r.invalidCharacterBehaviour = behavior default: return errors.New("unknown behavior") } 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 */ func (r *Response) updateStatusCode(statusCode int) { if r.statusCode == CRITICAL { //critical is the worst status code; if its critical, do not change anything return } if statusCode == CRITICAL { r.statusCode = statusCode return } if statusCode < OK || statusCode > UNKNOWN { statusCode = UNKNOWN } if statusCode > r.statusCode { r.statusCode = statusCode } } // 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 { 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 { x := err != nil if x { msg := statusMessage if includeErrorMessage { if msg != "" { msg = fmt.Sprintf("%s (error: %s)", msg, err) } else { msg = err.Error() } } r.UpdateStatus(statusCode, msg) } 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 */ func (r *Response) SetOutputDelimiter(delimiter string) { r.outputDelimiter = delimiter } // OutputDelimiterMultiline sets the outputDelimiter to "\n". (See Response.SetOutputDelimiter(string)) func (r *Response) OutputDelimiterMultiline() { r.SetOutputDelimiter("\n") } // 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. 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. func (r *Response) outputString() string { return string(r.output()) } // 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)) buffer.WriteString(": ") if r.statusCode == OK { buffer.WriteString(r.defaultOkMessage) if len(r.outputMessages) > 0 { buffer.WriteString(r.outputDelimiter) } } for c, x := range r.outputMessages { if c != 0 { buffer.WriteString(r.outputDelimiter) } buffer.WriteString(x.Message) } if r.printPerformanceData { firstPoint := true for _, perfDataPoint := range r.performanceData { if firstPoint { buffer.WriteString(" | ") firstPoint = false } else { buffer.WriteByte(' ') } buffer.Write(perfDataPoint.output(r.performanceDataJSONLabel)) } } return buffer.Bytes() } func (r *Response) validate() { if strings.Contains(r.defaultOkMessage, "|") { switch r.invalidCharacterBehaviour { case InvalidCharacterReplace: r.defaultOkMessage = strings.ReplaceAll(r.defaultOkMessage, "|", r.invalidCharacterReplaceChar) case InvalidCharacterRemoveMessage: r.defaultOkMessage = "" case InvalidCharacterReplaceWithError: r.defaultOkMessage = "default output message contains invalid character" case InvalidCharacterReplaceWithErrorAndSetUNKNOWN: r.statusCode = UNKNOWN r.outputMessages = []OutputMessage{{ Status: UNKNOWN, Message: "default output message contains invalid character", }} r.outputMessages = nil return default: // InvalidCharacterRemove r.defaultOkMessage = strings.ReplaceAll(r.defaultOkMessage, "|", "") } } r.validateMessages() if r.sortOutputMessagesByStatus { r.sortMessagesByStatus() } } func (r *Response) validateMessages() { var messages []OutputMessage out: for _, message := range r.outputMessages { if !strings.Contains(message.Message, "|") { messages = append(messages, message) } else { switch r.invalidCharacterBehaviour { case InvalidCharacterReplace: newMessage := strings.ReplaceAll(message.Message, "|", r.invalidCharacterReplaceChar) if newMessage != "" { messages = append(messages, OutputMessage{ Status: message.Status, Message: newMessage, }) } case InvalidCharacterRemoveMessage: // done case InvalidCharacterReplaceWithErrorAndSetUNKNOWN: r.statusCode = UNKNOWN message.Status = UNKNOWN fallthrough case InvalidCharacterReplaceWithError: messages = []OutputMessage{{ Status: message.Status, Message: "output message contains invalid character", }} break out default: // InvalidCharacterRemove newMessage := strings.ReplaceAll(message.Message, "|", "") if newMessage != "" { messages = append(messages, OutputMessage{ Status: message.Status, Message: newMessage, }) } } } } r.outputMessages = messages } func (r *Response) sortMessagesByStatus() { sort.Slice(r.outputMessages, func(i, j int) bool { if r.outputMessages[i].Status == CRITICAL { return true } return r.outputMessages[i].Status > r.outputMessages[j].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... */ 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. 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"` } // GetInfo returns all information for a response. func (r *Response) GetInfo() ResponseInfo { r.validate() return ResponseInfo{ RawOutput: r.outputString(), StatusCode: r.statusCode, PerformanceData: r.performanceData.getInfo(), Messages: r.outputMessages, } } // 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") } return nil } /* 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): return OK case strings.EqualFold("WARNING", s): return WARNING case strings.EqualFold("CRITICAL", s): return CRITICAL default: return UNKNOWN } } // StatusCode2Text is used to map the status code to a string. func StatusCode2Text(statusCode int) string { switch { case statusCode == OK: return "OK" case statusCode == WARNING: return "WARNING" case statusCode == CRITICAL: return "CRITICAL" default: return "UNKNOWN" } } go-monitoringplugin-1.0.13/response_test.go000066400000000000000000000342721415211500600210320ustar00rootroot00000000000000package monitoringplugin import ( "bytes" "github.com/stretchr/testify/assert" "os" "os/exec" "regexp" "strconv" "strings" "testing" ) func TestOKResponse(t *testing.T) { 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 } 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 } 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") } r.UpdateStatus(WARNING, "") if r.statusCode != WARNING { t.Error("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") } r.UpdateStatus(CRITICAL, "") if r.statusCode != CRITICAL { t.Error("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") } 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") } 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") } 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!") } 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") } 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") } } func TestOutputMessages(t *testing.T) { defaultMessage := "default" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse(defaultMessage) r.UpdateStatus(0, "message1") r.UpdateStatus(0, "message2") r.UpdateStatus(0, "message3") r.UpdateStatus(0, "message4") r.OutputAndExit() return } if os.Getenv("EXECUTE_PLUGIN") == "2" { r := NewResponse(defaultMessage) r.UpdateStatus(1, "message1") r.UpdateStatus(0, "message2") r.UpdateStatus(0, "message3") 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 } 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") } 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()) } if !match { t.Error("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) r.UpdateStatusIf(true, 1, "") assert.True(t, r.statusCode == 1) } func TestResponse_UpdateStatusIfNot(t *testing.T) { r := NewResponse("") r.UpdateStatusIfNot(true, 1, "") assert.True(t, r.statusCode == 0) r.UpdateStatusIfNot(false, 1, "") assert.True(t, r.statusCode == 1) } 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) } func TestOutputPerformanceData(t *testing.T) { p1 := NewPerformanceDataPoint("label1", 10). SetUnit("%"). SetMin(0). SetMax(100). SetThresholds( NewThresholds(0, 80, 0, 90)) p2 := NewPerformanceDataPoint("label2", 20). SetUnit("%"). SetMin(0). SetMax(100). SetThresholds( NewThresholds(0, 80, 0, 90)) p3 := NewPerformanceDataPoint("label3", 30). SetUnit("%"). SetMin(0). SetMax(100). SetThresholds( NewThresholds(0, 80, 0, 90)) 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") } err = r.AddPerformanceDataPoint(p2) if err != nil { r.UpdateStatus(3, "error during add performance data point") } err = r.AddPerformanceDataPoint(p3) if err != nil { r.UpdateStatus(3, "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") } output := outputB.String() if !strings.HasPrefix(output, "OK: "+defaultMessage+" | ") { t.Error("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)) p2 := NewPerformanceDataPoint("label2", 20). SetUnit("%"). SetMin(0). SetMax(100). SetThresholds( NewThresholds(0, 80, 0, 90)) p3 := NewPerformanceDataPoint("label3", 85). SetUnit("%"). SetMin(0). SetMax(100). SetThresholds( NewThresholds(0, 80, 0, 90)) 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") } err = r.AddPerformanceDataPoint(p2) if err != nil { r.UpdateStatus(3, "error during add performance data point") } err = r.AddPerformanceDataPoint(p3) if err != nil { r.UpdateStatus(3, "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") } output := outputB.String() if !strings.HasPrefix(output, "WARNING: label3 is outside of WARNING threshold | ") { t.Error("output did not match the expected regex") } } func failureResponse(t *testing.T, exitCode int) { 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: status = "CRITICAL" default: status = "UNKNOWN" } message := status + "Test" if os.Getenv("EXECUTE_PLUGIN") == "1" { r := NewResponse("") 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)) } 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 } func TestResponse_SortOutputMessagesByStatus(t *testing.T) { r := NewResponse("defaultMessage") r.UpdateStatus(OK, "message1") r.UpdateStatus(WARNING, "message2") r.UpdateStatus(UNKNOWN, "message3") r.UpdateStatus(CRITICAL, "message4") r.UpdateStatus(WARNING, "message5") 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") } } } 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") } func TestResponse_InvalidCharacterReplace(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|2") err := r.SetInvalidCharacterBehavior(InvalidCharacterReplace, "-") assert.NoError(t, err) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "OK: checked\ntest-2") } func TestResponse_InvalidCharacterReplaceError(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|") err := r.SetInvalidCharacterBehavior(InvalidCharacterReplace, "") assert.Error(t, err) } func TestResponse_InvalidCharacterRemoveMessage(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(OK, "test|") err := r.SetInvalidCharacterBehavior(InvalidCharacterRemoveMessage, "") assert.NoError(t, err) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "OK: checked") } func TestResponse_InvalidCharacterReplaceWithError(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithError, "") assert.NoError(t, err) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "WARNING: output message contains invalid character") } 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) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "WARNING: output message contains invalid character") } func TestResponse_InvalidCharacterReplaceWithErrorAndSetUnknown(t *testing.T) { r := NewResponse("checked") r.UpdateStatus(WARNING, "test|") err := r.SetInvalidCharacterBehavior(InvalidCharacterReplaceWithErrorAndSetUNKNOWN, "") assert.NoError(t, err) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "UNKNOWN: output message contains invalid character") } 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) r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "UNKNOWN: output message contains invalid character") } func TestResponse_InvalidCharacterDefaultMessage(t *testing.T) { r := NewResponse("test|") r.validate() res := r.GetInfo() assert.True(t, res.RawOutput == "OK: test") } go-monitoringplugin-1.0.13/thresholds.go000066400000000000000000000117701415211500600203120ustar00rootroot00000000000000package monitoringplugin import ( "fmt" "github.com/pkg/errors" "math/big" "strconv" ) // 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"` } // NewThresholds creates a new threshold func NewThresholds(warningMin, warningMax, criticalMin, criticalMax interface{}) Thresholds { return Thresholds{ WarningMin: warningMin, WarningMax: warningMax, CriticalMin: criticalMin, CriticalMax: criticalMax, } } // 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") } } 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") } if res := min.Cmp(&max); res == 1 { return errors.New("critical 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.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") } } return nil } // HasWarning checks if a warning threshold is set func (c *Thresholds) HasWarning() bool { return c.WarningMax != nil || c.WarningMin != nil } // HasCritical checks if a critical threshold is set func (c *Thresholds) HasCritical() bool { return c.CriticalMax != nil || c.CriticalMin != nil } // 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 } // 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 } } return OK, nil } func (c *Thresholds) getWarning() string { return getRange(c.WarningMin, c.WarningMax) } func (c *Thresholds) getCritical() string { return getRange(c.CriticalMin, c.CriticalMax) } func getRange(min, max interface{}) string { if min == nil && max == nil { 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 + ":" } } else { res += "~:" } 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 } return res } go-monitoringplugin-1.0.13/thresholds_test.go000066400000000000000000000037111415211500600213450ustar00rootroot00000000000000package monitoringplugin import ( "github.com/stretchr/testify/assert" "testing" ) func TestValidateThresholds(t *testing.T) { th1 := Thresholds{ WarningMin: 5, WarningMax: 10, CriticalMin: 3, CriticalMax: 12, } assert.NoError(t, th1.Validate()) th2 := Thresholds{ WarningMin: 0, WarningMax: 10, CriticalMin: 0, CriticalMax: 12, } assert.NoError(t, th2.Validate()) th3 := Thresholds{} assert.NoError(t, th3.Validate()) th4 := Thresholds{ WarningMax: 3, } assert.NoError(t, th4.Validate()) th5 := Thresholds{ WarningMin: 2, WarningMax: 1, } assert.Error(t, th5.Validate()) th6 := Thresholds{ CriticalMin: 2, CriticalMax: 1, } assert.Error(t, th6.Validate()) th7 := Thresholds{ WarningMin: 1, CriticalMin: 2, } assert.Error(t, th7.Validate()) th8 := Thresholds{ WarningMax: 2, CriticalMax: 1, } assert.Error(t, th8.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) }