Skip to content

Commit

Permalink
Merge pull request #330 from atc0005/i324-add-plugin-output-size-metric
Browse files Browse the repository at this point in the history
Add plugin output size metric
  • Loading branch information
atc0005 authored Nov 27, 2024
2 parents 8e9907c + 2879a3b commit c4c477d
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ such time that the API is considered stable.
- Panics from client code are captured and reported
- panics are surfaced as `CRITICAL` state
- service output and error details are overridden to make panics prominent
- Optional support for emitting performance data metric for plugin output size
- disabled by default
- can be toggled on by client code as desired
- Optional support for emitting performance data generated by plugins
- if not overridden by client code *and* if using the provided
`nagios.NewPlugin()` constructor, a default `time` performance data metric
Expand Down
52 changes: 52 additions & 0 deletions example_enable_plugin_output_size_metric_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 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_test

import (
"github.com/atc0005/go-nagios"
)

// Ignore this. This is just to satisfy the "whole file" example requirements
// per https://go.dev/blog/examples.
var _ = "https://github.com/atc0005/go-nagios"

// This example demonstrates enabling the plugin output size metric. This
// optional metric is disabled by default.
func Example_enablePluginOutputSizeMetric() {
// First, create an instance of the Plugin type. By default this value is
// configured to indicate a successful execution. This should be
// overridden by client code to indicate the final plugin state to Nagios
// when the plugin exits.
var plugin = nagios.NewPlugin()

// Second, immediately defer ReturnCheckResults() so that it runs as the
// last step in your client code. If you do not defer ReturnCheckResults()
// immediately any other deferred functions in your client code will not
// run.
//
// Avoid calling os.Exit() directly from your code. If you do, this
// library is unable to function properly; this library expects that it
// will handle calling os.Exit() with the required exit code (and
// specifically formatted output).
//
// For handling error cases, the approach is roughly the same, only you
// call return explicitly to end execution of the client code and allow
// deferred functions to run.
defer plugin.ReturnCheckResults()

// Enable emitting a plugin output size metric. This optional metric is
// disabled by default.
plugin.EnablePluginOutputSizePerfDataMetric()

// more stuff here involving performing the actual service check

plugin.ServiceOutput = "one-line summary of plugin results " //nolint:goconst
plugin.LongServiceOutput = "more detailed output from plugin here" //nolint:goconst

// more stuff here involving wrapping up the service check
}
139 changes: 128 additions & 11 deletions exported_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ var (
//go:embed testdata/plugin-output-gh103-one-line-with-perf-data.txt
pluginOutputGH103OneLineWithPerfData string

//go:embed testdata/plugin-output-multiline-with-optional-perf-data-included.txt
pluginOutputMultiLineWithOptionalPerfDataIncluded string

//go:embed testdata/plugin-output-one-line-with-optional-perf-data-included.txt
pluginOutputOneLineWithOptionalPerfDataIncluded string

//go:embed testdata/payload/small_json_payload_unencoded.txt
smallJSONPayloadUnencoded string

Expand Down Expand Up @@ -201,14 +207,15 @@ func TestPluginOutputIsValid(t *testing.T) {
}
}

