mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 21:18:58 +00:00
Getting a skeleton in place for the control service.
This commit is contained in:
parent
2ae0b8c159
commit
097a163cf5
@ -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')
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package nu.marginalia.control.model;
|
||||||
|
|
||||||
|
public record EventLogEntry(
|
||||||
|
String serviceName,
|
||||||
|
String instance,
|
||||||
|
String eventTime,
|
||||||
|
String eventType,
|
||||||
|
String eventMessage)
|
||||||
|
{
|
||||||
|
}
|
@ -7,5 +7,8 @@ public record ServiceHeartbeat(
|
|||||||
double lastSeenMillis,
|
double lastSeenMillis,
|
||||||
boolean alive
|
boolean alive
|
||||||
) {
|
) {
|
||||||
|
public boolean isMissing() {
|
||||||
|
return lastSeenMillis > 10000;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
body {
|
||||||
|
font-family: serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user