blob: 0f4fffe66a989ef2ad2a535292a13a79079349ef [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({
gio0afbaee2025-05-22 04:34:33 +000025 commit: z.optional(
26 z.object({
27 hash: z.string(),
28 message: z.string(),
29 }),
30 ),
gioa1efbad2025-05-21 07:16:45 +000031 commands: z.optional(
32 z.array(
33 z.object({
34 command: z.string(),
35 state: z.string(),
36 }),
37 ),
38 ),
39 }),
40 ),
gio78a22882025-07-01 18:56:01 +000041 logs: LogItemsSchema.optional(),
gioa1efbad2025-05-21 07:16:45 +000042});
43
44export type Worker = z.infer<typeof WorkerSchema>;
45
46class ServiceMonitor {
47 private workers: Map<string, string> = new Map();
gio78a22882025-07-01 18:56:01 +000048 private logs: Map<string, LogItem[]> = new Map();
gioa1efbad2025-05-21 07:16:45 +000049 private statuses: Map<string, Worker["status"]> = new Map();
50
51 constructor(public readonly serviceName: string) {}
52
gio78a22882025-07-01 18:56:01 +000053 registerWorker(
54 workerId: string,
55 workerAddress: string,
56 workerLog?: LogItem[],
57 workerStatus?: Worker["status"],
58 ): void {
gioa1efbad2025-05-21 07:16:45 +000059 this.workers.set(workerId, workerAddress);
60 if (workerLog) {
gio78a22882025-07-01 18:56:01 +000061 const existingLogs = this.logs.get(workerId);
62 if (existingLogs) {
63 existingLogs.push(...workerLog);
64 } else {
65 this.logs.set(workerId, workerLog);
66 }
gioa1efbad2025-05-21 07:16:45 +000067 }
68 if (workerStatus) {
69 this.statuses.set(workerId, workerStatus);
70 }
71 }
72
73 getWorkerAddress(workerId: string): string | undefined {
74 return this.workers.get(workerId);
75 }
76
gio78a22882025-07-01 18:56:01 +000077 getWorkerLog(workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +000078 return this.logs.get(workerId);
79 }
80
81 getWorkerStatus(workerId: string): Worker["status"] | undefined {
82 return this.statuses.get(workerId);
83 }
84
gio78a22882025-07-01 18:56:01 +000085 getAllLogs(): Map<string, LogItem[]> {
gioa1efbad2025-05-21 07:16:45 +000086 return new Map(this.logs);
87 }
88
89 getAllStatuses(): Map<string, Worker["status"]> {
90 return new Map(this.statuses);
91 }
92
93 getWorkerAddresses(): string[] {
94 return Array.from(this.workers.values());
95 }
96
97 getWorkerIds(): string[] {
98 return Array.from(this.workers.keys());
99 }
100
101 hasLogs(): boolean {
102 return this.logs.size > 0;
103 }
gio918780d2025-05-22 08:24:41 +0000104
105 async reloadWorker(workerId: string): Promise<void> {
106 const workerAddress = this.workers.get(workerId);
107 if (!workerAddress) {
108 throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
109 }
110 try {
111 const response = await fetch(`${workerAddress}/update`, { method: "POST" });
112 if (!response.ok) {
113 throw new Error(
114 `Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
115 );
116 }
117 console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
118 } catch (error) {
119 console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
120 throw error; // Re-throw to be caught by ProjectMonitor
121 }
122 }
gioa1efbad2025-05-21 07:16:45 +0000123}
124
125export class ProjectMonitor {
126 private serviceMonitors: Map<string, ServiceMonitor> = new Map();
127
128 constructor() {}
129
130 registerWorker(workerData: Worker): void {
131 let serviceMonitor = this.serviceMonitors.get(workerData.service);
132 if (!serviceMonitor) {
133 serviceMonitor = new ServiceMonitor(workerData.service);
134 this.serviceMonitors.set(workerData.service, serviceMonitor);
135 }
136 serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.logs, workerData.status);
137 }
138
139 getWorkerAddresses(): string[] {
140 let allAddresses: string[] = [];
141 for (const serviceMonitor of this.serviceMonitors.values()) {
142 allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
143 }
144 return Array.from(new Set(allAddresses));
145 }
146
gio78a22882025-07-01 18:56:01 +0000147 getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +0000148 const serviceMonitor = this.serviceMonitors.get(serviceName);
149 if (serviceMonitor) {
150 return serviceMonitor.getWorkerLog(workerId);
151 }
152 return undefined;
153 }
154
155 getAllServiceNames(): string[] {
156 return Array.from(this.serviceMonitors.keys());
157 }
158
159 hasLogs(): boolean {
160 for (const serviceMonitor of this.serviceMonitors.values()) {
161 if (serviceMonitor.hasLogs()) {
162 return true;
163 }
164 }
165 return false;
166 }
167
168 getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
169 return this.serviceMonitors.get(serviceName);
170 }
171
172 getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
173 const serviceMonitor = this.serviceMonitors.get(serviceName);
174 if (serviceMonitor) {
175 return serviceMonitor.getAllStatuses();
176 }
177 return new Map();
178 }
gio918780d2025-05-22 08:24:41 +0000179
180 async reloadWorker(serviceName: string, workerId: string): Promise<void> {
181 const serviceMonitor = this.serviceMonitors.get(serviceName);
182 if (!serviceMonitor) {
183 throw new Error(`Service ${serviceName} not found`);
184 }
185 await serviceMonitor.reloadWorker(workerId);
186 }
gioa1efbad2025-05-21 07:16:45 +0000187}