// TestPerformanceDataIsOnSameLineAsServiceOutput asserts that performance
// data is emitted on the same line as the Service Output (aka, "one-line
// summary") if Long Service Output is empty.
// TestDefaultPerformanceDataIsOnSameLineAsServiceOutput asserts that
// performance data is emitted on the same line as the Service Output (aka,
// "one-line summary") if Long Service Output is empty. We use default
// performance data metrics for this test.
//
// See also:
//
// - https://github.com/atc0005/go-nagios/issues/103
func TestPerformanceDataIsOnSameLineAsServiceOutput(t *testing.T) {
func TestDefaultPerformanceDataIsOnSameLineAsServiceOutput(t *testing.T) {
t.Parallel()

want := pluginOutputGH103OneLineWithPerfData
Expand All @@ -233,7 +240,7 @@ func TestPerformanceDataIsOnSameLineAsServiceOutput(t *testing.T) {
" is 0.01% of 18.0TB with 18.0TB remaining" +
" [WARNING: 90% , CRITICAL: 95%]"

pluginOutputWithLongServiceOutputMetrics(t, &plugin)
pluginOutputWithLongServiceOutputDefaultMetrics(t, &plugin)

// Process exit state, emit output to our output buffer.
plugin.ReturnCheckResults()
Expand All @@ -247,13 +254,14 @@ func TestPerformanceDataIsOnSameLineAsServiceOutput(t *testing.T) {
}
}

// TestPerformanceDataIsAfterLongServiceOutput asserts that performance data
// is emitted after Long Service Output when that content is available.
// TestDefaultPerformanceDataIsAfterLongServiceOutput asserts that performance
// data is emitted after Long Service Output when that content is available.
// We use default performance data metrics for this test.
//
// See also:
//
// - https://github.com/atc0005/go-nagios/issues/103
func TestPerformanceDataIsAfterLongServiceOutput(t *testing.T) {
func TestDefaultPerformanceDataIsAfterLongServiceOutput(t *testing.T) {
t.Parallel()

want := pluginOutputGH103MultiLineWithPerfData
Expand All @@ -274,7 +282,97 @@ func TestPerformanceDataIsAfterLongServiceOutput(t *testing.T) {
plugin.SkipOSExit()

pluginOutputWithLongServiceOutputSetup(t, &plugin)
pluginOutputWithLongServiceOutputMetrics(t, &plugin)
pluginOutputWithLongServiceOutputDefaultMetrics(t, &plugin)

// Process exit state, emit output to our output buffer.
plugin.ReturnCheckResults()

// Retrieve the output buffer content so that we can compare actual output
// against our expected output to assert we have a 1:1 match.
got := outputBuffer.String()

if d := cmp.Diff(want, got); d != "" {
t.Errorf("(-want, +got)\n:%s", d)
}
}

// TestAllOptionalPerformanceDataIsOnSameLineAsServiceOutput asserts that
// performance data is emitted on the same line as the Service Output (aka,
// "one-line summary") if Long Service Output is empty. We use all optional
// performance data metrics for this test.
//
// NOTE: Later additions of optional performance data metrics will require
// updating the "golden" file to reflect those values (and expected output
// size). We may need to refactor this test to either be less strict.
func TestAllOptionalPerformanceDataIsOnSameLineAsServiceOutput(t *testing.T) {
t.Parallel()

want := pluginOutputOneLineWithOptionalPerfDataIncluded

// Setup Plugin value manually. This approach does not provide the
// default time metric that would be provided when using the Plugin
// constructor.
plugin := nagios.Plugin{
LastError: nil,
ExitStatusCode: nagios.StateOKExitCode,
}

var outputBuffer strings.Builder
plugin.SetOutputTarget(&outputBuffer)

// os.Exit calls break tests
plugin.SkipOSExit()

//nolint:goconst
plugin.ServiceOutput =
"OK: Datastore HUSVM-DC1-vol6 space usage (0 VMs)" +
" is 0.01% of 18.0TB with 18.0TB remaining" +
" [WARNING: 90% , CRITICAL: 95%]"

pluginOutputWithLongServiceOutputDefaultMetrics(t, &plugin)
pluginOutputWithLongServiceOutputAllOptionalMetrics(t, &plugin)

// Process exit state, emit output to our output buffer.
plugin.ReturnCheckResults()

// Retrieve the output buffer content so that we can compare actual output
// against our expected output to assert we have a 1:1 match.
got := outputBuffer.String()

if d := cmp.Diff(want, got); d != "" {
t.Errorf("(-want, +got)\n:%s", d)
}
}

// TestAllOptionalPerformanceDataIsAfterLongServiceOutput asserts that
// performance data is emitted after Long Service Output when that content is
// available. We use all optional performance data metrics for this test.
//
// NOTE: Later additions of optional performance data metrics will require
// updating the "golden" file to reflect those values (and expected output
func TestAllOptionalPerformanceDataIsAfterLongServiceOutput(t *testing.T) {
t.Parallel()

want := pluginOutputMultiLineWithOptionalPerfDataIncluded

var outputBuffer strings.Builder

// Setup Plugin value manually. This approach does not provide the
// default time metric that would be provided when using the Plugin
// constructor.
plugin := nagios.Plugin{
LastError: nil,
ExitStatusCode: nagios.StateOKExitCode,
}

plugin.SetOutputTarget(&outputBuffer)

// os.Exit calls break tests
plugin.SkipOSExit()

pluginOutputWithLongServiceOutputSetup(t, &plugin)
pluginOutputWithLongServiceOutputDefaultMetrics(t, &plugin)
pluginOutputWithLongServiceOutputAllOptionalMetrics(t, &plugin)

// Process exit state, emit output to our output buffer.
plugin.ReturnCheckResults()
Expand Down Expand Up @@ -592,7 +690,7 @@ func TestPluginWithEncodedPayloadWithValidInputProducesValidOutput(t *testing.T)
t.Logf("Successfully appended %d bytes given input to payload buffer", written)
}

pluginOutputWithLongServiceOutputMetrics(t, plugin)
pluginOutputWithLongServiceOutputDefaultMetrics(t, plugin)

// Process exit state, emit output to our output buffer.
plugin.ReturnCheckResults()
Expand Down Expand Up @@ -1544,7 +1642,7 @@ func pluginOutputWithLongServiceOutputSetup(t *testing.T, plugin *nagios.Plugin)
plugin.LongServiceOutput += longServiceOutputBuffer.String()
}

func pluginOutputWithLongServiceOutputMetrics(t *testing.T, plugin *nagios.Plugin) {
func pluginOutputWithLongServiceOutputDefaultMetrics(t *testing.T, plugin *nagios.Plugin) {
t.Helper()

// os.Exit calls break tests. Potentially duplicated by caller, but
Expand All @@ -1560,3 +1658,22 @@ func pluginOutputWithLongServiceOutputMetrics(t *testing.T, plugin *nagios.Plugi
t.Errorf("failed to add performance data: %v", err)
}
}

func pluginOutputWithLongServiceOutputAllOptionalMetrics(t *testing.T, plugin *nagios.Plugin) {
t.Helper()

// os.Exit calls break tests. Potentially duplicated by caller, but
// effectively a NOOP if repeated so not an issue.
plugin.SkipOSExit()

// pd := nagios.PerformanceData{
// Label: "plugin_output_size",
// Value: "9999KB",
// }

plugin.EnablePluginOutputSizePerfDataMetric()

// if err := plugin.AddPerfData(false, pd); err != nil {
// t.Errorf("failed to add performance data: %v", err)
// }
}
60 changes: 60 additions & 0 deletions nagios.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"log"
"os"
"runtime/debug"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -305,6 +306,11 @@ type Plugin struct {
// instead.
shouldSkipOSExit bool

// shouldEmitTotalPluginSizeMetric indicates whether client code has opted
// to emit (append) a performance data metric calculating the total plugin
// output size.
shouldEmitTotalPluginSizeMetric bool

// debugLogging is the collection of debug logging options for the plugin.
debugLogging debugLoggingOptions

Expand Down Expand Up @@ -589,6 +595,13 @@ func (p *Plugin) SkipOSExit() {
p.shouldSkipOSExit = true
}

// EnablePluginOutputSizePerfDataMetric appends a performance data metric
// noting the total plugin output size.
func (p *Plugin) EnablePluginOutputSizePerfDataMetric() {
p.logAction("Enabling total plugin output size metric as requested")
p.shouldEmitTotalPluginSizeMetric = true
}

// SetPayloadBytes uses the given input in bytes to overwrite any existing
// content in the payload buffer. It returns the length of input and a
// potential error. If given empty input the payload buffer is reset without
Expand Down Expand Up @@ -704,6 +717,10 @@ func (p Plugin) emitOutput(pluginOutput string) {

p.logAction("Writing plugin output")

if p.shouldEmitTotalPluginSizeMetric {
pluginOutput = addPluginOutputSizeMetric(pluginOutput)
}

// Attempt to write to output sink. If this fails, send error to the
// default abort message output target. If that fails (however unlikely),
// we have bigger problems and should abort.
Expand Down Expand Up @@ -754,6 +771,49 @@ func (p *Plugin) tryAddDefaultTimeMetric() {
p.logAction("Added default time metric to collection")
}

// addPluginOutputSizeMetric appends a performance data metric to the given
// input noting the total plugin output size. If the metric is already present
// the original input is returned unmodified.
func addPluginOutputSizeMetric(pluginOutput string) string {
metricLabel := "plugin_output_size"

// Attempt to prevent adding the same metric twice.
if strings.Contains(pluginOutput, metricLabel) {
return pluginOutput
}

pluginOutput = strings.TrimSuffix(pluginOutput, CheckOutputEOL)

outputSizeMetric := PerformanceData{
Label: metricLabel,
Value: "PLACEHOLDER",
UnitOfMeasurement: "B", // bytes
}

var modifiedPluginOutput string

// Code snippet inspired by or generated with the help of ChatGPT, OpenAI.
for {
// Calculate the current length.
length := len(modifiedPluginOutput)

// Construct metric using updated length value.
outputSizeMetric.Value = strconv.Itoa(length)
outputSizeMetricString := outputSizeMetric.String() + CheckOutputEOL

// Construct modified plugin output using the updated metric.
modifiedPluginOutput = pluginOutput + outputSizeMetricString

// Break when the actual length of modified plugin output containing
// the output size metric matches the calculated length.
if len(modifiedPluginOutput) == length {
break
}
}

return modifiedPluginOutput
}

// defaultTimeMetric is a helper function that wraps the logic used to provide
// a default performance data metric that tracks plugin execution time.
func defaultTimeMetric(start time.Time) PerformanceData {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
OK: Datastore HUSVM-DC1-vol6 space usage (0 VMs) is 0.01% of 18.0TB with 18.0TB remaining [WARNING: 90% , CRITICAL: 95%]
**THRESHOLDS**

* CRITICAL: 95% datastore usage
* WARNING: 90% datastore usage

**DETAILED INFO**

Datastore Space Summary:

* Name: HUSVM-DC1-vol6
* Space Used: 2.3GB (0.01%)
* Space Remaining: 18.0TB (99.99%)
* VMs: 0


---

* vSphere environment: https://vc1.example.com:443/sdk
* Plugin User Agent: check-vmware/v0.30.6-0-g25fdcdc

| 'time'=874ms;;;; 'plugin_output_size'=530B;;;;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OK: Datastore HUSVM-DC1-vol6 space usage (0 VMs) is 0.01% of 18.0TB with 18.0TB remaining [WARNING: 90% , CRITICAL: 95%] | 'time'=874ms;;;; 'plugin_output_size'=171B;;;;

0 comments on commit c4c477d

Please sign in to comment.