Canvas: Rework monitoring page
Display worker statuses with list of commands
Commit hash
Change-Id: I7054ecc5ce81f35cad3fe26fc20677b6f50d3147
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index aa25383..de18582 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -5,12 +5,12 @@
import { GithubClient } from "./github";
import { AppManager } from "./app_manager";
import { z } from "zod";
+import { ProjectMonitor, WorkerSchema } from "./project_monitor";
const db = new PrismaClient();
const appManager = new AppManager();
-const workers = new Map<number, string[]>();
-const logs = new Map<number, Map<string, string>>();
+const projectMonitors = new Map<number, ProjectMonitor>();
const handleProjectCreate: express.Handler = async (req, resp) => {
try {
@@ -486,8 +486,18 @@
resp.write(JSON.stringify({ error: "Project not found" }));
return;
}
- const projectLogs = logs.get(projectId) || new Map();
- const services = Array.from(projectLogs.keys());
+ const monitor = projectMonitors.get(projectId);
+ const serviceNames = monitor ? monitor.getAllServiceNames() : [];
+ const services = serviceNames.map((name) => ({
+ name,
+ workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
+ ([id, status]) => ({
+ ...status,
+ id,
+ }),
+ ),
+ }));
+
resp.status(200);
resp.write(
JSON.stringify({
@@ -540,6 +550,7 @@
try {
const projectId = Number(req.params["projectId"]);
const service = req.params["service"];
+ const workerId = req.params["workerId"];
const project = await db.project.findUnique({
where: {
id: projectId,
@@ -551,16 +562,16 @@
resp.write(JSON.stringify({ error: "Project not found" }));
return;
}
- const projectLogs = logs.get(projectId);
- if (!projectLogs) {
+ const monitor = projectMonitors.get(projectId);
+ if (!monitor || !monitor.hasLogs()) {
resp.status(404);
resp.write(JSON.stringify({ error: "No logs found for this project" }));
return;
}
- const serviceLog = projectLogs.get(service);
+ const serviceLog = monitor.getWorkerLog(service, workerId);
if (!serviceLog) {
resp.status(404);
- resp.write(JSON.stringify({ error: "No logs found for this service" }));
+ resp.write(JSON.stringify({ error: "No logs found for this service/worker" }));
return;
}
resp.status(200);
@@ -574,12 +585,6 @@
}
};
-const WorkerSchema = z.object({
- service: z.string(),
- address: z.string().url(),
- logs: z.optional(z.string()),
-});
-
const handleRegisterWorker: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
@@ -594,17 +599,12 @@
);
return;
}
- const { service, address, logs: log } = result.data;
- const projectWorkers = workers.get(projectId) || [];
- if (!projectWorkers.includes(address)) {
- projectWorkers.push(address);
+ let monitor = projectMonitors.get(projectId);
+ if (!monitor) {
+ monitor = new ProjectMonitor();
+ projectMonitors.set(projectId, monitor);
}
- workers.set(projectId, projectWorkers);
- if (log) {
- const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
- svcLogs.set(service, log);
- logs.set(projectId, svcLogs);
- }
+ monitor.registerWorker(result.data);
resp.status(200);
resp.write(
JSON.stringify({
@@ -621,7 +621,8 @@
};
async function reloadProject(projectId: number): Promise<boolean> {
- const projectWorkers = workers.get(projectId) || [];
+ const monitor = projectMonitors.get(projectId);
+ const projectWorkers = monitor ? monitor.getWorkerAddresses() : [];
const workerCount = projectWorkers.length;
if (workerCount === 0) {
return true;
@@ -750,7 +751,7 @@
projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
projectRouter.get("/:projectId/env", handleEnv);
projectRouter.post("/:projectId/reload", handleReload);
- projectRouter.get("/:projectId/logs/:service", handleServiceLogs);
+ projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
projectRouter.get("/", handleProjectAll);
projectRouter.post("/", handleProjectCreate);
diff --git a/apps/canvas/back/src/project_monitor.ts b/apps/canvas/back/src/project_monitor.ts
new file mode 100644
index 0000000..6c178f0
--- /dev/null
+++ b/apps/canvas/back/src/project_monitor.ts
@@ -0,0 +1,130 @@
+import { z } from "zod";
+
+export const WorkerSchema = z.object({
+ id: z.string(),
+ service: z.string(),
+ address: z.string().url(),
+ status: z.optional(
+ z.object({
+ repoOK: z.boolean(),
+ commit: z.string(),
+ commands: z.optional(
+ z.array(
+ z.object({
+ command: z.string(),
+ state: z.string(),
+ }),
+ ),
+ ),
+ }),
+ ),
+ logs: z.optional(z.string()),
+});
+
+export type Worker = z.infer<typeof WorkerSchema>;
+
+class ServiceMonitor {
+ private workers: Map<string, string> = new Map();
+ private logs: Map<string, string> = new Map();
+ private statuses: Map<string, Worker["status"]> = new Map();
+
+ constructor(public readonly serviceName: string) {}
+
+ registerWorker(workerId: string, workerAddress: string, workerLog?: string, workerStatus?: Worker["status"]): void {
+ this.workers.set(workerId, workerAddress);
+ if (workerLog) {
+ this.logs.set(workerId, workerLog);
+ }
+ if (workerStatus) {
+ this.statuses.set(workerId, workerStatus);
+ }
+ }
+
+ getWorkerAddress(workerId: string): string | undefined {
+ return this.workers.get(workerId);
+ }
+
+ getWorkerLog(workerId: string): string | undefined {
+ return this.logs.get(workerId);
+ }
+
+ getWorkerStatus(workerId: string): Worker["status"] | undefined {
+ return this.statuses.get(workerId);
+ }
+
+ getAllLogs(): Map<string, string> {
+ return new Map(this.logs);
+ }
+
+ getAllStatuses(): Map<string, Worker["status"]> {
+ return new Map(this.statuses);
+ }
+
+ getWorkerAddresses(): string[] {
+ return Array.from(this.workers.values());
+ }
+
+ getWorkerIds(): string[] {
+ return Array.from(this.workers.keys());
+ }
+
+ hasLogs(): boolean {
+ return this.logs.size > 0;
+ }
+}
+
+export class ProjectMonitor {
+ private serviceMonitors: Map<string, ServiceMonitor> = new Map();
+
+ constructor() {}
+
+ registerWorker(workerData: Worker): void {
+ let serviceMonitor = this.serviceMonitors.get(workerData.service);
+ if (!serviceMonitor) {
+ serviceMonitor = new ServiceMonitor(workerData.service);
+ this.serviceMonitors.set(workerData.service, serviceMonitor);
+ }
+ serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.logs, workerData.status);
+ }
+
+ getWorkerAddresses(): string[] {
+ let allAddresses: string[] = [];
+ for (const serviceMonitor of this.serviceMonitors.values()) {
+ allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
+ }
+ return Array.from(new Set(allAddresses));
+ }
+
+ getWorkerLog(serviceName: string, workerId: string): string | undefined {
+ const serviceMonitor = this.serviceMonitors.get(serviceName);
+ if (serviceMonitor) {
+ return serviceMonitor.getWorkerLog(workerId);
+ }
+ return undefined;
+ }
+
+ getAllServiceNames(): string[] {
+ return Array.from(this.serviceMonitors.keys());
+ }
+
+ hasLogs(): boolean {
+ for (const serviceMonitor of this.serviceMonitors.values()) {
+ if (serviceMonitor.hasLogs()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
+ return this.serviceMonitors.get(serviceName);
+ }
+
+ getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
+ const serviceMonitor = this.serviceMonitors.get(serviceName);
+ if (serviceMonitor) {
+ return serviceMonitor.getAllStatuses();
+ }
+ return new Map();
+ }
+}