Skip to content

Commit

Permalink
Merge pull request #178 from infraweavers/perfdata-alerting
Browse files Browse the repository at this point in the history
Warning and Critical Threshold Handling
  • Loading branch information
atc0005 authored Jan 26, 2023
2 parents a0f849b + 2acced0 commit 77ed0c2
Show file tree
Hide file tree
Showing 46 changed files with 19,787 additions and 1 deletion.
11 changes: 10 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@ module github.com/atc0005/go-nagios

go 1.19

require github.com/google/go-cmp v0.5.9
require (
github.com/google/go-cmp v0.5.9
github.com/stretchr/testify v1.8.1
)

require (
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
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/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=
147 changes: 147 additions & 0 deletions nagios.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"fmt"
"io"
"os"
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -167,6 +169,122 @@ 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 {
Expand Down Expand Up @@ -455,6 +573,35 @@ 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...)
Expand Down
192 changes: 192 additions & 0 deletions nagios_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package nagios

import (
"testing"

"github.com/stretchr/testify/assert"
)

// Best documentation for this is: https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT

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)
assert.Equal(t, parsedThing.CheckRange("54"), true)
assert.Equal(t, parsedThing.CheckRange("-1"), true)
})

t.Run("Test N to infinity or alert", func(t *testing.T) {
parsedThing := ParseRangeString("10:")
assert.Equal(t, parsedThing.Start, 10.0)
assert.Equal(t, parsedThing.EndInfinity, true)
assert.Equal(t, parsedThing.CheckRange("10"), false)
assert.Equal(t, parsedThing.CheckRange("9"), true)
assert.Equal(t, parsedThing.CheckRange("-1"), true)
assert.Equal(t, parsedThing.CheckRange("11"), false)
})

t.Run("Within a range involving -inf", func(t *testing.T) {
parsedThing := ParseRangeString("~:30")
assert.Equal(t, parsedThing.StartInfinity, true)
assert.Equal(t, parsedThing.End, 30.0)
assert.Equal(t, parsedThing.CheckRange("5"), false)
assert.Equal(t, parsedThing.CheckRange("-10"), false)
assert.Equal(t, parsedThing.CheckRange("-100"), false)
assert.Equal(t, parsedThing.CheckRange("30"), false)
assert.Equal(t, parsedThing.CheckRange("31"), true)
})

t.Run("Outside a defined range", func(t *testing.T) {
parsedThing := ParseRangeString("5:33")
assert.Equal(t, parsedThing.Start, 5.0)
assert.Equal(t, parsedThing.End, 33.0)
assert.Equal(t, parsedThing.CheckRange("33"), false)
assert.Equal(t, parsedThing.CheckRange("34"), true)
assert.Equal(t, parsedThing.CheckRange("4"), true)
assert.Equal(t, parsedThing.CheckRange("5"), false)

})

t.Run("Within a defined range", func(t *testing.T) {
parsedThing := ParseRangeString("10:200")
assert.Equal(t, parsedThing.Start, 10.0)
assert.Equal(t, parsedThing.End, 200.0)
assert.Equal(t, parsedThing.CheckRange("54"), false)
assert.Equal(t, parsedThing.CheckRange("10"), false)
assert.Equal(t, parsedThing.CheckRange("9"), true)
assert.Equal(t, parsedThing.CheckRange("200"), false)
assert.Equal(t, parsedThing.CheckRange("201"), true)
})

t.Run("InsideRange", func(t *testing.T) {
parsedThing := ParseRangeString("@32:64")
assert.Equal(t, parsedThing.CheckRange("32"), true)
assert.Equal(t, parsedThing.CheckRange("33"), true)
assert.Equal(t, parsedThing.CheckRange("64"), true)
assert.Equal(t, parsedThing.CheckRange("63"), true)
assert.Equal(t, parsedThing.CheckRange("31"), false)
assert.Equal(t, parsedThing.CheckRange("65"), false)
})

