Documentation
¶
Overview ¶
Package health implements file-based healthchecks for distroless containers.
Docker's HEALTHCHECK needs a command inside the container. Distroless images have no curl/wget/shell, so the canonical approach is to re-invoke the app binary with a `health` subcommand that probes the application's liveness. This package uses a file marker at DefaultPath: the running process touches the file at lifecycle points, the probe process stats it.
Failure modes:
- If the marker directory is not writable (typically compose declares `read_only: true` without a `tmpfs: /tmp` mount), the constructor logs one Warn with a fix hint and enters degraded mode. In degraded mode the long-running process treats Set / Cleanup as no-ops. The probe process independently detects the same condition and reports healthy, because the container is alive and the only broken piece is the signaling channel. Reporting unhealthy would trigger a Docker restart loop that cannot fix a compose misconfiguration.
- Transient failures during Set are logged at Warn but do not change the marker's mode. A failed Set that leaves the marker absent on a still-writable directory (e.g. directory churn) surfaces at the next probe as unhealthy. A failure whose cause also leaves the directory unwritable (full tmpfs), and a failed Set(false) that leaves the marker present, are both reported healthy by the probe, matching the degraded-mode rationale above.
Logging goes through slog.Default(); configure it via slog.SetDefault in main before constructing a Marker.
Thread-safe; Set may be called from any goroutine.
Example ¶
Example demonstrates the two-process healthcheck pattern. The long-running process creates a marker; the probe process stats it.
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/cplieger/health"
)
func main() {
path := filepath.Join(os.TempDir(), ".healthy-example")
m := health.NewMarker(path)
defer m.Cleanup()
m.Set(true)
fmt.Println("healthy:", m.Healthy())
m.Set(false)
fmt.Println("healthy:", m.Healthy())
}
Output: healthy: true healthy: false
Index ¶
Examples ¶
Constants ¶
const DefaultPath = "/tmp/.healthy"
DefaultPath is the default marker location. Docker healthchecks stat this path; the app creates and removes it at lifecycle points. /tmp is conventional because compose services with read_only:true typically mount /tmp as tmpfs.
Variables ¶
This section is empty.
Functions ¶
func Handler ¶
Handler returns an http.Handler that reports the health of the given Signal as a JSON object. Returns 200 with {"status":"OK"} when healthy, 503 with {"status":"Unavailable"} otherwise. This mirrors the response shape of hellofresh/health-go and satisfies K8s HTTP probe expectations.
If s is nil, the handler always reports unhealthy (503).
The handler is optional — import and wire it only if your container exposes an HTTP endpoint alongside the file-marker probe.
Note: in degraded mode (unwritable marker directory) Marker.Healthy() returns false, so this endpoint reports 503 -- intentionally diverging from the `health` subcommand probe (ProbeCheck), which reports healthy to avoid a Docker restart loop (see package doc). Do not wire this endpoint as the sole liveness probe on a service that may run with a read-only filesystem and no /tmp tmpfs, or it will restart-loop a container that is actually alive.
func ProbeCheck ¶
ProbeCheck implements the health-probe decision without calling os.Exit, so it can be unit-tested. Returns 0 for healthy or degraded, 1 for unhealthy.
Example ¶
ExampleProbeCheck shows how to use ProbeCheck for a testable probe that does not call os.Exit.
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/cplieger/health"
)
func main() {
dir, _ := os.MkdirTemp("", "health-example-*")
defer os.RemoveAll(dir)
path := filepath.Join(dir, ".healthy")
// No marker yet — writable dir means unhealthy.
fmt.Println("code:", health.ProbeCheck(path))
// Create marker — healthy.
os.WriteFile(path, nil, 0o600)
fmt.Println("code:", health.ProbeCheck(path))
}
Output: code: 1 code: 0
func ProbeDir ¶ added in v1.1.0
ProbeDir reports whether the marker's parent directory is writable by creating and deleting a temp file: nil when writable, the underlying error otherwise. This is the exact check NewMarker and ProbeCheck use internally to decide degraded mode; it is exported so consumers (and their tests) can assert marker-directory writability without copying the probe into their own package.
func RunProbe ¶
func RunProbe(path string)
RunProbe runs in the separate `health` subcommand process. It exits 0 if the marker is present or the marker directory is unwritable (degraded mode: the long-running process cannot signal through the filesystem, so the probe falls back to "alive"). It exits 1 when the marker is absent from a writable directory, which is the real unhealthy signal.
Types ¶
type Marker ¶
type Marker struct {
// contains filtered or unexported fields
}
Marker implements the file-based distroless healthcheck pattern. Use NewMarker to construct it; call Set(bool) at lifecycle points; defer Cleanup on shutdown; call RunProbe from main when os.Args[1] is "health".
func NewMarker ¶
NewMarker constructs a marker for path and probes the parent directory for writability. On failure it logs a single Warn with a fix hint and returns a marker in degraded mode; callers need not branch on the result.
func (*Marker) Cleanup ¶
func (m *Marker) Cleanup()
Cleanup removes the marker. Typically called via defer at shutdown. In degraded mode Cleanup is a no-op.
func (*Marker) Healthy ¶
Healthy reports whether the marker file currently exists. Satisfies the Signal interface so HTTP handlers can report liveness without reaching into a package global. Strict os.Stat: a degraded marker directory (read-only mount, missing tmpfs) causes Healthy to return false so the HTTP endpoint honestly reports unhealthy.
In degraded mode this intentionally diverges from ProbeCheck, which returns 0 (healthy) to avoid a Docker restart loop. Healthy returns false because HTTP consumers deserve an honest signal; see package doc.