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:model')
implementation project(':code:common:service') implementation project(':code:common:service')
implementation project(':code:common:config') implementation project(':code:common:config')
implementation project(':code:common:renderer')
implementation project(':code:common:service-discovery') implementation project(':code:common:service-discovery')
implementation project(':code:common:service-client') implementation project(':code:common:service-client')
implementation project(':code:api:search-api') implementation project(':code:api:search-api')

View File

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