t.Run("If invalid range is provided (with positive infinity) parsing should return nil", func(t *testing.T) {
parsedThing := ParseRangeString("50:~")
assert.Nil(t, parsedThing)
})

t.Run("Alert in 0-32", func(t *testing.T) {
parsedThing := ParseRangeString("@32")

assert.Equal(t, parsedThing.CheckRange("32"), true)
assert.Equal(t, parsedThing.CheckRange("31"), true)
assert.Equal(t, parsedThing.CheckRange("0"), true)
assert.Equal(t, parsedThing.CheckRange("33"), false)
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")

assert.Equal(t, parsedThing.CheckRange("32"), true)
assert.Equal(t, parsedThing.CheckRange("31"), false)
assert.Equal(t, parsedThing.CheckRange("0"), false)
assert.Equal(t, parsedThing.CheckRange("33"), false)
assert.Equal(t, parsedThing.CheckRange("-32"), false)
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) {
var plugin = Plugin{
ExitStatusCode: StateOKExitCode,
}
plugin.ServiceOutput = "CHECK-NT-REPLACEMENT"

perfdata := PerformanceData{
Label: "perfdata label",
Value: "18.0",
UnitOfMeasurement: "C",
Warn: "5:30",
Crit: "0:40",
}
plugin.AddPerfData(false, perfdata)
plugin.EvaluateThreshold(perfdata)

assert.Equal(t, StateOKExitCode, plugin.ExitStatusCode)
})

t.Run("Plugin should return exit code WARNING when value is within warning range", func(t *testing.T) {
var plugin = Plugin{
ExitStatusCode: StateOKExitCode,
}
plugin.ServiceOutput = "CHECK-NT-REPLACEMENT"

perfdata := PerformanceData{
Label: "perfdata label",
Value: "31.0",
UnitOfMeasurement: "C",
Warn: "5:30",
Crit: "0:40",
}
plugin.AddPerfData(false, perfdata)
plugin.EvaluateThreshold(perfdata)

assert.Equal(t, StateWARNINGExitCode, plugin.ExitStatusCode)
})

t.Run("Plugin should return exit code WARNING when value is within warning range", func(t *testing.T) {
var plugin = Plugin{
ExitStatusCode: StateOKExitCode,
}
plugin.ServiceOutput = "CHECK-NT-REPLACEMENT"

perfdata := PerformanceData{
Label: "perfdata label",
Value: "4.0",
UnitOfMeasurement: "C",
Warn: "5:30",
Crit: "0:40",
}
plugin.AddPerfData(false, perfdata)
plugin.EvaluateThreshold(perfdata)

assert.Equal(t, StateWARNINGExitCode, plugin.ExitStatusCode)
})

t.Run("Plugin should return exit code CRITICAL when value is within warning range", func(t *testing.T) {
var plugin = Plugin{
ExitStatusCode: StateOKExitCode,
}
plugin.ServiceOutput = "CHECK-NT-REPLACEMENT"

perfdata := PerformanceData{
Label: "perfdata label",
Value: "41.0",
UnitOfMeasurement: "C",
Warn: "5:30",
Crit: "0:40",
}
plugin.AddPerfData(false, perfdata)
plugin.EvaluateThreshold(perfdata)

assert.Equal(t, StateCRITICALExitCode, plugin.ExitStatusCode)
})

t.Run("Plugin should return exit code CRITICAL when value is within warning range", func(t *testing.T) {
var plugin = Plugin{
ExitStatusCode: StateOKExitCode,
}
plugin.ServiceOutput = "CHECK-NT-REPLACEMENT"

perfdata := PerformanceData{
Label: "perfdata label",
Value: "-1.0",
UnitOfMeasurement: "C",
Warn: "5:30",
Crit: "0:40",
}
plugin.AddPerfData(false, perfdata)
plugin.EvaluateThreshold(perfdata)

assert.Equal(t, StateCRITICALExitCode, plugin.ExitStatusCode)
})
}
Loading

0 comments on commit 77ed0c2

Please sign in to comment.