Merge pull request #890 from unpoller/docker-healthcheck

Add Docker health check support
This commit is contained in:
Cody Lee 2025-12-08 13:25:57 -06:00 committed by GitHub
commit 4e6ebee524
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 103 additions and 1 deletions

View File

@ -12,4 +12,7 @@ FROM gcr.io/distroless/static-debian11
COPY unpoller /usr/bin/unpoller
COPY --from=builder /etc/unpoller /etc/unpoller
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD ["/usr/bin/unpoller", "--health"]
ENTRYPOINT [ "/usr/bin/unpoller" ]

View File

@ -24,7 +24,7 @@ examples and default configurations.
OPTIONS
---
`unpoller [-c <config-file>,[config-file]] [-j <filter>] [-e <pass>] [-h] [-v]`
`unpoller [-c <config-file>,[config-file]] [-j <filter>] [-e <pass>] [--health] [-d] [-h] [-v]`
-c, --config <config-file>,[config-file]
Provide a configuration file (instead of the default). You may provide
@ -39,6 +39,17 @@ OPTIONS
-v, --version
Display version and exit.
--health
Run a health check and exit with status 0 (healthy) or 1 (unhealthy).
This validates the configuration file, ensures input and output plugins
are properly configured, and performs basic connectivity checks. Useful
for Docker HEALTHCHECK and container orchestration readiness probes.
-d, --debugio
Debug the inputs and outputs configured and exit. This performs more
verbose validation checks than --health and is useful for troubleshooting
configuration issues.
-j, --dumpjson <filter>
This is a debug option; use this when you are missing data in your graphs,
and/or you want to inspect the raw data coming from the controller. The

View File

@ -9,3 +9,18 @@ in InfluxDB by UniFi Poller.
##### HOWTO
**Learn more about how and when to use these *Docker Compose* files in the
[Docker Wiki](https://unpoller.com/docs/install/dockercompose).**
## Health Check
The UniFi Poller Docker image includes a built-in health check that validates
the configuration and checks plugin connectivity. The health check runs every
30 seconds and marks the container as unhealthy if configuration issues are
detected or if enabled outputs cannot be reached.
You can manually run the health check:
```bash
docker exec <container_name> /usr/bin/unpoller --health
```
The health check is automatically used by Docker and container orchestration
platforms (Kubernetes, Docker Swarm, etc.) to determine container health status.

View File

@ -34,6 +34,8 @@ services:
unifi-poller:
restart: always
image: ghcr.io/unpoller/unpoller:${POLLER_TAG}
# Health check is built into the Docker image
# It validates configuration and checks plugin connectivity
depends_on:
- grafana
- influxdb

View File

@ -36,6 +36,8 @@ services:
unifi-poller:
restart: always
image: ghcr.io/unpoller/unpoller:${POLLER_TAG}
# Health check is built into the Docker image
# It validates configuration and checks plugin connectivity
environment:
- UP_INFLUXDB_USER=${INFLUXDB_ADMIN_USER}
- UP_INFLUXDB_PASS=${INFLUXDB_ADMIN_PASSWORD}

View File

@ -122,3 +122,66 @@ func (u *UnifiPoller) DebugIO() error {
return allErr
}
// HealthCheck performs a basic health check suitable for Docker HEALTHCHECK.
// 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 {
// Silence output for health checks (Docker doesn't need verbose logs).
u.Quiet = true
// Load configuration.
cfile, err := getFirstFile(strings.Split(u.Flags.ConfigFile, ","))
if err != nil {
return fmt.Errorf("health check failed: config file not found: %w", err)
}
u.Flags.ConfigFile = cfile
if err := u.ParseConfigs(); err != nil {
return fmt.Errorf("health check failed: config parse error: %w", err)
}
inputSync.RLock()
defer inputSync.RUnlock()
outputSync.RLock()
defer outputSync.RUnlock()
// Check that we have at least one input and one output configured.
if len(inputs) == 0 {
return fmt.Errorf("health check failed: no input plugins configured")
}
if len(outputs) == 0 {
return fmt.Errorf("health check failed: no output plugins configured")
}
// Check if at least one output is enabled.
hasEnabledOutput := false
for _, output := range outputs {
if output.Enabled() {
hasEnabledOutput = true
break
}
}
if !hasEnabledOutput {
return fmt.Errorf("health check failed: no enabled output plugins")
}
// Perform basic validation checks on enabled outputs.
for _, output := range outputs {
if !output.Enabled() {
continue
}
ok, err := output.DebugOutput()
if !ok || err != nil {
return fmt.Errorf("health check failed: output %s validation failed: %w", output.Name, err)
}
}
// All checks passed, application is healthy.
return nil
}

View File

@ -73,6 +73,7 @@ type Flags struct {
HashPW string
ShowVer bool
DebugIO bool
Health bool
*pflag.FlagSet
}

View File

@ -34,6 +34,10 @@ func (u *UnifiPoller) Start() error {
return u.PrintPasswordHash()
}
if u.Flags.Health {
return u.HealthCheck()
}
cfile, err := getFirstFile(strings.Split(u.Flags.ConfigFile, ","))
if err != nil {
return err
@ -76,6 +80,7 @@ func (f *Flags) Parse(args []string) {
f.StringVarP(&f.DumpJSON, "dumpjson", "j", "",
"This debug option prints a json payload and exits. See man page for more info.")
f.BoolVarP(&f.DebugIO, "debugio", "d", false, "Debug the Inputs and Outputs configured and exit.")
f.BoolVarP(&f.Health, "health", "", false, "Run health check and exit with status 0 (healthy) or 1 (unhealthy).")
f.StringVarP(&f.ConfigFile, "config", "c", DefaultConfFile(),
"Poller config file path. Separating multiple paths with a comma will load the first config file found.")
f.BoolVarP(&f.ShowVer, "version", "v", false, "Print the version and exit.")