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.")