Getting a skeleton in place for the control service.

This commit is contained in:
Viktor Lofgren 2023-07-04 18:25:42 +02:00
parent 2ae0b8c159
commit 097a163cf5
9 changed files with 158 additions and 2 deletions

View File

@ -26,6 +26,7 @@ dependencies {
implementation project(':code:common:model')
implementation project(':code:common:service')
implementation project(':code:common:config')
implementation project(':code:common:renderer')
implementation project(':code:common:service-discovery')
implementation project(':code:common:service-client')
implementation project(':code:api:search-api')

View File

@ -4,35 +4,53 @@ import com.google.gson.Gson;
import com.google.inject.Inject;
import nu.marginalia.client.ServiceMonitors;
import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.renderer.MustacheRenderer;
import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.service.server.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Spark;
import java.io.IOException;
import java.util.Map;
public class ControlService extends Service {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Gson gson = GsonFactory.get();
private final ServiceMonitors monitors;
private final MustacheRenderer<Object> indexRenderer;
private final MustacheRenderer<Map<?,?>> servicesRenderer;
@Inject
public ControlService(BaseServiceParams params,
ServiceMonitors monitors,
HeartbeatService heartbeatService
) {
HeartbeatService heartbeatService,
EventLogService eventLogService,
RendererFactory rendererFactory
) throws IOException {
super(params);
this.monitors = monitors;
indexRenderer = rendererFactory.renderer("control/index");
servicesRenderer = rendererFactory.renderer("control/services");
Spark.get("/public/heartbeats", (req, res) -> {
res.type("application/json");
return heartbeatService.getHeartbeats();
}, gson::toJson);
Spark.get("/public/", (req, rsp) -> indexRenderer.render(Map.of()));
Spark.get("/public/services", (req, rsp) -> servicesRenderer.render(
Map.of("heartbeats", heartbeatService.getHeartbeats(),
"events", eventLogService.getLastEntries(100)
)));
monitors.subscribe(this::logMonitorStateChange);
}
private void logMonitorStateChange() {

View File

@ -0,0 +1,49 @@
package nu.marginalia.control;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.control.model.EventLogEntry;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Singleton
public class EventLogService {
private final HikariDataSource dataSource;
@Inject
public EventLogService(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public List<EventLogEntry> getLastEntries(int n) {
try (var conn = dataSource.getConnection();
var query = conn.prepareStatement("""
SELECT SERVICE_NAME, INSTANCE, EVENT_TIME, EVENT_TYPE, EVENT_MESSAGE
FROM PROC_SERVICE_EVENTLOG ORDER BY ID DESC LIMIT ?
""")) {
query.setInt(1, n);
List<EventLogEntry> entries = new ArrayList<>(n);
var rs = query.executeQuery();
while (rs.next()) {
entries.add(new EventLogEntry(
rs.getString("SERVICE_NAME"),
rs.getString("INSTANCE"),
rs.getTimestamp("EVENT_TIME").toLocalDateTime().toLocalTime().toString(),
rs.getString("EVENT_TYPE"),
rs.getString("EVENT_MESSAGE")
));
}
return entries;
}
catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@ -0,0 +1,10 @@
package nu.marginalia.control.model;
public record EventLogEntry(
String serviceName,
String instance,
String eventTime,
String eventType,
String eventMessage)
{
}

View File

@ -7,5 +7,8 @@ public record ServiceHeartbeat(
double lastSeenMillis,
boolean alive
) {
public boolean isMissing() {
return lastSeenMillis > 10000;
}
}

View File

@ -0,0 +1,4 @@
body {
font-family: serif;
line-height: 1.6;
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
<viewport content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
{{> control/partials/nav}}
<section>
<h1>Overview</h1>
</section>
</body>
</html>

View File

@ -0,0 +1,7 @@
<nav>
<ul>
<li><a href="/">Overview</a></li>
<li><a href="services">Services</a></li>
<li><a href="processes">Processes</a></li>
</ul>
</nav>

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
<viewport content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
{{> control/partials/nav}}
<section>
<h1>Services</h1>
<table id="heartbeats">
<tr>
<th>Service ID</th>
<th>UUID</th>
<th>Last Seen (ms)</th>
</tr>
{{#each heartbeats}}
<tr class="{{#if isMissing}}missing{{/if}} {{#unless alive}}terminated{{/unless}}">
<td>{{serviceId}}</td>
<td>{{uuid}}</td>
<td>{{lastSeenMillis}}</td>
</tr>
{{/each}}
</table>
<h2>Events</h2>
<table>
<tr>
<th>Service Name</th>
<th>Instance</th>
<th>Event Time</th>
<th>Type</th>
<th>Message</th>
</tr>
{{#each events}}
<tr>
<td>{{serviceName}}</td>
<td>{{instance}}</td>
<td>{{eventTime}}</td>
<td>{{eventType}}</td>
<td>{{eventMessage}}</td>
</tr>
{{/each}}
</table>
</section>
</body>
</html>