diff --git a/check.go b/check.go index 1b6f348..db93136 100644 --- a/check.go +++ b/check.go @@ -19,14 +19,46 @@ func Exit(status Status, message string) { // Represents the state of a Nagios check. type Check struct { - results []Result - perfdata []PerfDatum - status Status + results []Result + perfdata []PerfDatum + status Status + statusPolicy *statusPolicy +} + +// CheckOptions contains knobs that modify default Check behaviour. See +// NewCheckWithOptions(). +type CheckOptions struct { + + // StatusPolicy defines the relative severity of different check + // results by status value. + // + // A Nagios plugin must ultimately report a single status to its + // parent process (OK, CRITICAL, etc.). nagiosplugin allows plugin + // developers to batch multiple check results in a single plugin + // invocation. The most severe result will be reflected in the + // plugin's final exit status. By default, results are prioritised + // by the numeric 'plugin return codes' defined by the Nagios Plugin + // Development Guidelines. Results with CRITICAL status will take + // precedence over WARNING, WARNING over OK, and UNKNOWN over all + // other results. This ordering may be tailored with a custom + // policy. See NewStatusPolicy(). + StatusPolicy *statusPolicy } // NewCheck returns an empty Check object. func NewCheck() *Check { c := new(Check) + c.statusPolicy = NewDefaultStatusPolicy() + return c +} + +// NewCheckWithOptions returns an empty Check object with +// caller-specified behavioural modifications. See CheckOptions. +func NewCheckWithOptions(options CheckOptions) *Check { + c := NewCheck() + if options.StatusPolicy != nil { + c.statusPolicy = options.StatusPolicy + } return c } @@ -38,7 +70,8 @@ func (c *Check) AddResult(status Status, message string) { result.status = status result.message = message c.results = append(c.results, result) - if result.status > c.status { + + if (*c.statusPolicy)[result.status] > (*c.statusPolicy)[c.status] { c.status = result.status } } diff --git a/check_test.go b/check_test.go index debfe2c..c3baa68 100644 --- a/check_test.go +++ b/check_test.go @@ -3,6 +3,7 @@ package nagiosplugin import ( "fmt" "math/rand" + "strings" "testing" "time" ) @@ -22,3 +23,30 @@ func TestCheck(t *testing.T) { t.Errorf("Expected check output %v, got check output %v", expected, result) } } + +func TestDefaultStatusPolicy(t *testing.T) { + c := NewCheck() + c.AddResult(WARNING, "Isolated-frame flux emission outside threshold") + c.AddResult(UNKNOWN, "No response from betaform amplifier") + + expected := "UNKNOWN" + actual := strings.SplitN(c.String(), ":", 2)[0] + if actual != expected { + t.Errorf("Expected %v status, got %v", expected, actual) + } +} + +func TestCustomStatusPolicy(t *testing.T) { + p, _ := NewStatusPolicy([]Status{OK, UNKNOWN, WARNING, CRITICAL}) + c := NewCheckWithOptions(CheckOptions{ + StatusPolicy: p, + }) + c.AddResult(WARNING, "Isolated-frame flux emission outside threshold") + c.AddResult(UNKNOWN, "No response from betaform amplifier") + + expected := "WARNING" + actual := strings.SplitN(c.String(), ":", 2)[0] + if actual != expected { + t.Errorf("Expected %v status, got %v", expected, actual) + } +} diff --git a/result.go b/result.go index 691779b..1d540c6 100644 --- a/result.go +++ b/result.go @@ -1,9 +1,11 @@ package nagiosplugin +import "fmt" + // Nagios plugin exit status. type Status uint -// The usual mapping from 0-3. +// https://nagios-plugins.org/doc/guidelines.html#AEN78 const ( OK Status = iota WARNING @@ -11,6 +13,49 @@ const ( UNKNOWN ) +type ( + // Check results are ordered by severity: only the most severe check + // result will be captured in the plugin's exit status. A status + // policy is used to define severity as a function of check status. + // Higher relative statusSeverity values assign higher severity to a + // status. (Absolute values are insignificant.) + statusSeverity uint + statusPolicy map[Status]statusSeverity +) + +// NewDefaultStatusPolicy returns a status policy that assigns relative +// severity in accordance with conventional Nagios plugin return codes. +// Statuses associated with higher return codes are more severe. +func NewDefaultStatusPolicy() *statusPolicy { + return &statusPolicy{ + OK: statusSeverity(OK), + WARNING: statusSeverity(WARNING), + CRITICAL: statusSeverity(CRITICAL), + UNKNOWN: statusSeverity(UNKNOWN), + } +} + +// NewStatusPolicy returns a status policy that assigns relative +// severity in accordance with a user-configurable prioritised slice. +// Check statuses must be listed in ascending severity order. +func NewStatusPolicy(statuses []Status) (*statusPolicy, error) { + newPol := make(statusPolicy) + for i, status := range statuses { + newPol[status] = statusSeverity(i) + } + + // Ensure all statuses are covered by the new policy. + defaultPol := NewDefaultStatusPolicy() + for status, _ := range *defaultPol { + _, ok := newPol[status] + if !ok { + return nil, fmt.Errorf("missing status: %v", status) + } + } + + return &newPol, nil +} + // Returns string representation of a Status. Panics if given an invalid // status (this will be recovered in check.Finish if it has been deferred). func (s Status) String() string { @@ -27,10 +72,10 @@ func (s Status) String() string { panic("Invalid nagiosplugin.Status.") } -// Result is a combination of a Status and infotext. A check can have -// multiple of these, and only the most important (greatest badness) -// will be reported on the first line of output or represented in the -// plugin's exit status. +// Result encapsulates a machine-readable result code and a +// human-readable description of a problem. A check may have multiple +// Results. Only the most severe Result will be reported on the first +// line of plugin output and in the plugin's exit status. type Result struct { status Status message string diff --git a/result_test.go b/result_test.go new file mode 100644 index 0000000..49e1d7b --- /dev/null +++ b/result_test.go @@ -0,0 +1,20 @@ +package nagiosplugin + +import ( + "testing" +) + +func TestNewStatusPolicyAcceptsCompleteStatuses(t *testing.T) { + _, err := NewStatusPolicy([]Status{OK, UNKNOWN, WARNING, CRITICAL}) + if err != nil { + t.Errorf("NewStatusPolicy(): %v", err) + } +} + +func TestNewStatusPolicyRejectsIncompleteStatuses(t *testing.T) { + // Missing UNKNOWN. + _, err := NewStatusPolicy([]Status{OK, WARNING, CRITICAL}) + if err == nil { + t.Errorf("expected NewStatusPolicy() to return an error") + } +}