javaintermediate

Health Check — Liveness and Readiness

Implement health check endpoints for liveness and readiness probes with dependency status reporting.

java
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;

public class HealthCheck {

    enum Status { UP, DOWN, DEGRADED }

    record ComponentHealth(String name, Status status, String detail, Duration responseTime) {}

    record HealthReport(Status overall, Instant timestamp,
                        List<ComponentHealth> components) {
        String toJson() {
            var sb = new StringBuilder();
            sb.append(String.format(
                "{\"status\":\"%s\",\"timestamp\":\"%s\",\"components\":[",
                overall, timestamp));
            for (int i = 0; i < components.size(); i++) {
                var c = components.get(i);
                if (i > 0) sb.append(",");
                sb.append(String.format(
                    "{\"name\":\"%s\",\"status\":\"%s\",\"detail\":\"%s\",\"responseTimeMs\":%d}",
                    c.name, c.status, c.detail, c.responseTime.toMillis()));
            }
            sb.append("]}");
            return sb.toString();
        }
    }

    private final Map<String, Supplier<ComponentHealth>> checks = new LinkedHashMap<>();

    // Register a health check
    public void register(String name, Supplier<ComponentHealth> check) {
        checks.put(name, check);
    }

    // Helper to create timed checks
    public static Supplier<ComponentHealth> timedCheck(String name, Callable<String> check) {
        return () -> {
            Instant start = Instant.now();
            try {
                String detail = check.call();
                Duration elapsed = Duration.between(start, Instant.now());
                return new ComponentHealth(name, Status.UP, detail, elapsed);
            } catch (Exception e) {
                Duration elapsed = Duration.between(start, Instant.now());
                return new ComponentHealth(name, Status.DOWN, e.getMessage(), elapsed);
            }
        };
    }

    // Run all checks
    public HealthReport check() {
        List<ComponentHealth> results = checks.values().stream()
            .map(Supplier::get).toList();

        Status overall = results.stream().anyMatch(c -> c.status == Status.DOWN)
            ? Status.DOWN
            : results.stream().anyMatch(c -> c.status == Status.DEGRADED)
                ? Status.DEGRADED : Status.UP;

        return new HealthReport(overall, Instant.now(), results);
    }

    // Expose as HTTP endpoint
    public void startServer(int port) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);

        server.createContext("/health", exchange -> {
            HealthReport report = check();
            int code = report.overall == Status.UP ? 200 : 503;
            byte[] body = report.toJson().getBytes();
            exchange.getResponseHeaders().set("Content-Type", "application/json");
            exchange.sendResponseHeaders(code, body.length);
            exchange.getResponseBody().write(body);
            exchange.close();
        });

        server.createContext("/health/live", exchange -> {
            byte[] body = "{\"status\":\"UP\"}".getBytes();
            exchange.getResponseHeaders().set("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, body.length);
            exchange.getResponseBody().write(body);
            exchange.close();
        });

        server.start();
        System.out.println("Health server on port " + port);
    }

    public static void main(String[] args) throws Exception {
        HealthCheck health = new HealthCheck();

        health.register("database", timedCheck("database", () -> {
            Thread.sleep(10); // simulate DB ping
            return "PostgreSQL 15 — connected";
        }));

        health.register("cache", timedCheck("cache", () -> {
            Thread.sleep(5);
            return "Redis 7 — 128MB used";
        }));

        health.register("disk", timedCheck("disk", () -> {
            long free = new File("/").getFreeSpace() / (1024 * 1024);
            return free + " MB free";
        }));

        HealthReport report = health.check();
        System.out.println(report.toJson());

        health.startServer(8081);
    }
}

Sponsored

Railway

Use Cases

  • Kubernetes liveness and readiness probes
  • Service dependency monitoring
  • Operational health dashboards

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.