From 9a45c919d76df3cee63d19c8e68687121e292d29 Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Thu, 26 Jan 2023 06:43:36 -0600 Subject: [PATCH] Refactor newly added Range support Minor changes applied as a follow-up to PR #178: - concentrate new Range functionality in dedicated files - this reflects similar work underway for GH-175 - update README - mention new Range support - link to dependent projects (including one from contributor of new functionality) - doc comment tweaks - misc changes to satisfy current CI linters Credit for original work: - @infraweavers (https://github.com/infraweavers) - https://github.com/atc0005/go-nagios/pull/178 refs GH-176 refs GH-178 --- README.md | 22 ++++- nagios.go | 147 --------------------------- range.go | 170 ++++++++++++++++++++++++++++++++ nagios_test.go => range_test.go | 53 ++++++---- 4 files changed, 224 insertions(+), 168 deletions(-) create mode 100644 range.go rename nagios_test.go => range_test.go (79%) diff --git a/README.md b/README.md index ba4a19d..c6b307b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Shared Golang package for Nagios plugins - [Changelog](#changelog) - [Examples](#examples) - [License](#license) +- [Used by](#used-by) - [References](#references) ## Status @@ -47,9 +48,11 @@ various plugins and help reduce typos associated with literal strings. - simple label and exit code "wrapper" - useful in client code as a way to map internal check results to a Nagios service state value -- Supports "branding" callback function to display application name, - version, or other information as a "trailer" for check results provided to - Nagios +- Support for evaluating a given performance data value and setting the final + plugin state exit code +- Support for using a "branding" callback function to display application + name, version, or other information as a "trailer" for check results + provided to Nagios - this could be useful for identifying what version of a plugin determined the service or host state to be an issue - Panics from client code are captured and reported @@ -139,6 +142,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## Used by + +See the Known importers lists below for a dynamically updated list of projects +using either this library or the original project. + +- [Known importers (pkg.go.dev)](https://pkg.go.dev/github.com/atc0005/go-nagios?tab=importedby) +- + ## References - Nagios @@ -159,3 +170,8 @@ SOFTWARE. - - Panics, stack traces - + +See also the [Used by](#used-by) section for projects known to be using this +package. Please +[report](https://github.com/atc0005/go-nagios/issues/new/choose) any +additional projects that we've missed! diff --git a/nagios.go b/nagios.go index 2ce60dd..c1d1784 100644 --- a/nagios.go +++ b/nagios.go @@ -12,9 +12,7 @@ import ( "fmt" "io" "os" - "regexp" "runtime/debug" - "strconv" "strings" "time" ) @@ -169,122 +167,6 @@ type PerformanceData struct { Max string } -// Range represents the thresholds that the user can pass in for warning -// and critical, this format is defined here: -// https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT -type Range struct { - StartInfinity bool - EndInfinity bool - AlertOn string - Start float64 - End float64 -} - -// CheckRange returns Returns true if an alert should be raised, -// otherwise false -func (r Range) CheckRange(value string) bool { - - valueAsAFloat, _ := strconv.ParseFloat(value, 64) - isOutsideRange := r.checkOutsideRange(valueAsAFloat) - if r.AlertOn == "INSIDE" { - return !isOutsideRange - } - return isOutsideRange -} - -// checkOutsideRange returns in the inverse of CheckRange -// it is used to handle the inverting logic of "inside" vs -// "outside" ranges in a cleanish way -func (r Range) checkOutsideRange(valueAsAFloat float64) bool { - - if r.EndInfinity == false && r.StartInfinity == false { - if r.Start <= valueAsAFloat && valueAsAFloat <= r.End { - return false - } else { - return true - } - } else if r.StartInfinity == false && r.EndInfinity == true { - if valueAsAFloat >= r.Start { - return false - } else { - return true - } - } else if r.StartInfinity == true && r.EndInfinity == false { - if valueAsAFloat <= r.End { - return false - } else { - return true - } - } else { - return false - } -} - -// ParseRangeString static method to construct a Range object -// from the string representation based on the definition here: -// https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT -func ParseRangeString(input string) *Range { - - r := Range{} - - digitOrInfinity := regexp.MustCompile(`[\d~]`) - optionalInvertAndRange := regexp.MustCompile(`^\@?((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?|~)?(:((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?)?)?$`) - firstHalfOfRange := regexp.MustCompile(`^((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?)?:`) - endOfRange := regexp.MustCompile(`^(?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?$`) - - r.Start = 0 - r.StartInfinity = false - r.End = 0 - r.EndInfinity = false - r.AlertOn = "OUTSIDE" - - valid := true - - if !(digitOrInfinity.MatchString(input) && optionalInvertAndRange.MatchString(input)) { //not match regex - return nil - } - - // invert the range, i.e. @10:20 means ≥ 10 and ≤ 20, (inside the range of {10 .. 20} inclusive) - if strings.HasPrefix(input, "@") { - r.AlertOn = "INSIDE" - input = input[1:] - } - // ~ represents infinity - if strings.HasPrefix(input, "~") { - r.StartInfinity = true - input = input[1:] - } - - // 10: - rangeComponents := firstHalfOfRange.FindAllStringSubmatch(input, -1) - if rangeComponents != nil { - if rangeComponents[0][1] != "" { - r.Start, _ = strconv.ParseFloat(rangeComponents[0][1], 64) - r.StartInfinity = false - } - - r.EndInfinity = true - input = strings.TrimPrefix(input, rangeComponents[0][0]) - valid = true - } - - // x:10 or 10 - endOfRangeComponents := endOfRange.FindAllStringSubmatch(input, -1) - if endOfRangeComponents != nil { - - r.End, _ = strconv.ParseFloat(endOfRangeComponents[0][0], 64) - r.EndInfinity = false - valid = true - } - - if valid && (r.StartInfinity || r.EndInfinity || r.Start <= r.End) { - return &r - } else { - return nil - } - -} - // Validate performs basic validation of PerformanceData. An error is returned // for any validation failures. func (pd PerformanceData) Validate() error { @@ -573,35 +455,6 @@ func (p *Plugin) AddPerfData(skipValidate bool, perfData ...PerformanceData) err return nil } -// EvaluateThreshold causes the performance data to be checked against -// the Warn and Crit provided and set the ExitStatusCode of the plugin -// as is appropriate -func (p *Plugin) EvaluateThreshold(perfData ...PerformanceData) error { - for i := range perfData { - - if perfData[i].Crit != "" { - - CriticalThresholdObject := ParseRangeString(perfData[i].Crit) - - if CriticalThresholdObject.CheckRange(perfData[i].Value) { - p.ExitStatusCode = StateCRITICALExitCode - return nil - } - } - - if perfData[i].Warn != "" { - warningThresholdObject := ParseRangeString(perfData[i].Warn) - - if warningThresholdObject.CheckRange(perfData[i].Value) { - p.ExitStatusCode = StateWARNINGExitCode - return nil - } - } - } - - return nil -} - // AddError appends provided errors to the collection. func (p *Plugin) AddError(err ...error) { p.Errors = append(p.Errors, err...) diff --git a/range.go b/range.go new file mode 100644 index 0000000..845fb49 --- /dev/null +++ b/range.go @@ -0,0 +1,170 @@ +// Copyright 2023 Codeweavers Ltd +// Copyright 2023 Adam Chalkley +// +// https://github.com/atc0005/go-nagios +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package nagios + +import ( + "regexp" + "strconv" + "strings" +) + +// Range represents the thresholds that the user can pass in for warning and +// critical, this format is based on the [Nagios Plugin Dev Guidelines: +// Threshold and Ranges] definition. +// +// [Nagios Plugin Dev Guidelines: Threshold and Ranges]: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +type Range struct { + StartInfinity bool + EndInfinity bool + AlertOn string + Start float64 + End float64 +} + +// CheckRange returns true if an alert should be raised for a given +// performance data Value, otherwise false. +func (r Range) CheckRange(value string) bool { + valueAsAFloat, _ := strconv.ParseFloat(value, 64) + isOutsideRange := r.checkOutsideRange(valueAsAFloat) + if r.AlertOn == "INSIDE" { + return !isOutsideRange + } + return isOutsideRange +} + +// checkOutsideRange returns in the inverse of CheckRange. It is used to +// handle the inverting logic of "inside" vs "outside" ranges. +// +// See the [Nagios Plugin Dev Guidelines: Threshold and Ranges] definition for +// additional details. +// +// [Nagios Plugin Dev Guidelines: Threshold and Ranges]: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +func (r Range) checkOutsideRange(valueAsAFloat float64) bool { + switch { + case !r.EndInfinity && !r.StartInfinity: + if r.Start <= valueAsAFloat && valueAsAFloat <= r.End { + return false + } + return true + + case !r.StartInfinity && r.EndInfinity: + if valueAsAFloat >= r.Start { + return false + } + return true + + case r.StartInfinity && !r.EndInfinity: + if valueAsAFloat <= r.End { + return false + } + return true + + default: + return false + } +} + +// ParseRangeString static method to construct a Range object from the string +// representation based on the [Nagios Plugin Dev Guidelines: Threshold and +// Ranges] definition. +// +// [Nagios Plugin Dev Guidelines: Threshold and Ranges]: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +func ParseRangeString(input string) *Range { + r := Range{} + + digitOrInfinity := regexp.MustCompile(`[\d~]`) + optionalInvertAndRange := regexp.MustCompile(`^\@?((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?|~)?(:((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?)?)?$`) + firstHalfOfRange := regexp.MustCompile(`^((?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?)?:`) + endOfRange := regexp.MustCompile(`^(?:[-+]?[\d\.]+)(?:e(?:[-+]?[\d\.]+))?$`) + + r.Start = 0 + r.StartInfinity = false + r.End = 0 + r.EndInfinity = false + r.AlertOn = "OUTSIDE" + + valid := true + + // If regex does not match ... + if !(digitOrInfinity.MatchString(input) && optionalInvertAndRange.MatchString(input)) { + return nil + } + + // Invert the range. + // + // i.e. @10:20 means ≥ 10 and ≤ 20 (inside the range of {10 .. 20} + // inclusive) + if strings.HasPrefix(input, "@") { + r.AlertOn = "INSIDE" + input = input[1:] + } + + // ~ represents infinity + if strings.HasPrefix(input, "~") { + r.StartInfinity = true + input = input[1:] + } + + // 10: + rangeComponents := firstHalfOfRange.FindAllStringSubmatch(input, -1) + if rangeComponents != nil { + if rangeComponents[0][1] != "" { + r.Start, _ = strconv.ParseFloat(rangeComponents[0][1], 64) + r.StartInfinity = false + } + + r.EndInfinity = true + input = strings.TrimPrefix(input, rangeComponents[0][0]) + valid = true + } + + // x:10 or 10 + endOfRangeComponents := endOfRange.FindAllStringSubmatch(input, -1) + if endOfRangeComponents != nil { + + r.End, _ = strconv.ParseFloat(endOfRangeComponents[0][0], 64) + r.EndInfinity = false + valid = true + } + + if valid && (r.StartInfinity || r.EndInfinity || r.Start <= r.End) { + return &r + } + + return nil +} + +// EvaluateThreshold causes the performance data to be checked against the +// Warn and Crit thresholds provided by client code and sets the +// ExitStatusCode of the plugin as appropriate. +func (p *Plugin) EvaluateThreshold(perfData ...PerformanceData) error { + for i := range perfData { + + if perfData[i].Crit != "" { + + CriticalThresholdObject := ParseRangeString(perfData[i].Crit) + + if CriticalThresholdObject.CheckRange(perfData[i].Value) { + p.ExitStatusCode = StateCRITICALExitCode + return nil + } + } + + if perfData[i].Warn != "" { + warningThresholdObject := ParseRangeString(perfData[i].Warn) + + if warningThresholdObject.CheckRange(perfData[i].Value) { + p.ExitStatusCode = StateWARNINGExitCode + return nil + } + } + } + + return nil +} diff --git a/nagios_test.go b/range_test.go similarity index 79% rename from nagios_test.go rename to range_test.go index 96a93f8..a824bc3 100644 --- a/nagios_test.go +++ b/range_test.go @@ -1,3 +1,11 @@ +// Copyright 2023 Codeweavers Ltd +// Copyright 2023 Adam Chalkley +// +// https://github.com/atc0005/go-nagios +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + package nagios import ( @@ -6,9 +14,17 @@ import ( "github.com/stretchr/testify/assert" ) -// Best documentation for this is: https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +// TestParseRange asserts that the Threshold and Range parsing and evaluation +// functionality works as expected. +// +// See the [Nagios Plugin Dev Guidelines: Threshold and Ranges] definition for +// additional details. +// +// [Nagios Plugin Dev Guidelines: Threshold and Ranges]: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +func TestParseRange(t *testing.T) { + + const pluginServiceOutput string = "CHECK-NT-REPLACEMENT" -func TestThis(t *testing.T) { t.Run("Test 0 to N or alert", func(t *testing.T) { parsedThing := ParseRangeString("10") assert.Equal(t, parsedThing.End, 10.0) @@ -84,6 +100,7 @@ func TestThis(t *testing.T) { assert.Equal(t, parsedThing.CheckRange("-32"), false) assert.Equal(t, parsedThing.CheckRange("-1"), false) }) + t.Run("Alert on value 32", func(t *testing.T) { parsedThing := ParseRangeString("@32:32") @@ -95,11 +112,11 @@ func TestThis(t *testing.T) { assert.Equal(t, parsedThing.CheckRange("-1"), false) }) - t.Run("Plugin should return exit code OK when value is within accetptable range", func(t *testing.T) { + t.Run("Plugin should return exit code OK when value is within acceptable range", func(t *testing.T) { var plugin = Plugin{ ExitStatusCode: StateOKExitCode, } - plugin.ServiceOutput = "CHECK-NT-REPLACEMENT" + plugin.ServiceOutput = pluginServiceOutput perfdata := PerformanceData{ Label: "perfdata label", @@ -108,8 +125,8 @@ func TestThis(t *testing.T) { Warn: "5:30", Crit: "0:40", } - plugin.AddPerfData(false, perfdata) - plugin.EvaluateThreshold(perfdata) + assert.NoError(t, plugin.AddPerfData(false, perfdata)) + assert.NoError(t, plugin.EvaluateThreshold(perfdata)) assert.Equal(t, StateOKExitCode, plugin.ExitStatusCode) }) @@ -118,7 +135,7 @@ func TestThis(t *testing.T) { var plugin = Plugin{ ExitStatusCode: StateOKExitCode, } - plugin.ServiceOutput = "CHECK-NT-REPLACEMENT" + plugin.ServiceOutput = pluginServiceOutput perfdata := PerformanceData{ Label: "perfdata label", @@ -127,8 +144,8 @@ func TestThis(t *testing.T) { Warn: "5:30", Crit: "0:40", } - plugin.AddPerfData(false, perfdata) - plugin.EvaluateThreshold(perfdata) + assert.NoError(t, plugin.AddPerfData(false, perfdata)) + assert.NoError(t, plugin.EvaluateThreshold(perfdata)) assert.Equal(t, StateWARNINGExitCode, plugin.ExitStatusCode) }) @@ -137,7 +154,7 @@ func TestThis(t *testing.T) { var plugin = Plugin{ ExitStatusCode: StateOKExitCode, } - plugin.ServiceOutput = "CHECK-NT-REPLACEMENT" + plugin.ServiceOutput = pluginServiceOutput perfdata := PerformanceData{ Label: "perfdata label", @@ -146,8 +163,8 @@ func TestThis(t *testing.T) { Warn: "5:30", Crit: "0:40", } - plugin.AddPerfData(false, perfdata) - plugin.EvaluateThreshold(perfdata) + assert.NoError(t, plugin.AddPerfData(false, perfdata)) + assert.NoError(t, plugin.EvaluateThreshold(perfdata)) assert.Equal(t, StateWARNINGExitCode, plugin.ExitStatusCode) }) @@ -156,7 +173,7 @@ func TestThis(t *testing.T) { var plugin = Plugin{ ExitStatusCode: StateOKExitCode, } - plugin.ServiceOutput = "CHECK-NT-REPLACEMENT" + plugin.ServiceOutput = pluginServiceOutput perfdata := PerformanceData{ Label: "perfdata label", @@ -165,8 +182,8 @@ func TestThis(t *testing.T) { Warn: "5:30", Crit: "0:40", } - plugin.AddPerfData(false, perfdata) - plugin.EvaluateThreshold(perfdata) + assert.NoError(t, plugin.AddPerfData(false, perfdata)) + assert.NoError(t, plugin.EvaluateThreshold(perfdata)) assert.Equal(t, StateCRITICALExitCode, plugin.ExitStatusCode) }) @@ -175,7 +192,7 @@ func TestThis(t *testing.T) { var plugin = Plugin{ ExitStatusCode: StateOKExitCode, } - plugin.ServiceOutput = "CHECK-NT-REPLACEMENT" + plugin.ServiceOutput = pluginServiceOutput perfdata := PerformanceData{ Label: "perfdata label", @@ -184,8 +201,8 @@ func TestThis(t *testing.T) { Warn: "5:30", Crit: "0:40", } - plugin.AddPerfData(false, perfdata) - plugin.EvaluateThreshold(perfdata) + assert.NoError(t, plugin.AddPerfData(false, perfdata)) + assert.NoError(t, plugin.EvaluateThreshold(perfdata)) assert.Equal(t, StateCRITICALExitCode, plugin.ExitStatusCode) })