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.
// Returns nil (exit 0) if healthy, error (exit 1) if unhealthy.
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).
u.Quiet = true
@@ -170,7 +174,8 @@ func (u *UnifiPoller) HealthCheck() error {
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 {
if !output.Enabled() {
continue

View File

@@ -11,6 +11,8 @@ var (
outputSync sync.RWMutex // nolint: gochecknoglobals
errNoOutputPlugins = fmt.Errorf("no output plugins imported")
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.
@@ -50,6 +52,17 @@ func NewOutput(o *Output) {
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.
func (u *UnifiPoller) Poller() 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)
}
// 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)
if err != nil {
return false, err

View File

@@ -103,11 +103,11 @@ func (s *Server) DebugOutput() (bool, error) {
if s == nil {
return true, nil
}
if !s.Enabled() {
return true, nil
}
if s.HTMLPath == "" {
return true, nil
}
@@ -117,6 +117,12 @@ func (s *Server) DebugOutput() (bool, error) {
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
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port))
if err != nil {