From 9339412f286780bf996091c709d6626cc52ddc6a Mon Sep 17 00:00:00 2001 From: Ryan Slominski Date: Tue, 9 Jun 2026 17:32:41 -0400 Subject: [PATCH 1/2] Add simple healthcheck Fixes #23 --- .../epics2web/controller/Healthcheck.java | 100 ++++++++++++++++++ .../jlab/epics2web/epics/ChannelMonitor.java | 2 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/jlab/epics2web/controller/Healthcheck.java 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..df4a52c --- /dev/null +++ b/src/main/java/org/jlab/epics2web/controller/Healthcheck.java @@ -0,0 +1,100 @@ +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 org.jlab.epics2web.Application; +import org.jlab.epics2web.epics.ChannelManager; +import org.jlab.epics2web.epics.ChannelMonitor; + +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; + +/** + * 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; From 207a0834e313add3615f3bf6698f10eacb62a951 Mon Sep 17 00:00:00 2001 From: Ryan Slominski Date: Tue, 9 Jun 2026 17:33:25 -0400 Subject: [PATCH 2/2] format --- .../epics2web/controller/Healthcheck.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jlab/epics2web/controller/Healthcheck.java b/src/main/java/org/jlab/epics2web/controller/Healthcheck.java index df4a52c..e11e012 100644 --- a/src/main/java/org/jlab/epics2web/controller/Healthcheck.java +++ b/src/main/java/org/jlab/epics2web/controller/Healthcheck.java @@ -8,10 +8,6 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.jlab.epics2web.Application; -import org.jlab.epics2web.epics.ChannelManager; -import org.jlab.epics2web.epics.ChannelMonitor; - import java.io.IOException; import java.io.PrintWriter; import java.time.Duration; @@ -19,11 +15,13 @@ 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). + * Controller for Healthcheck page. Return 200 OK, for healthy Return 503 Service Unavailable for + * unhealthy (or ANYTHING non 200-299). * * @author slominskir */ @@ -55,21 +53,24 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) JsonArrayBuilder unhealthyChannelArray = Json.createArrayBuilder(); - for(Map.Entry entry : monitorMap.entrySet()) { + 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) { + // 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)) { + 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)); + unhealthyChannel.add( + "disconnected_minutes", String.format("%.1f", differenceInSeconds / 60.0)); unhealthyChannelArray.add(unhealthyChannel); } }