blob: df20e0593de10ec2da1a9f7db95f412880b1e1a1 [file] [log] [blame]
gioa1efbad2025-05-21 07:16:45 +00001import { z } from "zod";
2
gio78a22882025-07-01 18:56:01 +00003const LogItemSchema = z.object({
4 runId: z.string(),
5 timestampMilli: z.number(),
6 commit: z.string().optional(),
7 contents: z.preprocess((val) => {
8 if (typeof val === "string") {
9 return Buffer.from(val, "base64").toString("utf-8");
10 }
11 throw new Error("Log item contents is not a string");
12 }, z.string()),
13});
14
15export type LogItem = z.infer<typeof LogItemSchema>;
16
17const LogItemsSchema = z.array(LogItemSchema);
18
gioa1efbad2025-05-21 07:16:45 +000019export const WorkerSchema = z.object({
20 id: z.string(),
21 service: z.string(),
22 address: z.string().url(),
23 status: z.optional(
24 z.object({
gioa70535a2025-07-02 15:50:25 +000025 commit: z
26 .optional(
27 z.object({
28 hash: z.string(),
29 message: z.string(),
30 }),
31 )
32 .nullable(),
gioa1efbad2025-05-21 07:16:45 +000033 commands: z.optional(
34 z.array(
35 z.object({
36 command: z.string(),
37 state: z.string(),
38 }),
39 ),
40 ),
41 }),
42 ),
gio78a22882025-07-01 18:56:01 +000043 logs: LogItemsSchema.optional(),
gioa1efbad2025-05-21 07:16:45 +000044});
45
46export type Worker = z.infer<typeof WorkerSchema>;
47
48class ServiceMonitor {
49 private workers: Map<string, string> = new Map();
gio78a22882025-07-01 18:56:01 +000050 private logs: Map<string, LogItem[]> = new Map();
gioa1efbad2025-05-21 07:16:45 +000051 private statuses: Map<string, Worker["status"]> = new Map();
52
53 constructor(public readonly serviceName: string) {}
54
gio40c0c992025-07-02 13:18:05 +000055 registerWorker(workerId: string, workerAddress: string, workerStatus?: Worker["status"]): void {
gioa1efbad2025-05-21 07:16:45 +000056 this.workers.set(workerId, workerAddress);
gioa1efbad2025-05-21 07:16:45 +000057 if (workerStatus) {
58 this.statuses.set(workerId, workerStatus);
59 }
60 }
61
62 getWorkerAddress(workerId: string): string | undefined {
63 return this.workers.get(workerId);
64 }
65
gio78a22882025-07-01 18:56:01 +000066 getWorkerLog(workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +000067 return this.logs.get(workerId);
68 }
69
70 getWorkerStatus(workerId: string): Worker["status"] | undefined {
71 return this.statuses.get(workerId);
72 }
73
gio78a22882025-07-01 18:56:01 +000074 getAllLogs(): Map<string, LogItem[]> {
gioa1efbad2025-05-21 07:16:45 +000075 return new Map(this.logs);
76 }
77
78 getAllStatuses(): Map<string, Worker["status"]> {
79 return new Map(this.statuses);
80 }
81
82 getWorkerAddresses(): string[] {
83 return Array.from(this.workers.values());
84 }
85
86 getWorkerIds(): string[] {
87 return Array.from(this.workers.keys());
88 }
89
90 hasLogs(): boolean {
91 return this.logs.size > 0;
92 }
gio918780d2025-05-22 08:24:41 +000093
94 async reloadWorker(workerId: string): Promise<void> {
95 const workerAddress = this.workers.get(workerId);
96 if (!workerAddress) {
97 throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
98 }
99 try {
100 const response = await fetch(`${workerAddress}/update`, { method: "POST" });
101 if (!response.ok) {
102 throw new Error(
103 `Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
104 );
105 }
106 console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
107 } catch (error) {
108 console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
109 throw error; // Re-throw to be caught by ProjectMonitor
110 }
111 }
gioa1efbad2025-05-21 07:16:45 +0000112}
113
114export class ProjectMonitor {
115 private serviceMonitors: Map<string, ServiceMonitor> = new Map();
116
117 constructor() {}
118
119 registerWorker(workerData: Worker): void {
120 let serviceMonitor = this.serviceMonitors.get(workerData.service);
121 if (!serviceMonitor) {
122 serviceMonitor = new ServiceMonitor(workerData.service);
123 this.serviceMonitors.set(workerData.service, serviceMonitor);
124 }
gio40c0c992025-07-02 13:18:05 +0000125 serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.status);
gioa1efbad2025-05-21 07:16:45 +0000126 }
127
128 getWorkerAddresses(): string[] {
129 let allAddresses: string[] = [];
130 for (const serviceMonitor of this.serviceMonitors.values()) {
131 allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
132 }
133 return Array.from(new Set(allAddresses));
134 }
135
gio78a22882025-07-01 18:56:01 +0000136 getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +0000137 const serviceMonitor = this.serviceMonitors.get(serviceName);
138 if (serviceMonitor) {
139 return serviceMonitor.getWorkerLog(workerId);
140 }
141 return undefined;
142 }
143
144 getAllServiceNames(): string[] {
145 return Array.from(this.serviceMonitors.keys());
146 }
147
148 hasLogs(): boolean {
149 for (const serviceMonitor of this.serviceMonitors.values()) {
150 if (serviceMonitor.hasLogs()) {
151 return true;
152 }
153 }
154 return false;
155 }
156
157 getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
158 return this.serviceMonitors.get(serviceName);
159 }
160
161 getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
162 const serviceMonitor = this.serviceMonitors.get(serviceName);
163 if (serviceMonitor) {
164 return serviceMonitor.getAllStatuses();
165 }
166 return new Map();
167 }
gio918780d2025-05-22 08:24:41 +0000168
169 async reloadWorker(serviceName: string, workerId: string): Promise<void> {
170 const serviceMonitor = this.serviceMonitors.get(serviceName);
171 if (!serviceMonitor) {
172 throw new Error(`Service ${serviceName} not found`);
173 }
174 await serviceMonitor.reloadWorker(workerId);
175 }
gioa1efbad2025-05-21 07:16:45 +0000176}