health

repository·main·Indexed 21 days ago

https://github.com/alexliesenfeld/health

A simple and flexible health check library for Go that allows developers to build robust health endpoints. It supports both synchronous and asynchronous check patterns and is designed to be compatible with existing Go health check libraries such as hellofresh/health-go, etherlabsio/healthcheck, heptiolabs/healthcheck, and InVisionApp/go-health.

Tokens
5.5K
Snippets
18
Records
25
Agent score
73%

What's inside health

  1. Use Middleware and Interceptors to hook into the lifecycle

    main

    The library provides two ways to intercept the health check process:

    Middleware (MiddlewareFunc)

    Middleware intercepts calls to Checker.Check (every incoming HTTP request). It allows you to access check-related information and post-process results before they are sent as an HTTP response.

    Available middlewares:

    • BasicAuth: Reduces exposed health details based on authentication success.
    • CustomAuth: Same as BasicAuth, but uses an arbitrary function for authentication.
    • FullDetailsOnQueryParam: Disables health details unless a specific query parameter is present.
    • BasicLogger: Provides basic request-oriented logging.

    Interceptors (InterceptorFunc)

    Interceptors intercept calls to the individual check functions. This is useful for cross-functional code that needs access to check state information or needs to perform reusable logic during the execution of a specific component check.

    Available interceptors:

    • BasicLogger: Provides basic component check function logging.
  2. Synchronous vs. Asynchronous health checks

    main

    The library supports two modes of executing health checks:

    1. Synchronous Checks (WithCheck): These are executed every time an HTTP request hits the health endpoint. The handler waits for all check functions to complete before returning the aggregated result. This is simple but can increase latency and impact service availability if checks are slow.

    2. Asynchronous (Periodic) Checks (WithPeriodicCheck): These execute on a fixed schedule in the background. The HTTP handler reads the most recent result from a local cache, allowing for near-instant responses. This is recommended for expensive or high-latency checks to avoid service disruptions in cloud environments like Kubernetes.

    You can mix both types in a single Checker.

  3. Listen to health status changes

    main

    You can react to changes in health status (e.g., for logging or metrics) using two types of listeners:

    1. Overall Status Listener: Registered via health.WithStatusListener on the Checker. It is invoked when the aggregated health status of the entire system changes.
    2. Component Status Listener: Registered via the StatusListener field within a specific health.Check struct. It is invoked when the status of that specific component changes.
    // Component-specific listener
    health.WithPeriodicCheck(5*time.Second, 0, health.Check{
        Name:   "search",
        Check:  myCheckFunc,
        StatusListener: func (ctx context.Context, name string, state health.CheckState) {
            log.Printf("status of component '%s' changed to %s", name, state.Status)
        },
    }),
    
    // Overall system listener
    health.WithStatusListener(func (ctx context.Context, state health.CheckerState) {
        log.Printf("overall system health status changed to %s", state.Status)
    }),
  4. Implement a basic health check endpoint

    main

    The library provides a health.NewHandler(checker) which returns an http.Handler. This handler serves the aggregated health status of all configured checks as a JSON response.

    Common configuration options for health.NewChecker include:

    • health.WithCacheDuration(time.Duration): Sets the TTL for the cache (default is 1 second).
    • health.WithTimeout(time.Duration): Sets a global timeout applied to all checks.
    • health.WithCheck(health.Check): Adds a synchronous check that runs on every HTTP request.
    • health.WithPeriodicCheck(interval, delay, health.Check): Adds an asynchronous check that runs on a fixed schedule in the background.
    • health.WithStatusListener(func(context.Context, health.CheckerState)): Registers a listener for changes to the overall system health status.
    package main
    
    import (
    	"context"
    	"fmt"
    	"github.com/alexliesenfeld/health"
    	"log"
    	"net/http"
    	"time"
    )
    
    func main() {
    	checker := health.NewChecker(
    		health.WithCacheDuration(1*time.Second),
    		health.WithTimeout(10*time.Second),
    		health.WithCheck(health.Check{
    			Name:    "database",
    			Timeout: 2 * time.Second,
    			Check:   func(ctx context.Context) error { return nil }, // Replace with actual check
    		}),
    		health.WithPeriodicCheck(15*time.Second, 3*time.Second, health.Check{
    			Name: "search",
    			Check: func(ctx context.Context) error {
    				return fmt.Errorf("this makes the check fail")
    			},
    		}),
    		health.WithStatusListener(func(ctx context.Context, state health.CheckerState) {
    			log.Printf("health status changed to %s", state.Status)
    		}),
    	)
    
    	http.Handle("/health", health.NewHandler(checker))
    	log.Fatalln(http.ListenAndServe(":3000", nil))
    }
  5. Reuse existing Go health check libraries with health

    main

    The health library is designed to be compatible with existing Go health check implementations (e.g., for Redis, Postgres, etc.) instead of requiring you to rewrite them. You can wrap existing check functions or objects into a health.Check struct and register them using health.WithCheck.

    Common compatible libraries include:

    • github.com/hellofresh/health-go
    • github.com/etherlabsio/healthcheck
    • github.com/heptiolabs/healthcheck
    • github.com/InVisionApp/go-health
    // Example: Integrating hellofresh/health-go
    import httpCheck "github.com/hellofresh/health-go/v4/checks/http"
    
    health.WithCheck(health.Check{
       Name:    "google",
       Check:   httpCheck.New(httpCheck.Config{
          URL: "https://www.google.com",
       }),
    })
  6. Understand CheckState and its lifecycle

    main

    The CheckState struct tracks the historical and current status of an individual component check:

    • Status: Current AvailabilityStatus.
    • LastCheckedAt: Timestamp of the last execution.
    • LastSuccessAt: Timestamp of the last successful execution (no error).
    • LastFailureAt: Timestamp of the last failed execution.
    • FirstCheckStartedAt: When the component was first monitored.
    • ContiguousFails: Number of consecutive failures.
    • Result: The actual error returned by the last check.

    Status transitions to StatusDown are governed by MaxTimeInError and MaxContiguousFails configurations, which prevent flapping by requiring a threshold of failures or a duration of error before declaring a component 'down'.

  7. Implement custom middleware for health handlers

    main

    Middleware allows you to intercept the health check process. A Middleware is a factory function that takes a MiddlewareFunc (the next step in the chain) and returns a new MiddlewareFunc.

    Crucial Requirement: Every middleware must call the next function passed to it. If next is not called, the underlying Checker.Check will never be executed.

    // Example of a middleware that logs the request
    func LoggingMiddleware(next health.MiddlewareFunc) health.MiddlewareFunc {
    	return func(r *http.Request) health.CheckerResult {
    		fmt.Printf("Health check requested: %s\n", r.URL.Path)
    		return next(r)
    	}
    }
    
    // Usage
    handler := health.NewHandler(myChecker, health.WithMiddleware(LoggingMiddleware))
  8. Configure caching for health checks

    main

    To prevent excessive load on dependencies and mitigate DoS attacks, the library caches health results.

    • The default TTL is 1 second.
    • Use health.WithCacheDuration(duration) to customize the TTL.
    • Use health.WithDisabledCache() to disable caching entirely.
  9. Understand CheckerResult and CheckResult data structures

    main

    The library uses two main structures to report health:

    1. CheckerResult: The top-level response from Checker.Check(). It contains:

      • Status: The aggregated AvailabilityStatus of the entire system.
      • Details: A map of CheckResult objects, keyed by component name.
      • Info: A map of additional metadata.
    2. CheckResult: The status of an individual component. It contains:

      • Status: The AvailabilityStatus of that specific component.
      • Timestamp: When the check was last executed.
      • Error: The error returned by the check (if any).

    Note: CheckResult implements custom JSON marshalling to ensure the error interface is correctly serialized as a string.

  10. Add health checks to a Checker

    main

    There are two ways to add checks to a Checker depending on your performance and frequency requirements:

    1. On-demand checks (WithCheck / WithChecks): These checks are executed every time Checker.Check is called (e.g., on every incoming HTTP request). Use this for lightweight checks.
    2. Periodic checks (WithPeriodicCheck): These checks run on a fixed schedule in the background. The Checker always returns the last successful result. Use this for expensive or long-running checks to avoid blocking HTTP requests.
    // On-demand check
    checker := health.NewChecker(health.WithCheck(myCheck))
    
    // Periodic check (runs every 30s, starts after 5s delay)
    checker := health.NewChecker(health.WithPeriodicCheck(30*time.Second, 5*time.Second, myCheck))
  11. Integrate github.com/InVisionApp/go-health

    main

    To use InVisionApp/go-health, wrap the checker's .Status() method in a closure that returns the error.

    import "github.com/InVisionApp/go-health/checkers"
    
    // ... setup check ...
    
    health.WithCheck(health.Check{
        Name: "google",
        Check: func(_ context.Context) error {
            _, err := check.Status() 
            return err
        },
    })