(control) WIP control service

This commit is contained in:
Viktor Lofgren 2023-07-10 18:58:33 +02:00
parent fba466d6e2
commit 2283ceb77d
13 changed files with 267 additions and 34 deletions

View File

@ -11,6 +11,8 @@ import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.service.server.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Spark;
import java.io.IOException;
@ -25,7 +27,10 @@ public class ControlService extends Service {
private final ServiceMonitors monitors;
private final MustacheRenderer<Object> indexRenderer;
private final MustacheRenderer<Map<?,?>> servicesRenderer;
private final MustacheRenderer<Map<?,?>> eventsRenderer;
private final MustacheRenderer<Map<?,?>> messageQueueRenderer;
private final MqPersistence messageQueuePersistence;
private final StaticResources staticResources;
@Inject
@ -35,14 +40,20 @@ public class ControlService extends Service {
EventLogService eventLogService,
RendererFactory rendererFactory,
MqPersistence messageQueuePersistence,
ControlProcesses controlProcesses
ControlProcesses controlProcesses,
StaticResources staticResources,
MessageQueueViewService messageQueueViewService
) throws IOException {
super(params);
this.monitors = monitors;
indexRenderer = rendererFactory.renderer("control/index");
servicesRenderer = rendererFactory.renderer("control/services");
eventsRenderer = rendererFactory.renderer("control/events");
messageQueueRenderer = rendererFactory.renderer("control/message-queue");
this.messageQueuePersistence = messageQueuePersistence;
this.staticResources = staticResources;
Spark.get("/public/heartbeats", (req, res) -> {
res.type("application/json");
@ -50,15 +61,18 @@ public class ControlService extends Service {
}, 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)
)));
Spark.get("/public/services", (req, rsp) -> servicesRenderer.render(Map.of("heartbeats", heartbeatService.getHeartbeats())));
Spark.get("/public/events", (req, rsp) -> eventsRenderer.render(Map.of("events", eventLogService.getLastEntries(20))));
Spark.get("/public/message-queue", (req, rsp) -> messageQueueRenderer.render(Map.of("messages", messageQueueViewService.getLastEntries(20))));
Spark.get("/public/repartition", (req, rsp) -> {
controlProcesses.start("REPARTITION-REINDEX");
return "OK";
});
Spark.get("/public/:resource", this::serveStatic);
monitors.subscribe(this::logMonitorStateChange);
Thread reaperThread = new Thread(this::reapMessageQueue, "message-queue-reaper");
@ -66,6 +80,16 @@ public class ControlService extends Service {
reaperThread.start();
}
private Object serveStatic(Request request, Response response) {
String resource = request.params("resource");
staticResources.serveStatic("control", resource, request, response);
return "";
}
private void reapMessageQueue() {
for (;;) {

View File

@ -32,7 +32,7 @@ public class EventLogService {
while (rs.next()) {
entries.add(new EventLogEntry(
rs.getString("SERVICE_NAME"),
rs.getString("INSTANCE"),
trimUUID(rs.getString("INSTANCE")),
rs.getTimestamp("EVENT_TIME").toLocalDateTime().toLocalTime().toString(),
rs.getString("EVENT_TYPE"),
rs.getString("EVENT_MESSAGE")
@ -44,6 +44,11 @@ public class EventLogService {
throw new RuntimeException(ex);
}
}
private String trimUUID(String uuid) {
if (uuid.length() > 8) {
return uuid.substring(0, 8);
}
return uuid;
}
}

View File

@ -33,7 +33,7 @@ public class HeartbeatService {
heartbeats.add(new ServiceHeartbeat(
rs.getString("SERVICE_NAME"),
rs.getString("SERVICE_BASE"),
rs.getString("INSTANCE"),
trimUUID(rs.getString("INSTANCE")),
rs.getInt("TSDIFF") / 1000.,
rs.getBoolean("ALIVE")
));
@ -45,4 +45,11 @@ public class HeartbeatService {
return heartbeats;
}
private String trimUUID(String uuid) {
if (uuid.length() > 8) {
return uuid.substring(0, 8);
}
return uuid;
}
}

View File

@ -0,0 +1,67 @@
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 nu.marginalia.control.model.MessageQueueEntry;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Singleton
public class MessageQueueViewService {
private final HikariDataSource dataSource;
@Inject
public MessageQueueViewService(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public List<MessageQueueEntry> getLastEntries(int n) {
try (var conn = dataSource.getConnection();
var query = conn.prepareStatement("""
SELECT ID, RELATED_ID, SENDER_INBOX, RECIPIENT_INBOX, FUNCTION, OWNER_INSTANCE, OWNER_TICK, STATE, CREATED_TIME, UPDATED_TIME, TTL
FROM PROC_MESSAGE
ORDER BY ID DESC
LIMIT ?
""")) {
query.setInt(1, n);
List<MessageQueueEntry> entries = new ArrayList<>(n);
var rs = query.executeQuery();
while (rs.next()) {
entries.add(new MessageQueueEntry(
rs.getLong("ID"),
rs.getLong("RELATED_ID"),
rs.getString("SENDER_INBOX"),
rs.getString("RECIPIENT_INBOX"),
rs.getString("FUNCTION"),
trimUUID(rs.getString("OWNER_INSTANCE")),
rs.getLong("OWNER_TICK"),
rs.getString("STATE"),
rs.getTimestamp("CREATED_TIME").toLocalDateTime().toLocalTime().toString(),
rs.getTimestamp("UPDATED_TIME").toLocalDateTime().toLocalTime().toString(),
rs.getInt("TTL")
));
}
return entries;
}
catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
private String trimUUID(String uuid) {
if (null == uuid) {
return "";
}
if (uuid.length() > 8) {
return uuid.substring(0, 8);
}
return uuid;
}
}

View File

@ -0,0 +1,17 @@
package nu.marginalia.control.model;
public record MessageQueueEntry (
long id,
long relatedId,
String senderInbox,
String recipientInbox,
String function,
String ownerInstance,
long ownerTick,
String state,
String createdTime,
String updatedTime,
int ttl
)
{
}

View File

@ -1,4 +1,38 @@
body {
font-family: serif;
line-height: 1.6;
display: grid;
grid-template-columns: 20ch auto;
grid-gap: 1em;
grid-template-areas:
"left right";
}
body > nav {
grid-area: left;
}
nav ul {
list-style-type: none;
padding: 0;
}
nav ul li {
line-height: 2;
}
nav ul li a {
text-decoration: none;
padding: 0.5ch;
display: block;
color: #000;
background-color: #ccc;
}
nav ul li a:focus {
text-decoration: underline;
}
nav ul li a.current {
color: #000;
background-color: #fff;
}
body > section {
grid-area: right;
}

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
{{> control/partials/nav}}
<section>
<h1>Events</h1>
<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>

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>Control Service</title>
<viewport content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
{{> control/partials/nav}}
<section>
<h1>Events</h1>
<table>
<tr>
<th>Message ID</th>
<th>Related ID</th>
<th>Recipient</th>
<th>Sender</th>
<th>Function</th>
<th>Owner Instance</th>
<th>Owner Tick</th>
<th>State</th>
<th>Created Time</th>
<th>Updated Time</th>
<th>TTL</th>
</tr>
{{#each messages}}
<tr>
<td>{{id}}</td>
<td>{{relatedId}}</td>
<td>{{recipientInbox}}</td>
<td>{{senderInbox}}</td>
<td>{{function}}</td>
<td>{{ownerInstance}}</td>
<td>{{ownerTick}}</td>
<td>{{state}}</td>
<td>{{createdTime}}</td>
<td>{{updatedTime}}</td>
<td>{{ttl}}</td>
</tr>
{{/each}}
</table>
</section>
</body>
</html>

View File

@ -2,6 +2,8 @@
<ul>
<li><a href="/">Overview</a></li>
<li><a href="services">Services</a></li>
<li><a href="events">Events</a></li>
<li><a href="message-queue">Message Queue</a></li>
<li><a href="processes">Processes</a></li>
</ul>
</nav>

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>Control Service</title>
<viewport content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
@ -24,27 +24,6 @@
</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>

View File

@ -98,6 +98,7 @@ services:
container_name: "nginx-gw"
ports:
- "127.0.0.1:8080:80"
- "127.0.0.1:8081:81"
volumes:
- "./run/nginx-site.conf:/etc/nginx/conf.d/default.conf"
networks:

View File

@ -33,11 +33,27 @@ server {
proxy_pass http://assistant-service:5025/public$request_uri;
access_log off;
}
location /control/ {
proxy_pass http://control-service:5090/public/;
}
location / {
proxy_pass http://search-service:5023/public/;
}
}
server {
listen 81;
listen [::]:81;
server_name control;
proxy_set_header X-Context $remote_addr-$connection;
proxy_set_header X-Extern-Url $scheme://$host$request_uri;
proxy_set_header X-Extern-Domain $scheme://$host;
proxy_set_header X-User-Agent $http_user_agent;
proxy_set_header X-Public "1";
location / {
proxy_pass http://control-service:5090/public/;
}
}