diff --git a/src/main/java/org/jlab/epics2web/controller/Healthcheck.java b/src/main/java/org/jlab/epics2web/controller/Healthcheck.java
new file mode 100644
index 0000000..e11e012
--- /dev/null
+++ b/src/main/java/org/jlab/epics2web/controller/Healthcheck.java
@@ -0,0 +1,101 @@
+package org.jlab.epics2web.controller;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.jlab.epics2web.Application;
+import org.jlab.epics2web.epics.ChannelManager;
+import org.jlab.epics2web.epics.ChannelMonitor;
+
+/**
+ * Controller for Healthcheck page. Return 200 OK, for healthy Return 503 Service Unavailable for
+ * unhealthy (or ANYTHING non 200-299).
+ *
+ * @author slominskir
+ */
+@WebServlet(
+ name = "Healthcheck",
+ urlPatterns = {"/healthcheck"})
+public class Healthcheck extends HttpServlet {
+
+ private final ChannelManager channelManager = Application.channelManager;
+ private static final Logger LOGGER = Logger.getLogger(Healthcheck.class.getName());
+
+ /**
+ * Handles the HTTP GET method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ boolean healthy = true;
+
+ Map monitorMap = channelManager.getMonitorMap();
+
+ Instant now = Instant.now();
+
+ JsonArrayBuilder unhealthyChannelArray = Json.createArrayBuilder();
+
+ for (Map.Entry entry : monitorMap.entrySet()) {
+ String pv = entry.getKey();
+ ChannelMonitor monitor = entry.getValue();
+
+ // If never an update, then we assume PV doesn't exist. Might miss some cases. Better than
+ // nothing health check!
+ if (monitor.getLastTimestamp() != null) {
+ Instant lastTimestamp = monitor.getLastTimestamp().toInstant();
+ Duration duration = Duration.between(now, lastTimestamp);
+ long differenceInSeconds = Math.abs(duration.toSeconds());
+
+ if (monitor.getState() != ChannelMonitor.MonitorState.CONNECTED
+ && (differenceInSeconds > 30)) {
+ healthy = false;
+ JsonObjectBuilder unhealthyChannel = Json.createObjectBuilder();
+ unhealthyChannel.add("name", pv);
+ unhealthyChannel.add(
+ "disconnected_minutes", String.format("%.1f", differenceInSeconds / 60.0));
+ unhealthyChannelArray.add(unhealthyChannel);
+ }
+ }
+ }
+
+ response.setContentType("application/json");
+
+ PrintWriter pw = response.getWriter();
+
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ if (!healthy) {
+ response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ }
+
+ String jsonStr = unhealthyChannelArray.build().toString();
+
+ pw.write(jsonStr);
+
+ pw.flush();
+
+ boolean error = pw.checkError();
+
+ if (error) {
+ LOGGER.log(Level.SEVERE, "PrintWriter Error");
+ }
+ }
+}
diff --git a/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java b/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java
index 62d28a8..8c471aa 100644
--- a/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java
+++ b/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java
@@ -62,7 +62,7 @@ public class ChannelMonitor implements Closeable {
private final ExecutorService callbackExecutor;
private final String pv;
- enum MonitorState {
+ public enum MonitorState {
CONNECTING,
CONNECTED,
DISCONNECTED;