Skip to content

Commit 8b2b644

Browse files
authored
Feature/health check plugin + Metric Plugin (#105)
* Add HealthCheckPlugin for server status endpoint * Add HealthCheckPlugin for server status endpoint * Refactor HealthCheckPlugin to use SERVER_NAME constant and optimize response handling * Add build metadata, memory stats, and response time to HealthCheckPlugin responses Integrate `build.properties` for versioning and metadata. Enhance `/health` endpoint with response time, memory usage, and build details. Add UI widget for health status and dynamic frontend updates. * Add build metadata, memory stats, and response time to HealthCheckPlugin responses Integrate `build.properties` for versioning and metadata. Enhance `/health` endpoint with response time, memory usage, and build details. Add UI widget for health status and dynamic frontend updates. * Adjust time units and improve UI/UX for health status display Refine response time calculations from milliseconds to microseconds. Update button interaction styles and enhance accuracy of frontend health status presentation. * Refactor HealthCheckPlugin for improved metadata handling and error resilience; update resource filtering and enhance accessibility for health status UI * Migrate HealthCheckPlugin functionalities to MetricPlugin and update related routes, tests, and UI for enhanced monitoring capabilities. * Migrate HealthCheckPlugin functionalities to MetricPlugin and update related routes, tests, and UI for enhanced monitoring capabilities. * Update response time unit from milliseconds to microseconds across backend and frontend * Update MetricPlugin with improved logging, adjusted response time unit, and frontend status enhancements
1 parent 251a3a8 commit 8b2b644

File tree

11 files changed

+364
-6
lines changed

11 files changed

+364
-6
lines changed

build.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build.commit=1c8d31d
2+
build.version=1.0-SNAPSHOT
3+
build.time=2026-02-20T22:14:43Z

pom.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@
88
<artifactId>JavaHttpServer</artifactId>
99
<version>1.0-SNAPSHOT</version>
1010

11+
<scm>
12+
<connection>scm:git:https://github.com/ithsjava25/project-webserver-juv25d.git</connection>
13+
<developerConnection>scm:git:https://github.com/ithsjava25/project-webserver-juv25d.git</developerConnection>
14+
<url>https://github.com/ithsjava25/project-webserver-juv25d</url>
15+
<tag>HEAD</tag>
16+
</scm>
17+
1118
<properties>
1219
<maven.compiler.release>25</maven.compiler.release>
1320
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1421
<junit.jupiter.version>6.0.2</junit.jupiter.version>
1522
<assertj.core.version>3.27.7</assertj.core.version>
1623
<mockito.version>5.21.0</mockito.version>
24+
<build.time>${maven.build.timestamp}</build.time>
1725
<argLine/>
1826
</properties>
1927
<dependencies>
@@ -176,6 +184,24 @@
176184
</targetTests>
177185
</configuration>
178186
</plugin>
187+
<plugin>
188+
<groupId>org.codehaus.mojo</groupId>
189+
<artifactId>buildnumber-maven-plugin</artifactId>
190+
<version>3.2.0</version>
191+
<executions>
192+
<execution>
193+
<phase>validate</phase>
194+
<goals>
195+
<goal>create</goal>
196+
</goals>
197+
</execution>
198+
</executions>
199+
<configuration>
200+
<doCheck>false</doCheck>
201+
<doUpdate>false</doUpdate>
202+
<shortRevisionLength>7</shortRevisionLength>
203+
</configuration>
204+
</plugin>
179205
<plugin>
180206
<groupId>org.apache.maven.plugins</groupId>
181207
<artifactId>maven-shade-plugin</artifactId>
@@ -200,5 +226,32 @@
200226
</executions>
201227
</plugin>
202228
</plugins>
229+
<pluginManagement>
230+
<plugins>
231+
<plugin>
232+
<groupId>org.codehaus.mojo</groupId>
233+
<artifactId>buildnumber-maven-plugin</artifactId>
234+
<version>3.2.0</version>
235+
<executions>
236+
<execution>
237+
<phase>validate</phase>
238+
<goals>
239+
<goal>create</goal>
240+
</goals>
241+
</execution>
242+
</executions>
243+
</plugin>
244+
</plugins>
245+
</pluginManagement>
246+
<resources>
247+
<resource>
248+
<directory>src/main/resources</directory>
249+
<filtering>false</filtering>
250+
</resource>
251+
<resource>
252+
<directory>src/main/resources-filtered</directory>
253+
<filtering>true</filtering>
254+
</resource>
255+
</resources>
203256
</build>
204257
</project>

src/main/java/org/juv25d/App.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import org.juv25d.filter.*;
44
import org.juv25d.logging.ServerLogging;
55
import org.juv25d.http.HttpParser;
6-
import org.juv25d.plugin.NotFoundPlugin;
6+
import org.juv25d.plugin.HealthCheckPlugin;
7+
import org.juv25d.plugin.MetricPlugin;
8+
import org.juv25d.plugin.NotFoundPlugin; // New import
79
import org.juv25d.plugin.StaticFilesPlugin;
810
import org.juv25d.router.SimpleRouter;
911
import org.juv25d.util.ConfigLoader;
@@ -39,9 +41,11 @@ public static void main(String[] args) {
3941

4042

4143
SimpleRouter router = new SimpleRouter();
42-
router.registerPlugin("/", new StaticFilesPlugin());
43-
router.registerPlugin("/*", new StaticFilesPlugin());
44-
router.registerPlugin("/notfound", new NotFoundPlugin());
44+
router.registerPlugin("/metric", new MetricPlugin()); //Register MetricPlugin for a specified path
45+
router.registerPlugin("/health", new HealthCheckPlugin()); //Register HealthCheckPlugin for a specified path
46+
router.registerPlugin("/", new StaticFilesPlugin()); // Register StaticFilesPlugin for the root path
47+
router.registerPlugin("/*", new StaticFilesPlugin()); // Register StaticFilesPlugin for all paths
48+
router.registerPlugin("/notfound", new NotFoundPlugin()); // Example: Register NotFoundPlugin for a specific path
4549

4650
pipeline.setRouter(router);
4751

src/main/java/org/juv25d/http/HttpRequest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@ public record HttpRequest(
99
String httpVersion,
1010
Map<String, String> headers,
1111
byte[] body,
12-
String remoteIp
13-
) {}
12+
String remoteIp,
13+
long creationTimeNanos
14+
) {
15+
public HttpRequest(String method, String path, String queryString, String httpVersion, Map<String, String> headers, byte[] body, String remoteIp) {
16+
this(method, path, queryString, httpVersion, headers, body, remoteIp, System.nanoTime());
17+
}
18+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.juv25d.plugin;
2+
3+
import org.juv25d.http.HttpRequest;
4+
import org.juv25d.http.HttpResponse;
5+
6+
import java.io.IOException;
7+
import java.nio.charset.StandardCharsets;
8+
9+
/**
10+
* HealthCheckPlugin provides a simple JSON endpoint to verify the server's status.
11+
* Responds to /health by default.
12+
*/
13+
public class HealthCheckPlugin implements Plugin {
14+
15+
16+
@Override
17+
public void handle(HttpRequest req, HttpResponse res) throws IOException {
18+
19+
20+
String jsonBody = """
21+
{
22+
"status": "UP"
23+
}
24+
""";
25+
26+
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
27+
28+
res.setStatusCode(200);
29+
res.setHeader("Content-Type", "application/json");
30+
res.setHeader("Content-Length", String.valueOf(bodyBytes.length));
31+
res.setBody(bodyBytes);
32+
}
33+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.juv25d.plugin;
2+
3+
import org.juv25d.http.HttpRequest;
4+
import org.juv25d.http.HttpResponse;
5+
import org.juv25d.logging.ServerLogging;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.nio.charset.StandardCharsets;
10+
import java.time.ZoneId;
11+
import java.time.ZonedDateTime;
12+
import java.time.format.DateTimeFormatter;
13+
import java.util.Properties;
14+
import java.util.logging.Logger;
15+
16+
public class MetricPlugin implements Plugin {
17+
18+
19+
private static final String SERVER_NAME = "juv25d-webserver";
20+
private static final DateTimeFormatter TIME_FORMAT =
21+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
22+
23+
private final String version;
24+
private final String commit;
25+
26+
public MetricPlugin() {
27+
Logger logger = ServerLogging.getLogger();
28+
Properties props = new Properties();
29+
try (InputStream is = getClass().getClassLoader().getResourceAsStream("build.properties")) {
30+
if (is != null) {
31+
props.load(is);
32+
}
33+
} catch (IOException e) {
34+
logger.warning("Error loading build.properties: " + e.getMessage());
35+
}
36+
this.version = escapeJson(props.getProperty("build.version", "dev"));
37+
String commitValue = props.getProperty("build.commit");
38+
if (commitValue == null || commitValue.isBlank()) {
39+
this.commit = "unknown";
40+
} else {
41+
this.commit = escapeJson(commitValue);
42+
}
43+
}
44+
45+
private String escapeJson(String value) {
46+
if (value == null) {
47+
return "";
48+
}
49+
return value.replace("\\", "\\\\").replace("\"", "\\\"");
50+
}
51+
52+
@Override
53+
public void handle(HttpRequest req, HttpResponse res) throws IOException {
54+
55+
Runtime runtime = Runtime.getRuntime();
56+
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
57+
long maxMemory = runtime.maxMemory();
58+
long responseTimeUs =
59+
(System.nanoTime() - req.creationTimeNanos()) / 1_000;
60+
String localTime = ZonedDateTime
61+
.now(ZoneId.systemDefault())
62+
.format(TIME_FORMAT);
63+
String utcTime = ZonedDateTime
64+
.now(ZoneId.of("UTC"))
65+
.format(TIME_FORMAT);
66+
67+
String jsonBody = String.format("""
68+
{
69+
"localTime": "%s",
70+
"utcTime": "%s",
71+
"server": "%s",
72+
"buildVersion": "%s",
73+
"gitCommit": "%s",
74+
"responseTimeUs": %d,
75+
"memory": {
76+
"usedBytes": %d,
77+
"maxBytes": %d
78+
}
79+
}
80+
""",
81+
localTime,
82+
utcTime,
83+
SERVER_NAME,
84+
version,
85+
commit,
86+
responseTimeUs,
87+
usedMemory,
88+
maxMemory
89+
);
90+
91+
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
92+
93+
res.setStatusCode(200);
94+
res.setHeader("Content-Type", "application/json");
95+
res.setHeader("Content-Length", String.valueOf(bodyBytes.length));
96+
res.setBody(bodyBytes);
97+
}
98+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build.version=${project.version}
2+
build.commit=${buildNumber}
3+
build.time=${build.time}

src/main/resources/static/css/styles.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,61 @@ body {
3030
font-weight: bold;
3131
font-family: system-ui;
3232
}
33+
/* Health box */
34+
35+
#health-box {
36+
position: fixed;
37+
top: 10px;
38+
right: 10px;
39+
font-family: monospace;
40+
z-index: 9999;
41+
}
42+
43+
#health-box button {
44+
background: rgba(34, 34, 34, 1);
45+
color: white;
46+
border: none;
47+
border-radius: 50%;
48+
width: 36px;
49+
height: 36px;
50+
cursor: pointer;
51+
font-size: 1.2rem;
52+
transition: 150ms ease;
53+
anchor-name: --health-button;
54+
}
55+
56+
#health-box button:hover {
57+
box-shadow: 0 0 4px 0px rgba(255, 255, 255, 0.2);
58+
}
59+
60+
#health-box button:active {
61+
font-size: 1.4rem;
62+
}
63+
64+
.health-content {
65+
visibility: hidden;
66+
opacity: 0;
67+
background: #111;
68+
color: #00eaff;
69+
padding: 10px;
70+
border-radius: 6px;
71+
font-size: 12px;
72+
position: absolute;
73+
margin-right: 8px;
74+
inset: auto;
75+
top: anchor(top);
76+
right: anchor(left);
77+
position-anchor: --health-button;
78+
filter: grayscale(100%);
79+
transition: 250ms ease-in-out allow-discrete;
80+
}
81+
82+
.health-content:popover-open {
83+
animation: fade-in 350ms ease;
84+
visibility: visible;
85+
opacity: 1;
86+
filter: grayscale(0%);
87+
}
3388

