Fix health check port binding conflict (issue #892)

The Docker health check was attempting to bind to ports already in use by
the running application, causing "address already in use" errors. This fix
adds a health check mode that skips network binding operations while still
validating output configuration (listen addresses, paths, etc.).

Changes:
- Add health check mode flag in pkg/poller/outputs.go
- Update prometheus and webserver DebugOutput() to skip port binding in health check mode
- Maintain full configuration validation without network conflicts

Fixes #892

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Cody Lee
2025-12-09 08:11:21 -06:00
parent 4e6ebee524
commit 832334655c
4 changed files with 33 additions and 3 deletions

View File

@@ -127,6 +127,10 @@ func (u *UnifiPoller) DebugIO() error {
// It validates configuration and checks if inputs/outputs are properly configured. // It validates configuration and checks if inputs/outputs are properly configured.
// Returns nil (exit 0) if healthy, error (exit 1) if unhealthy. // Returns nil (exit 0) if healthy, error (exit 1) if unhealthy.
func (u *UnifiPoller) HealthCheck() error { func (u *UnifiPoller) HealthCheck() error {
// Enable health check mode to skip network binding in output validation.
SetHealthCheckMode(true)
defer SetHealthCheckMode(false)
// Silence output for health checks (Docker doesn't need verbose logs). // Silence output for health checks (Docker doesn't need verbose logs).
u.Quiet = true u.Quiet = true
@@ -170,7 +174,8 @@ func (u *UnifiPoller) HealthCheck() error {
return fmt.Errorf("health check failed: no enabled output plugins") return fmt.Errorf("health check failed: no enabled output plugins")
} }
// Perform basic validation checks on enabled outputs. // Perform configuration validation on enabled outputs.
// Network binding checks will be skipped automatically due to health check mode.
for _, output := range outputs { for _, output := range outputs {
if !output.Enabled() { if !output.Enabled() {
continue continue

View File

@@ -11,6 +11,8 @@ var (
outputSync sync.RWMutex // nolint: gochecknoglobals outputSync sync.RWMutex // nolint: gochecknoglobals
errNoOutputPlugins = fmt.Errorf("no output plugins imported") errNoOutputPlugins = fmt.Errorf("no output plugins imported")
errAllOutputStopped = fmt.Errorf("all output plugins have stopped, or none enabled") errAllOutputStopped = fmt.Errorf("all output plugins have stopped, or none enabled")
// healthCheckMode indicates when we're running in health check mode to skip network operations
healthCheckMode bool // nolint: gochecknoglobals
) )
// Collect is passed into output packages so they may collect metrics to output. // Collect is passed into output packages so they may collect metrics to output.
@@ -50,6 +52,17 @@ func NewOutput(o *Output) {
outputs = append(outputs, o) outputs = append(outputs, o)
} }
// SetHealthCheckMode enables or disables health check mode.
// When enabled, output plugins should skip network operations in DebugOutput().
func SetHealthCheckMode(enabled bool) {
healthCheckMode = enabled
}
// IsHealthCheckMode returns true if we're running in health check mode.
func IsHealthCheckMode() bool {
return healthCheckMode
}
// Poller returns the poller config. // Poller returns the poller config.
func (u *UnifiPoller) Poller() Poller { func (u *UnifiPoller) Poller() Poller {
return *u.Config.Poller return *u.Config.Poller

View File

@@ -139,6 +139,12 @@ func (u *promUnifi) DebugOutput() (bool, error) {
return false, fmt.Errorf("invalid listen address: %s (must be of the form \"IP:Port\"", u.HTTPListen) return false, fmt.Errorf("invalid listen address: %s (must be of the form \"IP:Port\"", u.HTTPListen)
} }
// Skip network binding check during health checks to avoid "address already in use"
// errors when the main application is already running and bound to the port.
if poller.IsHealthCheckMode() {
return true, nil
}
ln, err := net.Listen("tcp", u.HTTPListen) ln, err := net.Listen("tcp", u.HTTPListen)
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -103,11 +103,11 @@ func (s *Server) DebugOutput() (bool, error) {
if s == nil { if s == nil {
return true, nil return true, nil
} }
if !s.Enabled() { if !s.Enabled() {
return true, nil return true, nil
} }
if s.HTMLPath == "" { if s.HTMLPath == "" {
return true, nil return true, nil
} }
@@ -117,6 +117,12 @@ func (s *Server) DebugOutput() (bool, error) {
return false, fmt.Errorf("problem with HTML path: %w", err) return false, fmt.Errorf("problem with HTML path: %w", err)
} }
// Skip network binding check during health checks to avoid "address already in use"
// errors when the main application is already running and bound to the port.
if poller.IsHealthCheckMode() {
return true, nil
}
// check the port // check the port
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port)) ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port))
if err != nil { if err != nil {