From e38b98e1d1c14fef00ca900f124ed3abe21d8cf4 Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Wed, 16 Oct 2024 18:01:49 -0500 Subject: [PATCH] Add support for (internal) debug logging output OVERVIEW The changes provided by this commit were initially intended to provide a minimal abstraction to support toggling the size of plugin output off/on as needed for troubleshooting. From there the scope widened as further iteration showed that it could be useful to add debug logging support (internally for now) to the library as a whole. To support toggling on/off specific debug log activity, debug log output is currently split into two categories: - debug logging of actions - debug logging of plugin output size The intent of these changes is to provide a way to easily toggle on/off the internal workings of this library. If client code does not enable debug logging, the result should be this library working just as it did before. The current implementation is subject to change as real world testing is applied and feedback collected. CHANGES - allow toggling debug logging output on/off - allow toggling specific debug logging output on/off - e.g., allow all debug messages except for general plugin activity - e.g., allow only debug messages for plugin output size calculations - support setting custom debug logging output target - e.g., redirect debug log messages to a target file instead of stderr (the default debug log target) - add test coverage for new behavior REFERENCES - refs GH-264 - refs GH-271 --- exported_test.go | 6 + logging.go | 291 ++++++++++++++ logging_exported_test.go | 12 + logging_unexported_test.go | 756 +++++++++++++++++++++++++++++++++++++ nagios.go | 142 ++++++- sections.go | 214 ++++++++--- textutils.go | 29 ++ unexported_test.go | 3 +- 8 files changed, 1375 insertions(+), 78 deletions(-) create mode 100644 logging.go create mode 100644 logging_exported_test.go create mode 100644 logging_unexported_test.go diff --git a/exported_test.go b/exported_test.go index 58359bc..f6e3b99 100644 --- a/exported_test.go +++ b/exported_test.go @@ -1322,6 +1322,12 @@ func TestExtractAndDecodeASCII85Payload_FailsToExtractAndDecodePayloadWithInvali this value is what we're left with: \x90\xac8 \x04\x9f\xe6\xc2\xfe\x87\x91\x1a\xa6\x85'\xce2 + Reproduce via: + fmt.Printf("%+q\n", decodedPayload) + + See also: + https://go.dev/blog/strings + This in no way represents the encoded payload nor the original extracted & decoded payload we would expect to see. */ diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..71ba924 --- /dev/null +++ b/logging.go @@ -0,0 +1,291 @@ +// Copyright 2024 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 ( + "io" + "log" + "os" + "strings" +) + +// Logger related values set as constants so that their values are exposed to +// internal tests. +const ( + logMsgPrefix string = "[" + MyPackageName + "] " + logFlags int = log.Ldate | log.Ltime + + // using log.Lshortfile reports the helper function use instead of the + // caller's location. + // + // logFlags int = log.Ldate | log.Ltime | log.Lshortfile +) + +// debugLoggingOptions controls all debug logging behavior for this library. +type debugLoggingOptions struct { + // actions indicates whether actions taken by this library are logged. + // This covers enabling/disabling settings or other general plugin + // activity. + actions bool + + // pluginOutputSize indicates whether all output to the configured plugin + // output sink should be measured and written to the log output sink. + pluginOutputSize bool +} + +// defaultPluginDebugLoggingOutputTarget returns the default debug logging +// output target used when a user-specified value is not provided. +func defaultPluginDebugLoggingOutputTarget() io.Writer { + return os.Stderr +} + +// defaultPluginAbortMessageOutputTarget returns the default abort message +// output target. +func defaultPluginAbortMessageOutputTarget() io.Writer { + return os.Stderr +} + +// defaultPluginDebugLoggerTarget returns the default debug logger target used +// when a user-specified value is not provided for the debug output target. +func defaultPluginDebugLoggerTarget() io.Writer { + // The intended default behavior is to throw away debug log messages if a + // debug log message output target has not been specified. + return io.Discard +} + +// allDebugLoggingOptionsEnabled is a helper function that provides a +// debugLoggingOptions value with all settings enabled. +func allDebugLoggingOptionsEnabled() debugLoggingOptions { + return debugLoggingOptions{ + actions: true, + pluginOutputSize: true, + // Expand this for any new fields added in the future. + } +} + +// allDebugLoggingOptionsDisabled is a helper function that provides a +// debugLoggingOptions value with all settings disabled. +func allDebugLoggingOptionsDisabled() debugLoggingOptions { + return debugLoggingOptions{ + actions: false, + pluginOutputSize: false, + // Expand this for any new fields added in the future. + } +} + +// enableAll enables all debug logging options. The user is able to optionally +// disable select portions of the debug logging output that they do not wish +// to see. +func (dlo *debugLoggingOptions) enableAll() { + *dlo = allDebugLoggingOptionsEnabled() +} + +// disableAll disables all debug logging options. +func (dlo *debugLoggingOptions) disableAll() { + *dlo = allDebugLoggingOptionsDisabled() +} + +// enableActions enables logging plugin actions. +func (dlo *debugLoggingOptions) enableActions() { + dlo.actions = true +} + +// disableActions disables logging plugin actions. +func (dlo *debugLoggingOptions) disableActions() { + dlo.actions = false +} + +// enablePluginOutputSize enables logging plugin output size. +func (dlo *debugLoggingOptions) enablePluginOutputSize() { + dlo.pluginOutputSize = true +} + +// disablePluginOutputSize disables logging plugin output size. +func (dlo *debugLoggingOptions) disablePluginOutputSize() { + dlo.pluginOutputSize = false +} + +// DebugLoggingEnableAll changes the default state of all debug logging +// options for this library from disabled to enabled. +// +// Once enabled, debug logging output is emitted to os.Stderr. This can be +// overridden by explicitly setting a custom debug output target. +func (p *Plugin) DebugLoggingEnableAll() { + // Enable all (granular) debug log options. + p.debugLogging.enableAll() + + // Ensure we have a valid output target, but do not overwrite any custom + // target already set. + if p.logOutputSink == nil { + p.setFallbackDebugLogTarget() + } + + // Connect logger to configured debug log target. + p.setupLogger() +} + +// DebugLoggingDisableAll changes the default state of all debug logging +// options for this library from any custom state back to the default state of +// disabled. +// +// Any custom debug log output target remains as it was before calling this +// function. +func (p *Plugin) DebugLoggingDisableAll() { + p.debugLogging.disableAll() +} + +// DebugLoggingDisableActions disables debug logging of general "actions" or +// plugin activity. This is the most verbose debug logging output generated by +// this library. +func (p *Plugin) DebugLoggingDisableActions() { + p.debugLogging.disableActions() +} + +// DebugLoggingEnableActions enables debug logging of general "actions" or +// plugin activity. This is the most verbose debug logging output generated by +// this library. +// +// Once enabled, debug logging output is emitted to os.Stderr. This can be +// overridden by explicitly setting a custom debug output target. +func (p *Plugin) DebugLoggingEnableActions() { + p.debugLogging.enableActions() + + // Ensure we have a valid output target, but do not overwrite any custom + // target already set. + if p.logOutputSink == nil { + p.setFallbackDebugLogTarget() + } + + // Connect logger to configured debug log target. + p.setupLogger() +} + +// DebugLoggingDisablePluginOutputSize disables debug logging of plugin output +// size calculations. +func (p *Plugin) DebugLoggingDisablePluginOutputSize() { + p.debugLogging.disablePluginOutputSize() +} + +// DebugLoggingEnablePluginOutputSize enables debug logging of plugin output +// size calculations. This debug logging output produces minimal output. +// +// Once enabled, debug logging output is emitted to os.Stderr. This can be +// overridden by explicitly setting a custom debug output target. +func (p *Plugin) DebugLoggingEnablePluginOutputSize() { + p.debugLogging.enablePluginOutputSize() + + // Ensure we have a valid output target, but do not overwrite any custom + // target already set. + if p.logOutputSink == nil { + p.setFallbackDebugLogTarget() + } + + // Connect logger to configured debug log target. + p.setupLogger() +} + +// SetDebugLoggingOutputTarget overrides the current debug logging target with +// the given output target. If the given output target is not valid the +// current target will be used instead. If there isn't a debug logging target +// already set then the default debug logging output target of os.Stderr will +// be used. This behavior is chosen for consistency with the current behavior +// of the Plugin.SetOutputTarget function. +// +// NOTE: While an error message is logged when calling this function with an +// invalid target, calling this function does not change the default debug +// logging state from disabled to enabled. That step must be performed +// separately by either enabling all debug logging options OR enabling select +// debug logging options. +func (p *Plugin) SetDebugLoggingOutputTarget(w io.Writer) { + if w == nil { + if p.logOutputSink == nil { + p.setFallbackDebugLogTarget() + } + + // Connect logger to configured debug log target. + p.setupLogger() + + // We log using an "unfiltered" logger call to ensure this has the + // best chance of being seen. + p.log("invalid output target provided; using default debug log target instead") + + return + } + + p.logOutputSink = w + + // Connect logger to configured debug log target. + p.setupLogger() + + // Use a filtered logger call to allow this message to be emitted or + // excluded based on user-specified debug logging settings. + p.logAction("custom debug logging target set as requested") +} + +// DebugLoggingOutputTarget returns the user-specified debug output target or +// the default value if one was not specified. +func (p *Plugin) DebugLoggingOutputTarget() io.Writer { + if p.logOutputSink == nil { + return defaultPluginDebugLoggingOutputTarget() + } + + return p.logOutputSink +} + +func (p *Plugin) setFallbackDebugLogTarget() { + p.logOutputSink = defaultPluginDebugLoggingOutputTarget() +} + +// setupLogger should be called after the debug log output sink is explicitly +// configured. If called before configuring the debug log output sink the +// plugin's default debug logger target will be used instead. +func (p *Plugin) setupLogger() { + var loggerTarget io.Writer + switch { + case p.logOutputSink == nil: + loggerTarget = defaultPluginDebugLoggerTarget() + default: + loggerTarget = p.logOutputSink + } + + p.logger = log.New(loggerTarget, logMsgPrefix, logFlags) +} + +// log uses the plugin's logger to write the given message to the configured +// output sink. +func (p *Plugin) log(msg string) { + if p.logger == nil { + return + } + + if !strings.HasSuffix(msg, CheckOutputEOL) { + msg += CheckOutputEOL + } + + p.logger.Print(msg) +} + +// logAction is used to log actions taken by this library such as +// enabling/disabling settings or other general plugin activity. +func (p *Plugin) logAction(msg string) { + if !p.debugLogging.actions { + return + } + + p.log(msg) +} + +// logPluginOutputSize is used to log activity related to measuring all output +// to the configured plugin output sink. +func (p *Plugin) logPluginOutputSize(msg string) { + if !p.debugLogging.pluginOutputSize { + return + } + + p.log(msg) +} diff --git a/logging_exported_test.go b/logging_exported_test.go new file mode 100644 index 0000000..b8064dc --- /dev/null +++ b/logging_exported_test.go @@ -0,0 +1,12 @@ +// Copyright 2024 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 provides test coverage for exported package +// functionality. +// +//nolint:dupl,gocognit // ignore "lines are duplicate of" and function complexity +package nagios_test diff --git a/logging_unexported_test.go b/logging_unexported_test.go new file mode 100644 index 0000000..12e680a --- /dev/null +++ b/logging_unexported_test.go @@ -0,0 +1,756 @@ +// Copyright 2024 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 provides test coverage for unexported package functionality. +// +//nolint:dupl,gocognit // ignore "lines are duplicate of" and function complexity +package nagios + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestPlugin_SetDebugLoggingOutputTarget_IsValidWithValidInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Assert that log output sink is still unset + if plugin.logOutputSink != nil { + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + } else { + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + // Assert that logger is still unset + if plugin.logger != nil { + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + } else { + t.Log("OK: plugin logger is at the expected default unset value.") + } + + var outputBuffer strings.Builder + + plugin.SetDebugLoggingOutputTarget(&outputBuffer) + + // All debug logging options were previously enabled after setting a debug + // logging output target. This behavior has changed and now debug logging + // must be explicitly enabled. + // + // assertAllDebugLoggingOptionsAreEnabled(plugin, t) + + // Assert that plugin.outputSink is set as expected. + switch { + case plugin.logOutputSink == nil: + t.Fatal("ERROR: plugin logOutputSink is unset instead of the given custom value.") + case plugin.logOutputSink == defaultPluginDebugLoggingOutputTarget(): + t.Fatal("ERROR: plugin logOutputSink is set to the default/fallback value instead of the expected custom value.") + case plugin.logOutputSink != &outputBuffer: + t.Error("ERROR: logOutputSink is not set to custom output target") + // t.Logf("plugin.logOutputSink address: %p", plugin.logOutputSink) + // t.Logf("&outputBuffer address: %p", &outputBuffer) + + d := cmp.Diff(&outputBuffer, plugin.logOutputSink) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: plugin logOutputSink is at the expected custom value.") + } + + assertLoggerIsConfiguredProperlyAfterSettingDebugLoggingOutputTarget(plugin, t) +} + +func TestPlugin_SetDebugLoggingOutputTarget_CorrectlySetsFallbackLoggingTargetWithInvalidInput(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // By calling this method we implicitly enable debug logging. + // + // By setting an invalid output target a log message is emitted to the + // default debug log output target. + plugin.SetDebugLoggingOutputTarget(nil) + + // Assert that plugin.outputSink is set as expected. + want := defaultPluginDebugLoggingOutputTarget() + got := plugin.logOutputSink + + switch { + case got == nil: + t.Error("ERROR: plugin debug log output target is unset instead of the default/fallback value.") + case got != want: + t.Error("ERROR: plugin debug log output target is not set to the default/fallback value.") + d := cmp.Diff(want, got) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: plugin debug log output target is at the expected default/fallback value.") + } +} + +func TestPlugin_setupLogger_CorrectlySetsDefaultLoggerTargetWhenDebugLogOutputSinkIsUnset(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + switch { + case plugin.logger != nil: + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + default: + t.Log("OK: plugin logger is at the expected default unset value.") + } + + plugin.setupLogger() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + loggerTarget := plugin.logger.Writer() + switch { + case loggerTarget == nil: + t.Fatal("ERROR: plugin logger target is unset instead of being configured for use.") + case loggerTarget != defaultPluginDebugLoggerTarget(): + t.Fatal("ERROR: plugin logger target is not set to use default logger target as expected.") + default: + t.Logf("OK: plugin logger target is set to default logger target ('%#v') as expected.", defaultPluginDebugLoggerTarget()) + } +} + +func TestPlugin_setupLogger_CorrectlySetsLoggerTargetWhenLogOutputSinkIsSet(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Configure log output sink to a custom target. + var outputBuffer strings.Builder + plugin.logOutputSink = &outputBuffer + + switch { + case plugin.logger != nil: + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + default: + t.Log("OK: plugin logger is at the expected default unset value.") + } + + plugin.setupLogger() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + loggerTarget := plugin.logger.Writer() + switch { + case loggerTarget == nil: + t.Fatal("ERROR: plugin logger target is unset instead of being configured for use.") + case loggerTarget != plugin.logOutputSink: + t.Fatal("ERROR: plugin logger target is not set to custom output sink as expected.") + default: + t.Log("OK: plugin logger target is set as expected.") + } +} + +func TestPlugin_DebugLoggingEnableAll_CorrectlyConfiguresLogTargetAndLoggerWithFallbackValues(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Assert that log output sink is still unset + if plugin.logOutputSink != nil { + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + } else { + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + // Assert that logger is still unset + if plugin.logger != nil { + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + } else { + t.Log("OK: plugin logger is at the expected default unset value.") + } + + // Expected results of calling this function: + // + // - the fallback debug log target is set + // - the logger is setup + plugin.DebugLoggingEnableAll() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + loggerTarget := plugin.logger.Writer() + switch { + case loggerTarget == nil: + t.Fatal("ERROR: plugin logger target is unset instead of being configured for use.") + case loggerTarget != plugin.logOutputSink: + t.Fatal("ERROR: plugin logger target is not set to match debug log output target as expected.") + default: + t.Log("OK: plugin logger target is set as expected.") + } +} + +func TestPlugin_DebugLoggingEnableAll_CorrectlyEnablesAllDebugLoggingOptions(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + plugin.DebugLoggingEnableAll() + + assertAllDebugLoggingOptionsAreEnabled(plugin, t) +} + +func TestPlugin_DebugLoggingDisableAll_CorrectlyLeavesLogTargetAndLoggerUnmodified(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Assert that log output sink is still unset + if plugin.logOutputSink != nil { + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + } else { + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + // Configure log output sink to a custom target. + var outputBuffer strings.Builder + plugin.logOutputSink = &outputBuffer + + switch { + case plugin.logger != nil: + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + default: + t.Log("OK: plugin logger is at the expected default unset value.") + } + + plugin.setupLogger() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + pluginLoggerBeforeDisablingLogging := plugin.logger + pluginLogTargetBeforeDisablingLogging := plugin.logOutputSink + + plugin.DebugLoggingDisableAll() + + // Assert that the debug log target remains untouched. + switch { + case plugin.logOutputSink != pluginLogTargetBeforeDisablingLogging: + t.Errorf("ERROR: plugin debug log target is not set to same value before logging was disabled.") + d := cmp.Diff( + plugin.logOutputSink, + pluginLogTargetBeforeDisablingLogging, + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(strings.Builder{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + + default: + t.Log("OK: plugin debug log target is set to same value before logging was disabled.") + } + + // Assert that the logger remains untouched. + switch { + case plugin.logger != pluginLoggerBeforeDisablingLogging: + t.Fatal("ERROR: plugin logger is not set to same value before logging was disabled.") + default: + t.Log("OK: plugin logger is set to same value before logging was disabled.") + } +} + +func TestPlugin_DebugLoggingDisableAll_CorrectlyDisablesAllDebugLoggingOptions(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + plugin.DebugLoggingDisableAll() + + assertAllDebugLoggingOptionsAreDisabled(plugin, t) +} + +func TestPlugin_DebugLoggingEnableActions_CorrectlyConfiguresLogTargetAndLoggerWithFallbackValues(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Assert that log output sink is still unset + if plugin.logOutputSink != nil { + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + } else { + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + // Assert that logger is still unset + if plugin.logger != nil { + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + } else { + t.Log("OK: plugin logger is at the expected default unset value.") + } + + // Expected results of calling this function: + // + // - the fallback debug log target is set + // - the logger is setup + plugin.DebugLoggingEnableActions() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + loggerTarget := plugin.logger.Writer() + switch { + case loggerTarget == nil: + t.Fatal("ERROR: plugin logger target is unset instead of being configured for use.") + case loggerTarget != plugin.logOutputSink: + t.Fatal("ERROR: plugin logger target is not set to match debug log output target as expected.") + default: + t.Log("OK: plugin logger target is set as expected.") + } +} + +func TestPlugin_DebugLoggingEnableActions_CorrectlyEnablesOnlyDebugLoggingActionsOption(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Flip everything off to start with so we can selectively enable just the + // debug logging option we're interested in. + plugin.debugLogging = allDebugLoggingOptionsDisabled() + + plugin.DebugLoggingEnableActions() + + selectDebugLoggingOptionsEnabled := allDebugLoggingOptionsDisabled() + selectDebugLoggingOptionsEnabled.actions = true + + if !cmp.Equal( + selectDebugLoggingOptionsEnabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + selectDebugLoggingOptionsEnabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } +} + +func TestPlugin_DebugLoggingDisableActions_CorrectlyDisablesOnlyDebugLoggingActionsOption(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Flip everything on to start with so we can selectively disable specific + // debug logging options. + plugin.debugLogging = allDebugLoggingOptionsEnabled() + + plugin.DebugLoggingDisableActions() + + selectDebugLoggingOptionsDisabled := allDebugLoggingOptionsEnabled() + selectDebugLoggingOptionsDisabled.actions = false + + if !cmp.Equal( + selectDebugLoggingOptionsDisabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + selectDebugLoggingOptionsDisabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } +} + +func TestPlugin_DebugLoggingEnablePluginOutputSize_CorrectlyConfiguresLogTargetAndLoggerWithFallbackValues(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Assert that log output sink is still unset + if plugin.logOutputSink != nil { + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + } else { + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + // Assert that logger is still unset + if plugin.logger != nil { + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + } else { + t.Log("OK: plugin logger is at the expected default unset value.") + } + + // Expected results of calling this function: + // + // - the fallback debug log target is set + // - the logger is setup + plugin.DebugLoggingEnablePluginOutputSize() + + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + loggerTarget := plugin.logger.Writer() + switch { + case loggerTarget == nil: + t.Fatal("ERROR: plugin logger target is unset instead of being configured for use.") + case loggerTarget != plugin.logOutputSink: + t.Fatal("ERROR: plugin logger target is not set to match debug log output target as expected.") + default: + t.Log("OK: plugin logger target is set as expected.") + } +} + +func TestPlugin_DebugLoggingEnablePluginOutputSize_CorrectlyEnablesOnlyDebugLoggingOutputSizeOption(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Flip everything off to start with so we can selectively enable just the + // debug logging option we're interested in. + plugin.debugLogging = allDebugLoggingOptionsDisabled() + + plugin.DebugLoggingEnablePluginOutputSize() + + selectDebugLoggingOptionsEnabled := allDebugLoggingOptionsDisabled() + selectDebugLoggingOptionsEnabled.pluginOutputSize = true + + if !cmp.Equal( + selectDebugLoggingOptionsEnabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + selectDebugLoggingOptionsEnabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } +} + +func TestPlugin_DebugLoggingDisablePluginOutputSize_CorrectlyDisablesOnlyDebugLoggingOutputSizeOption(t *testing.T) { + t.Parallel() + + plugin := NewPlugin() + + // Flip everything on to start with so we can selectively disable specific + // debug logging options. + plugin.debugLogging = allDebugLoggingOptionsEnabled() + + plugin.DebugLoggingDisablePluginOutputSize() + + selectDebugLoggingOptionsDisabled := allDebugLoggingOptionsEnabled() + selectDebugLoggingOptionsDisabled.pluginOutputSize = false + + if !cmp.Equal( + selectDebugLoggingOptionsDisabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + selectDebugLoggingOptionsDisabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } + +} + +func TestPlugin_log_CorrectlyProducesNoOutputWhenLoggerIsUnset(t *testing.T) { + + // credit: Modified version of Google Gemini example code (via Search) + // + // prompt: "golang using os.Pipe() to change os.Stderr and os.Stdout for tests" + + // Intentionally *not* running this test in parallel since we're going to + // monkey patch io.StdOut and io.StdErr briefly. + // + // t.Parallel() + + var ( + origStdErr = os.Stderr + origStdOut = os.Stdout + ) + + // Ensure that no matter what, we restore the original values to prevent + // affecting other tests. + t.Cleanup(func() { + os.Stderr = origStdErr + os.Stdout = origStdOut + }) + + plugin := NewPlugin() + + switch { + case plugin.logOutputSink != nil: + t.Fatal("ERROR: plugin logOutputSink is not at the expected default unset value.") + default: + t.Log("OK: plugin logOutputSink is at the expected default unset value.") + } + + switch { + case plugin.logger != nil: + t.Fatal("ERROR: plugin logger is not at the expected default unset value.") + default: + t.Log("OK: plugin logger is at the expected default unset value.") + } + + // Create a new pipe for capturing standard output + r, w, stdOutOsPipeErr := os.Pipe() + if stdOutOsPipeErr != nil { + t.Fatal("Error creating pipe to emulate os.Stdout as part of test setup") + } + os.Stdout = w + + // Create a new pipe for capturing standard error + rErr, wErr, stdErrOsPipeErr := os.Pipe() + if stdErrOsPipeErr != nil { + t.Fatal("Error creating pipe to emulate os.Stderr as part of test setup") + } + os.Stderr = wErr + + // This shouldn't go anywhere. + plugin.log("Testing") + + // Close the write ends of the pipes to signal that we're done writing + if err := w.Close(); err != nil { + t.Fatalf("Error closing stdout pipe: %v", err) + } + + if err := wErr.Close(); err != nil { + t.Fatalf("Error closing stdout pipe: %v", err) + } + + // Restore original stdout and stderr values. + os.Stderr = origStdErr + os.Stdout = origStdOut + + // Read the output from the pipes + var stdOutBuffer, stdErrBuffer bytes.Buffer + + var written int64 + var ioCopyErr error + + written, ioCopyErr = io.Copy(&stdOutBuffer, r) + switch { + case ioCopyErr != nil: + t.Fatalf("ERROR: Failed to copy stdout pipe content to stdout buffer for evaluation: %v", ioCopyErr) + case written != 0: + t.Errorf("ERROR: Copied %d bytes of unexpected content to stdout buffer", written) + default: + t.Log("OK: io.Copy operation on stdout pipe found no content but also encountered no errors") + } + + written, ioCopyErr = io.Copy(&stdErrBuffer, rErr) + switch { + case ioCopyErr != nil: + t.Fatalf("ERROR: Failed to copy stderr pipe content to stderr buffer for evaluation: %v", ioCopyErr) + case written != 0: + t.Errorf("ERROR: Copied %d bytes of unexpected content to stderr buffer", written) + default: + t.Log("OK: io.Copy operation on stderr pipe found no content but also encountered no errors") + } + + capturedStdOut := stdOutBuffer.String() + switch { + case capturedStdOut != "": + want := "" + got := capturedStdOut + d := cmp.Diff(want, got) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: No output logged to stdout as expected.") + } + + capturedStdErr := stdErrBuffer.String() + switch { + case capturedStdErr != "": + want := "" + got := capturedStdErr + d := cmp.Diff(want, got) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: No output logged to stderr as expected.") + } +} + +func TestPlugin_logAction_CorrectlyProducesNoOutputWhenDebugLoggingActionsOptionIsDisabled(t *testing.T) { + plugin := NewPlugin() + + var outputBuffer strings.Builder + + plugin.SetDebugLoggingOutputTarget(&outputBuffer) + plugin.debugLogging.actions = false + + // This shouldn't go anywhere. + testMsg := "Test action entry" + plugin.logAction(testMsg) + + capturedDebugLogOutput := outputBuffer.String() + switch { + case strings.Contains(capturedDebugLogOutput, testMsg): + want := removeEntry(capturedDebugLogOutput, testMsg, CheckOutputEOL) + got := capturedDebugLogOutput + d := cmp.Diff(want, got) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: No debug logging output captured as expected.") + } +} + +func TestPlugin_logPluginOutputSize_CorrectlyProducesNoOutputWhenDebugLoggingOutputSizeOptionIsDisabled(t *testing.T) { + plugin := NewPlugin() + + var outputBuffer strings.Builder + + plugin.SetDebugLoggingOutputTarget(&outputBuffer) + plugin.debugLogging.pluginOutputSize = false + + // This shouldn't go anywhere. + testMsg := "Test output size entry" + plugin.logPluginOutputSize(testMsg) + + capturedDebugLogOutput := outputBuffer.String() + switch { + case strings.Contains(capturedDebugLogOutput, testMsg): + want := removeEntry(capturedDebugLogOutput, testMsg, CheckOutputEOL) + got := capturedDebugLogOutput + d := cmp.Diff(want, got) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Log("OK: No debug logging output captured as expected.") + } +} + +func assertLoggerIsConfiguredProperlyAfterSettingDebugLoggingOutputTarget(plugin *Plugin, t *testing.T) { + t.Helper() + + // Assert that plugin.logger is set as expected. + switch { + case plugin.logger == nil: + t.Fatal("ERROR: plugin logger is unset instead of being configured for use.") + default: + t.Log("OK: plugin logger is set as expected.") + } + + // Assert that plugin.logger prefix is set as expected. + actualLoggerPrefix := plugin.logger.Prefix() + switch { + case actualLoggerPrefix != logMsgPrefix: + t.Error("ERROR: plugin logger prefix not set to the expected value.") + d := cmp.Diff(logMsgPrefix, actualLoggerPrefix) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Logf("OK: plugin logger prefix is at the expected value %s", actualLoggerPrefix) + } + + // Assert that plugin.logger flags is set as expected. + actualLoggerFlags := plugin.logger.Flags() + switch { + case actualLoggerFlags != logFlags: + t.Error("ERROR: plugin logger flags are not set to the expected value.") + d := cmp.Diff(logFlags, actualLoggerFlags) + t.Fatalf("(-want, +got)\n:%s", d) + default: + t.Logf("OK: plugin logger flags is set to the expected value %d", actualLoggerFlags) + } +} + +func assertAllDebugLoggingOptionsAreEnabled(plugin *Plugin, t *testing.T) { + t.Helper() + + // Assert that debug logging is enabled by requiring that all fields are + // set. + allDebugLoggingOptionsEnabled := allDebugLoggingOptionsEnabled() + + if !cmp.Equal( + allDebugLoggingOptionsEnabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + allDebugLoggingOptionsEnabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } +} + +func assertAllDebugLoggingOptionsAreDisabled(plugin *Plugin, t *testing.T) { + t.Helper() + + // Assert that debug logging is enabled by requiring that all fields are + // set. + allDebugLoggingOptionsDisabled := allDebugLoggingOptionsDisabled() + + if !cmp.Equal( + allDebugLoggingOptionsDisabled, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + plugin.debugLogging, cmp.AllowUnexported(debugLoggingOptions{}), + ) { + d := cmp.Diff( + allDebugLoggingOptionsDisabled, + plugin.debugLogging, + + // https://stackoverflow.com/questions/73476661/cmp-equal-gives-panic-message-cannot-handle-unexported-field-at + cmp.AllowUnexported(debugLoggingOptions{}), + ) + + t.Errorf("(-want, +got)\n:%s", d) + } +} diff --git a/nagios.go b/nagios.go index 0295b97..06f58e4 100644 --- a/nagios.go +++ b/nagios.go @@ -12,12 +12,19 @@ import ( "errors" "fmt" "io" + "log" "os" "runtime/debug" "strings" "time" ) +// General package information. +const ( + MyPackageName string = "go-nagios" + MyPackagePurpose string = "Provide support and functionality common to monitoring plugins." +) + // Nagios plugin/service check states. These constants replicate the values // from utils.sh which is normally found at one of these two locations, // depending on which Linux distribution you're using: @@ -187,6 +194,14 @@ type Plugin struct { // outputSink is the user-specified or fallback target for plugin output. outputSink io.Writer + // logOutputSink is the user-specified or fallback target for debug level + // plugin output. + logOutputSink io.Writer + + // logger is an embedded logger used to emit debug log messages (if + // enabled). + logger *log.Logger + // encodedPayloadBuffer holds a user-specified payload *before* encoding // is performed. If provided, this payload is later encoded and included // in the generated plugin output. @@ -278,6 +293,9 @@ type Plugin struct { // instead. shouldSkipOSExit bool + // debugLogging is the collection of debug logging options for the plugin. + debugLogging debugLoggingOptions + // BrandingCallback is a function that is called before application // termination to emit branding details at the end of the notification. // See also ExitCallBackFunc. @@ -342,7 +360,9 @@ func (p *Plugin) ReturnCheckResults() { // Check for unhandled panic in client code. If present, override // Plugin and make clear that the client code/plugin crashed. + p.logAction("Checking for unhandled panic") if err := recover(); err != nil { + p.logAction("Handling panic") p.AddError(fmt.Errorf("%w: %s", ErrPanicDetected, err)) @@ -373,35 +393,49 @@ func (p *Plugin) ReturnCheckResults() { } + p.logAction("No unhandled panic found") + + p.logAction("Processing ServiceOutput section") p.handleServiceOutputSection(&output) + p.logAction("Processing Errors section") p.handleErrorsSection(&output) + p.logAction("Processing Thresholds section") p.handleThresholdsSection(&output) + p.logAction("Processing LongServiceOutput section") p.handleLongServiceOutput(&output) + p.logAction("Processing Encoded Payload section") p.handleEncodedPayload(&output) // If set, call user-provided branding function before emitting // performance data and exiting application. - if p.BrandingCallback != nil { - _, _ = fmt.Fprintf(&output, "%s%s%s", CheckOutputEOL, p.BrandingCallback(), CheckOutputEOL) + switch { + case p.BrandingCallback != nil: + p.logAction("Adding Branding Callback") + written, err := fmt.Fprintf(&output, "%s%s%s", CheckOutputEOL, p.BrandingCallback(), CheckOutputEOL) + if err != nil { + panic("Failed to write BrandingCallback content to buffer") + } + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin BrandingCalling content written to buffer", written)) + + default: + p.logAction("Branding Callback not requested, skipping") } + p.logAction("Processing Performance Data section") p.handlePerformanceData(&output) // Emit all collected plugin output using user-specified or fallback // output target. + p.logAction("Processing final plugin output") p.emitOutput(output.String()) - // TODO: Should we offer an option to redirect the log message to stderr - // to another error output sink? - // - // TODO: Perhaps just don't emit anything at all? switch { case p.shouldSkipOSExit: - _, _ = fmt.Fprintln(os.Stderr, "Skipping os.Exit call as requested.") + p.logAction("Skipping os.Exit call as requested.") default: os.Exit(p.ExitStatusCode) } @@ -445,6 +479,11 @@ func (p *Plugin) AddPerfData(skipValidate bool, perfData ...PerformanceData) err // for ensuring that a given error is not already recorded in the collection. func (p *Plugin) AddError(errs ...error) { p.Errors = append(p.Errors, errs...) + + p.logAction(fmt.Sprintf( + "%d errors added to collection", + len(errs), + )) } // AddUniqueError appends provided errors to the collection if they are not @@ -458,12 +497,34 @@ func (p *Plugin) AddUniqueError(errs ...error) { existingErrStrings[i] = p.Errors[i].Error() } + var totalUniqueErrors int + for _, err := range errs { if inList(err.Error(), existingErrStrings, true) { continue } p.Errors = append(p.Errors, err) + totalUniqueErrors++ } + + p.logAction(fmt.Sprintf( + "%d unique errors added to collection", + totalUniqueErrors, + )) +} + +// OutputTarget returns the user-specified plugin output target or +// the default value if one was not specified. +func (p *Plugin) OutputTarget() io.Writer { + if p.outputSink == nil { + p.logAction("Plugin output target not explicitly set, returning default plugin output target") + + return defaultPluginOutputTarget() + } + + p.logAction("Returning current plugin output target") + + return p.outputSink } // SetOutputTarget assigns a target for Nagios plugin output. By default @@ -472,11 +533,17 @@ func (p *Plugin) AddUniqueError(errs ...error) { func (p *Plugin) SetOutputTarget(w io.Writer) { // Guard against potential nil argument. if w == nil { - p.outputSink = os.Stdout + // We log using an "filtered" logger call to retain previous behavior + // of not emitting a "problem has occurred" message. + p.logAction("Specified output target is invalid, falling back to default") + + p.outputSink = defaultPluginOutputTarget() return } + p.logAction("Setting output target to specified value") + p.outputSink = w } @@ -506,6 +573,7 @@ func (p *Plugin) SetEncodedPayloadDelimiterRight(delimiter string) { // Disabling the call to os.Exit is needed by tests to prevent panics in Go // 1.16 and newer. func (p *Plugin) SkipOSExit() { + p.logAction("Setting plugin to skip os.Exit call as requested") p.shouldSkipOSExit = true } @@ -516,6 +584,11 @@ func (p *Plugin) SkipOSExit() { // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) SetPayloadBytes(input []byte) (int, error) { + p.logAction(fmt.Sprintf( + "Overwriting payload buffer with %d bytes input", + len(input), + )) + p.encodedPayloadBuffer.Reset() return p.encodedPayloadBuffer.Write(input) @@ -528,6 +601,11 @@ func (p *Plugin) SetPayloadBytes(input []byte) (int, error) { // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) SetPayloadString(input string) (int, error) { + p.logAction(fmt.Sprintf( + "Overwriting payload buffer with %d bytes input", + len(input), + )) + p.encodedPayloadBuffer.Reset() return p.encodedPayloadBuffer.WriteString(input) @@ -539,6 +617,11 @@ func (p *Plugin) SetPayloadString(input string) (int, error) { // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) AddPayloadBytes(input []byte) (int, error) { + p.logAction(fmt.Sprintf( + "Appending %d bytes input to payload buffer", + len(input), + )) + return p.encodedPayloadBuffer.Write(input) } @@ -548,12 +631,22 @@ func (p *Plugin) AddPayloadBytes(input []byte) (int, error) { // The contents of this buffer will be included in the plugin's output as an // encoded payload suitable for later retrieval/decoding. func (p *Plugin) AddPayloadString(input string) (int, error) { + p.logAction(fmt.Sprintf( + "Appending %d bytes input to payload buffer", + len(input), + )) + return p.encodedPayloadBuffer.WriteString(input) } // UnencodedPayload returns the payload buffer contents in string format as-is // without encoding applied. func (p *Plugin) UnencodedPayload() string { + p.logAction(fmt.Sprintf( + "Returning %d bytes from payload buffer", + p.encodedPayloadBuffer.Len(), + )) + return p.encodedPayloadBuffer.String() } @@ -566,27 +659,36 @@ func defaultPluginOutputTarget() io.Writer { // emitOutput writes final plugin output to the previously set output target. // No further modifications to plugin output are performed. func (p Plugin) emitOutput(pluginOutput string) { + p.logPluginOutputSize(fmt.Sprintf("%d bytes total plugin output to write", len(pluginOutput))) - // Emit all collected output using user-specified output target. Fall back - // to standard output if not set. + // Emit all collected output using user-specified output target or + // fallback to the default if not set. if p.outputSink == nil { - p.outputSink = os.Stdout + p.logAction("Custom plugin output target not set") + p.logAction("Falling back to default plugin output target") + p.outputSink = defaultPluginOutputTarget() } - // Attempt to write to output sink. If this fails, send error to - // os.Stderr. If that fails (however unlikely), we have bigger problems - // and should abort. - _, sinkWriteErr := fmt.Fprint(p.outputSink, pluginOutput) + p.logAction("Writing plugin output") + + // 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. + pluginOutputWritten, sinkWriteErr := fmt.Fprint(p.outputSink, pluginOutput) if sinkWriteErr != nil { + p.logAction("Failed to write plugin output") + _, stdErrWriteErr := fmt.Fprintf( - os.Stderr, + defaultPluginAbortMessageOutputTarget(), "Failed to write output to given output sink: %s", sinkWriteErr.Error(), ) if stdErrWriteErr != nil { - panic("Failed to initial output sink failure error message to stderr") + panic("Failed to write initial output sink failure error message to stderr") } } + + p.logPluginOutputSize(fmt.Sprintf("%d bytes total plugin output written", pluginOutputWritten)) } // tryAddDefaultTimeMetric inserts a default `time` performance data metric @@ -596,6 +698,8 @@ func (p *Plugin) tryAddDefaultTimeMetric() { // We already have an existing time metric, skip replacing it. if _, hasTimeMetric := p.perfData[defaultTimeMetricLabel]; hasTimeMetric { + p.logAction("Existing time metric present, skipping replacement") + return } @@ -603,6 +707,8 @@ func (p *Plugin) tryAddDefaultTimeMetric() { // not have an internal plugin start time that we can use to generate a // default time metric. if p.start.IsZero() { + p.logAction("Plugin not created using constructor, so no default time metric to use") + return } @@ -611,6 +717,8 @@ func (p *Plugin) tryAddDefaultTimeMetric() { } p.perfData[defaultTimeMetricLabel] = defaultTimeMetric(p.start) + + p.logAction("Added default time metric to collection") } // defaultTimeMetric is a helper function that wraps the logic used to provide diff --git a/sections.go b/sections.go index cd0a058..de23c39 100644 --- a/sections.go +++ b/sections.go @@ -33,81 +33,119 @@ func (p Plugin) handleServiceOutputSection(w io.Writer) { // formatting changes to this content, simply emit it as-is. This helps // avoid potential issues with literal characters being interpreted as // formatting verbs. - _, _ = fmt.Fprint(w, p.ServiceOutput) + written, err := fmt.Fprint(w, p.ServiceOutput) + if err != nil { + // Very unlikely to occur, but we should still account for it. + panic("Failed to write ServiceOutput to given output sink") + } + + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin ServiceOutput content written to buffer", written)) } // handleErrorsSection is a wrapper around the logic used to handle/process // the Errors section header and listing. func (p Plugin) handleErrorsSection(w io.Writer) { + if p.isErrorsHidden() { + return + } // If one or more errors were recorded and client code has not opted to // hide the section ... - if !p.isErrorsHidden() { - _, _ = fmt.Fprintf(w, - "%s%s**%s**%s%s", - CheckOutputEOL, - CheckOutputEOL, - p.getErrorsLabelText(), - CheckOutputEOL, - CheckOutputEOL, - ) + var totalWritten int - if p.LastError != nil { - _, _ = fmt.Fprintf(w, "* %v%s", p.LastError, CheckOutputEOL) + writeErrorToOutputSink := func(err error) { + written, writeErr := fmt.Fprintf(w, "* %v%s", err, CheckOutputEOL) + if writeErr != nil { + panic("Failed to write LastError field content to given output sink") } - // Process any non-nil errors in the collection. - for _, err := range p.Errors { - if err != nil { - _, _ = fmt.Fprintf(w, "* %v%s", err, CheckOutputEOL) - } - } + totalWritten += written + } + + written, writeErr := fmt.Fprintf(w, + "%s%s**%s**%s%s", + CheckOutputEOL, + CheckOutputEOL, + p.getErrorsLabelText(), + CheckOutputEOL, + CheckOutputEOL, + ) + if writeErr != nil { + panic("Failed to write errors section label to given output sink") + } + totalWritten += written + if p.LastError != nil { + writeErrorToOutputSink(p.LastError) } + // Process any non-nil errors in the collection. + for _, err := range p.Errors { + if err != nil { + writeErrorToOutputSink(err) + } + } + + p.logPluginOutputSize(fmt.Sprintf("%d bytes total plugin errors content written to buffer", totalWritten)) } // handleThresholdsSection is a wrapper around the logic used to // handle/process the Thresholds section header and listing. func (p Plugin) handleThresholdsSection(w io.Writer) { - // We skip emitting the thresholds section if there isn't any // LongServiceOutput to process. - if p.LongServiceOutput != "" { - - // If one or more threshold values were recorded and client code has - // not opted to hide the section ... - if !p.isThresholdsSectionHidden() { - - _, _ = fmt.Fprintf(w, - "%s**%s**%s%s", - CheckOutputEOL, - p.getThresholdsLabelText(), - CheckOutputEOL, - CheckOutputEOL, - ) - - if p.CriticalThreshold != "" { - _, _ = fmt.Fprintf(w, - "* %s: %v%s", - StateCRITICALLabel, - p.CriticalThreshold, - CheckOutputEOL, - ) - } - - if p.WarningThreshold != "" { - _, _ = fmt.Fprintf(w, - "* %s: %v%s", - StateWARNINGLabel, - p.WarningThreshold, - CheckOutputEOL, - ) - } + if p.LongServiceOutput == "" || p.isThresholdsSectionHidden() { + return + } + + // If one or more threshold values were recorded and client code has + // not opted to hide the section ... + + var totalWritten int + + written, err := fmt.Fprintf(w, "%s**%s**%s%s", + CheckOutputEOL, + p.getThresholdsLabelText(), + CheckOutputEOL, + CheckOutputEOL, + ) + if err != nil { + panic("Failed to write thresholds section label to given output sink") + } + + totalWritten += written + + if p.CriticalThreshold != "" { + written, err := fmt.Fprintf(w, "* %s: %v%s", + StateCRITICALLabel, + p.CriticalThreshold, + CheckOutputEOL, + ) + if err != nil { + panic("Failed to write thresholds section label to given output sink") } + + totalWritten += written + } + + if p.WarningThreshold != "" { + warningThresholdText := fmt.Sprintf( + "* %s: %v%s", + StateWARNINGLabel, + p.WarningThreshold, + CheckOutputEOL, + ) + + written, err := fmt.Fprint(w, warningThresholdText) + if err != nil { + panic("Failed to write thresholds section label to given output sink") + } + + totalWritten += written } + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin thresholds section content written to buffer", totalWritten)) } // handleLongServiceOutput is a wrapper around the logic used to @@ -119,6 +157,8 @@ func (p Plugin) handleLongServiceOutput(w io.Writer) { return } + var totalWritten int + // Hide section header/label if threshold and error values were not // specified by client code or if client code opted to explicitly hide // those sections; there is no need to use a header to separate the @@ -129,26 +169,44 @@ func (p Plugin) handleLongServiceOutput(w io.Writer) { // ServiceOutput content. switch { case !p.isThresholdsSectionHidden() || !p.isErrorsHidden(): - _, _ = fmt.Fprintf(w, + written, err := fmt.Fprintf(w, "%s**%s**%s", CheckOutputEOL, p.getDetailedInfoLabelText(), CheckOutputEOL, ) + if err != nil { + panic("Failed to write LongServiceOutput section label to given output sink") + } + + totalWritten += written + default: - _, _ = fmt.Fprint(w, CheckOutputEOL) + written, err := fmt.Fprint(w, CheckOutputEOL) + if err != nil { + panic("Failed to write LongServiceOutput section label spacer to given output sink") + } + + totalWritten += written } // Note: fmt.Println() (and fmt.Fprintln()) has the same issue as `\n`: // Nagios seems to interpret them literally instead of emitting an actual // newline. We work around that by using fmt.Fprintf() for output that is // intended for display within the Nagios web UI. - _, _ = fmt.Fprintf(w, + written, err := fmt.Fprintf(w, "%s%v%s", CheckOutputEOL, p.LongServiceOutput, CheckOutputEOL, ) + if err != nil { + panic("Failed to write LongServiceOutput field content to given output sink") + } + + totalWritten += written + + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin LongServiceOutput content written to buffer", totalWritten)) } // handleEncodedPayload is a wrapper around the logic used to handle/process @@ -169,33 +227,53 @@ func (p Plugin) handleEncodedPayload(w io.Writer) { rightDelimiter, ) + var totalWritten int + // Hide section header/label if no payload was specified. // // If we hide the section header, we still provide some padding to prevent // this output from running up against the LongServiceOutput content. switch { case p.encodedPayloadBuffer.Len() > 0: - _, _ = fmt.Fprintf(w, + written, err := fmt.Fprintf(w, "%s**%s**%s", CheckOutputEOL, p.getEncodedPayloadLabelText(), CheckOutputEOL, ) + if err != nil { + panic("Failed to write EncodedPayload section label to given output sink") + } + + totalWritten += written // Note: fmt.Println() (and fmt.Fprintln()) has the same issue as // `\n`: Nagios seems to interpret them literally instead of emitting // an actual newline. We work around that by using fmt.Fprintf() for // output that is intended for display within the Nagios web UI. - _, _ = fmt.Fprintf(w, + written, err = fmt.Fprintf(w, "%s%v%s", CheckOutputEOL, encodedWithDelimiters, CheckOutputEOL, ) + if err != nil { + panic("Failed to write EncodedPayload content to given output sink") + } + + totalWritten += written + default: - _, _ = fmt.Fprint(w, CheckOutputEOL) + written, err := fmt.Fprint(w, CheckOutputEOL) + if err != nil { + panic("Failed to write EncodedPayload section spacer to given output sink") + } + + totalWritten += written } + + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin EncodedPayload content written to buffer", totalWritten)) } // handlePerformanceData is a wrapper around the logic used to @@ -217,22 +295,40 @@ func (p *Plugin) handlePerformanceData(w io.Writer) { return } + var totalWritten int + // Performance data metrics are appended to plugin output. These // metrics are provided as a single line, leading with a pipe // character, a space and one or more metrics each separated from // another by a single space. - _, _ = fmt.Fprint(w, " |") + written, err := fmt.Fprint(w, " |") + if err != nil { + panic("Failed to write performance data content to given output sink") + } + + totalWritten += written // Sort performance data values prior to emitting them so that the // output is consistent across plugin execution. perfData := p.getSortedPerfData() for _, pd := range perfData { - _, _ = fmt.Fprint(w, pd.String()) + written, err = fmt.Fprint(w, pd.String()) + if err != nil { + panic("Failed to write performance data content to given output sink") + } + totalWritten += written } // Add final trailing newline to satisfy Nagios plugin output format. - _, _ = fmt.Fprint(w, CheckOutputEOL) + written, err = fmt.Fprint(w, CheckOutputEOL) + if err != nil { + panic("Failed to write performance data content to given output sink") + } + + totalWritten += written + + p.logPluginOutputSize(fmt.Sprintf("%d bytes plugin performance data content written to buffer", totalWritten)) } diff --git a/textutils.go b/textutils.go index 572d1c7..a866df9 100644 --- a/textutils.go +++ b/textutils.go @@ -27,3 +27,32 @@ func inList(needle string, haystack []string, ignoreCase bool) bool { return false } + +// removeEntry is a helper function to allow removing an entry or "line" from +// input which matches a given substring. The specified delimiter is used to +// perform the initial line splitting for entry removal and then to rejoin the +// elements into the original input string (minus the intended entry to +// remove). +func removeEntry(input string, substr string, delimiter string) string { + if len(input) == 0 || len(substr) == 0 || len(delimiter) == 0 { + return input + } + + // https://stackoverflow.com/a/57213476/903870 + removeAtIndex := func(s []string, index int) []string { + // ret := make([]string, 0) + ret := make([]string, 0, len(s)-1) + ret = append(ret, s[:index]...) + return append(ret, s[index+1:]...) + } + + lines := strings.Split(input, delimiter) + var idxToRemove int + for idx, line := range lines { + if strings.Contains(line, substr) { + idxToRemove = idx + } + } + + return strings.Join(removeAtIndex(lines, idxToRemove), delimiter) +} diff --git a/unexported_test.go b/unexported_test.go index f19ab39..8d7a65e 100644 --- a/unexported_test.go +++ b/unexported_test.go @@ -13,7 +13,6 @@ package nagios import ( _ "embed" "fmt" - "os" "strings" "testing" @@ -69,7 +68,7 @@ func TestPluginSetOutputTargetIsValidWithValidInput(t *testing.T) { switch { case plugin.outputSink == nil: t.Fatal("ERROR: plugin outputSink is unset instead of the given custom value.") - case plugin.outputSink == os.Stdout: + case plugin.outputSink == defaultPluginOutputTarget(): t.Fatal("ERROR: plugin outputSink is set to the default/fallback value instead of the expected custom value.") default: t.Log("OK: plugin outputSink is at the expected custom value.")