3489
/* Container */
3590
.container {
@@ -388,3 +443,19 @@ code {
388443
font-size: 1.5em;
389444
}
390445
}
446+
447+
/* Keyframes */
448+
@keyframes fade-in {
449+
from {
450+
opacity: 0;
451+
transform: translateX(1rem);
452+
visibility: hidden;
453+
filter: grayscale(100%);
454+
}
455+
to {
456+
opacity: 1;
457+
transform: translateX(0rem);
458+
visibility: visible;
459+
filter: grayscale(0%);
460+
}
461+
}

src/main/resources/static/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@
77
<link rel="stylesheet" href="/css/styles.css">
88
</head>
99
<body>
10+
<div id="health-box">
11+
<button popovertarget="health-content" aria-label="Show server health status">🩺</button>
12+
13+
<div popover class="health-content" id="health-content">
14+
<div>Local Time: <span id="local-time"></span></div>
15+
<div>UTC Time: <span id="utc-time"></span></div>
16+
<div>Version: <span id="health-version"></span></div>
17+
<div>Commit: <span id="health-commit"></span></div>
18+
<div>Response: <span id="health-response"></span></div>
19+
<div>Memory: <span id="health-memory"></span></div>
20+
</div>
21+
</div>
22+
1023
<div class="container">
1124
<nav class="nav-menu">
1225
<ul>
@@ -46,5 +59,6 @@ <h2>Features</h2>
4659
<script src="/js/marked.min.js"></script>
4760
<script src="js/purify.min.js"></script>
4861
<script src="/js/app.js"></script>
62+
<script src="/js/metric.js"></script>
4963
</body>
5064
</html>

0 commit comments

Comments
 (0)