Health Aggregator Service

A combined health check system that integrates dependency checks (database, Redis, external APIs), a circuit breaker to prevent cascading timeouts when dependencies fail, and a health aggregation layer for multi-service monitoring. The circuit breaker opens after consecutive failures and retries after a cooldown period.

package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/redis/go-redis/v9"
)

// --- Health Check Types ---

type Status string

const (
	StatusUp       Status = "up"
	StatusDown     Status = "down"
	StatusDegraded Status = "degraded"
)

type CheckResult struct {
	Name     string        `json:"name"`
	Status   Status        `json:"status"`
	Duration time.Duration `json:"duration_ms"`
	Error    string        `json:"error,omitempty"`
}

type HealthResponse struct {
	Status  Status        `json:"status"`
	Checks  []CheckResult `json:"checks,omitempty"`
	Version string        `json:"version,omitempty"`
}

type Check struct {
	Name    string
	Fn      func() error
	Timeout time.Duration
}

type Checker struct {
	checks []Check
	mu     sync.RWMutex
}

func NewChecker() *Checker {
	return &Checker{}
}

func (c *Checker) AddCheck(name string, fn func() error, timeout time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.checks = append(c.checks, Check{Name: name, Fn: fn, Timeout: timeout})
}

func (c *Checker) RunChecks() HealthResponse {
	c.mu.RLock()
	checks := make([]Check, len(c.checks))
	copy(checks, c.checks)
	c.mu.RUnlock()

	results := make([]CheckResult, len(checks))
	var wg sync.WaitGroup

	for i, check := range checks {
		wg.Add(1)
		go func(idx int, chk Check) {
			defer wg.Done()
			start := time.Now()

			errCh := make(chan error, 1)
			go func() { errCh <- chk.Fn() }()

			select {
			case err := <-errCh:
				result := CheckResult{
					Name:     chk.Name,
					Duration: time.Since(start),
					Status:   StatusUp,
				}
				if err != nil {
					result.Status = StatusDown
					result.Error = err.Error()
				}
				results[idx] = result
			case <-time.After(chk.Timeout):
				results[idx] = CheckResult{
					Name:     chk.Name,
					Status:   StatusDown,
					Duration: chk.Timeout,
					Error:    "timeout",
				}
			}
		}(i, check)
	}

	wg.Wait()

	overall := StatusUp
	for _, r := range results {
		if r.Status == StatusDown {
			overall = StatusDown
			break
		}
	}

	return HealthResponse{Status: overall, Checks: results}
}

func (c *Checker) RunChecksWithDegradation(critical []string) HealthResponse {
	resp := c.RunChecks()

	criticalSet := make(map[string]bool)
	for _, name := range critical {
		criticalSet[name] = true
	}

	overall := StatusUp
	for _, check := range resp.Checks {
		if check.Status == StatusDown {
			if criticalSet[check.Name] {
				overall = StatusDown
				break
			}
			overall = StatusDegraded
		}
	}
	resp.Status = overall
	return resp
}

// --- Dependency Checks ---

func DatabaseCheck(db *sql.DB) func() error {
	return func() error {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		return db.PingContext(ctx)
	}
}

func RedisCheck(client *redis.Client) func() error {
	return func() error {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		return client.Ping(ctx).Err()
	}
}

func HTTPCheck(url string, expectedStatus int) func() error {
	client := &http.Client{Timeout: 5 * time.Second}
	return func() error {
		resp, err := client.Get(url)
		if err != nil {
			return err
		}
		defer resp.Body.Close()
		if resp.StatusCode != expectedStatus {
			return fmt.Errorf("expected %d, got %d", expectedStatus, resp.StatusCode)
		}
		return nil
	}
}

// --- Circuit Breaker ---

type State int

const (
	Closed   State = iota // Normal operation: requests pass through
	Open                  // Failing: requests blocked
	HalfOpen              // Testing: one request allowed through
)

var ErrCircuitOpen = errors.New("circuit breaker is open")

type CircuitBreaker struct {
	mu           sync.Mutex
	state        State
	failures     int
	threshold    int
	lastFailure  time.Time
	resetTimeout time.Duration
}

func NewCircuitBreaker(threshold int, resetTimeout time.Duration) *CircuitBreaker {
	return &CircuitBreaker{
		state:        Closed,
		threshold:    threshold,
		resetTimeout: resetTimeout,
	}
}

func (cb *CircuitBreaker) Execute(fn func() error) error {
	cb.mu.Lock()

	switch cb.state {
	case Open:
		if time.Since(cb.lastFailure) > cb.resetTimeout {
			cb.state = HalfOpen
			cb.mu.Unlock()
			return cb.tryExec(fn)
		}
		cb.mu.Unlock()
		return ErrCircuitOpen

	case HalfOpen:
		cb.mu.Unlock()
		return cb.tryExec(fn)

	default: // Closed
		cb.mu.Unlock()
		return cb.tryExec(fn)
	}
}

func (cb *CircuitBreaker) tryExec(fn func() error) error {
	err := fn()

	cb.mu.Lock()
	defer cb.mu.Unlock()

	if err != nil {
		cb.failures++
		cb.lastFailure = time.Now()
		if cb.failures >= cb.threshold {
			cb.state = Open
		}
		return err
	}

	// Success: reset
	cb.failures = 0
	cb.state = Closed
	return nil
}

// --- Health Aggregation ---

type ServiceHealth struct {
	Name   string `json:"name"`
	URL    string `json:"-"`
	Status Status `json:"status"`
	Error  string `json:"error,omitempty"`
}

func AggregateHealth(services []ServiceHealth) Status {
	downCount := 0
	for _, svc := range services {
		if svc.Status == StatusDown {
			downCount++
		}
	}

	switch {
	case downCount == 0:
		return StatusUp
	case downCount == len(services):
		return StatusDown
	default:
		return StatusDegraded
	}
}

// --- Main: Wire It All Together ---

func main() {
	checker := NewChecker()

	// Example: wrap database check with circuit breaker
	dbBreaker := NewCircuitBreaker(3, 30*time.Second)
	checker.AddCheck("postgres", func() error {
		return dbBreaker.Execute(func() error {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			// In production: return db.PingContext(ctx)
			_ = ctx
			return nil
		})
	}, 3*time.Second)

	mux := http.NewServeMux()

	// Liveness: is the process alive?
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status":"up"}`))
	})

	// Readiness: can we serve traffic?
	mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
		resp := checker.RunChecksWithDegradation([]string{"postgres"})
		status := http.StatusOK
		if resp.Status == StatusDown {
			status = http.StatusServiceUnavailable
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		json.NewEncoder(w).Encode(resp)
	})

	// Aggregation endpoint for multi-service monitoring
	mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
		services := []ServiceHealth{
			{Name: "api", Status: StatusUp},
			{Name: "worker", Status: StatusUp},
			{Name: "scheduler", Status: StatusDegraded},
		}
		overall := AggregateHealth(services)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"status":   overall,
			"services": services,
		})
	})

	fmt.Println("Health service listening on :8080")
	http.ListenAndServe(":8080", mux)
}

The service exposes three endpoints: /healthz (liveness), /readyz (readiness with degradation awareness), and /status (multi-service aggregation). The circuit breaker prevents cascading failures when a dependency is down.

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy