Skip to content

Commit a6268fd

Browse files
committed
Delete file store on cancel action
1 parent 31ff56f commit a6268fd

File tree

6 files changed

+120
-19
lines changed

6 files changed

+120
-19
lines changed

genkit-tools/common/src/manager/manager.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ export class RuntimeManager {
6868
private filenameToDevUiMap: Record<string, DevToolsInfo> = {};
6969
private idToFileMap: Record<string, string> = {};
7070
private eventEmitter = new EventEmitter();
71-
private watchers: chokidar.FSWatcher[] = [];
72-
private healthCheckInterval?: NodeJS.Timeout;
7371

7472
private constructor(
7573
readonly telemetryServerUrl: string | undefined,
@@ -93,27 +91,14 @@ export class RuntimeManager {
9391
await manager.setupRuntimesWatcher();
9492
await manager.setupDevUiWatcher();
9593
if (manager.manageHealth) {
96-
manager.healthCheckInterval = setInterval(
94+
setInterval(
9795
async () => await manager.performHealthChecks(),
9896
HEALTH_CHECK_INTERVAL
9997
);
10098
}
10199
return manager;
102100
}
103101

104-
/**
105-
* Stops the runtime manager and cleans up resources.
106-
*/
107-
async stop() {
108-
if (this.healthCheckInterval) {
109-
clearInterval(this.healthCheckInterval);
110-
}
111-
await Promise.all(this.watchers.map((watcher) => watcher.close()));
112-
if (this.processManager) {
113-
await this.processManager.kill();
114-
}
115-
}
116-
117102
/**
118103
* Lists all active runtimes
119104
*/
@@ -365,6 +350,10 @@ export class RuntimeManager {
365350
},
366351
}
367352
);
353+
354+
// Delete the trace from the telemetry server
355+
await this.deleteTrace(input.traceId);
356+
368357
return response.data;
369358
} catch (err) {
370359
const axiosError = err as AxiosError;
@@ -382,6 +371,18 @@ export class RuntimeManager {
382371
}
383372
}
384373

374+
/**
375+
* Deletes a trace by ID
376+
*/
377+
async deleteTrace(traceId: string): Promise<void> {
378+
try {
379+
await axios.delete(`${this.telemetryServerUrl}/api/traces/${traceId}`);
380+
} catch (err) {
381+
// Log but don't fail - trace deletion is best-effort
382+
logger.debug(`Failed to delete trace ${traceId}: ${err}`);
383+
}
384+
}
385+
385386
/**
386387
* Retrieves all traces
387388
*/
@@ -538,7 +539,6 @@ export class RuntimeManager {
538539
persistent: true,
539540
ignoreInitial: false,
540541
});
541-
this.watchers.push(watcher);
542542
watcher.on('add', (filePath) => this.handleNewRuntime(filePath));
543543
if (this.manageHealth) {
544544
watcher.on('unlink', (filePath) => this.handleRemovedRuntime(filePath));
@@ -563,7 +563,6 @@ export class RuntimeManager {
563563
persistent: true,
564564
ignoreInitial: false,
565565
});
566-
this.watchers.push(watcher);
567566
watcher.on('add', (filePath) => this.handleNewDevUi(filePath));
568567
if (this.manageHealth) {
569568
watcher.on('unlink', (filePath) => this.handleRemovedDevUi(filePath));

genkit-tools/telemetry-server/src/file-trace-store.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,16 @@ export class LocalFileTraceStore implements TraceStore {
197197
};
198198
}
199199

200+
async delete(traceId: string): Promise<void> {
201+
const filePath = path.resolve(this.storeRoot, `${traceId}`);
202+
if (fs.existsSync(filePath)) {
203+
fs.unlinkSync(filePath);
204+
logger.debug(`[Telemetry Server] Deleted trace ${traceId}`);
205+
}
206+
// Index entry is left orphaned but will be filtered out on load
207+
// and cleaned up on next reindex
208+
}
209+
200210
private async listFromFiles(query?: TraceQuery): Promise<TraceQueryResponse> {
201211
const files = fs.readdirSync(this.storeRoot);
202212
files.sort((a, b) => {

genkit-tools/telemetry-server/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ export async function startTelemetryServer(params: {
6565
}
6666
});
6767

68+
api.delete('/api/traces/:traceId', async (request, response, next) => {
69+
try {
70+
const { traceId } = request.params;
71+
await params.traceStore.delete(traceId);
72+
response.status(200).send('OK');
73+
} catch (e) {
74+
next(e);
75+
}
76+
});
77+
6878
// SSE endpoint for live trace streaming
6979
api.get('/api/traces/:traceId/stream', async (request, response, next) => {
7080
try {

genkit-tools/telemetry-server/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ export interface TraceStore {
5050
save(traceId: string, trace: TraceData): Promise<void>;
5151
load(traceId: string): Promise<TraceData | undefined>;
5252
list(query?: TraceQuery): Promise<TraceQueryResponse>;
53+
delete(traceId: string): Promise<void>;
5354
}

genkit-tools/telemetry-server/tests/broadcast_manager_test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ describe('BroadcastManager', () => {
9292
const event = createSpanEvent('trace-1');
9393
manager.broadcast('trace-1', event);
9494

95-
const expectedData = JSON.stringify(event) + '\n\n';
95+
// SSE format: "data: <json>\n\n"
96+
const expectedData = `data: ${JSON.stringify(event)}\n\n`;
9697
assert.strictEqual(response1.writtenData[0], expectedData);
9798
assert.strictEqual(response2.writtenData[0], expectedData);
9899
});

genkit-tools/telemetry-server/tests/file_store_test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,86 @@ describe('local-file-store', () => {
269269
);
270270
});
271271

272+
it('deletes traces', async () => {
273+
const traceData = {
274+
traceId: TRACE_ID,
275+
displayName: 'trace',
276+
spans: { [SPAN_A]: span(TRACE_ID, SPAN_A, 100, 100) },
277+
} as TraceData;
278+
279+
// Create the trace
280+
const createRes = await fetch(`${url}/api/traces`, {
281+
method: 'POST',
282+
headers: { 'Content-Type': 'application/json' },
283+
body: JSON.stringify(traceData),
284+
});
285+
assert.strictEqual(createRes.status, 200);
286+
287+
// Verify it exists
288+
const getRes1 = await fetch(`${url}/api/traces/${TRACE_ID}`);
289+
assert.strictEqual(getRes1.status, 200);
290+
const retrieved = await getRes1.json();
291+
assert.strictEqual(retrieved.traceId, TRACE_ID);
292+
293+
// Delete the trace
294+
const deleteRes = await fetch(`${url}/api/traces/${TRACE_ID}`, {
295+
method: 'DELETE',
296+
});
297+
assert.strictEqual(deleteRes.status, 200);
298+
299+
// Verify it's gone
300+
const getRes2 = await fetch(`${url}/api/traces/${TRACE_ID}`);
301+
assert.strictEqual(getRes2.status, 200);
302+
const responseText = await getRes2.text();
303+
assert.strictEqual(responseText, '');
304+
});
305+
306+
it('deletes trace and filters from list', async () => {
307+
// Create multiple traces
308+
for (let i = 0; i < 3; i++) {
309+
const spanId = `abc_${i}`;
310+
const traceId = TRACE_ID + `_${i}`;
311+
const spanData = span(traceId, spanId, 100 + i, 100 + i);
312+
const trace = {
313+
traceId: traceId,
314+
displayName: 'trace',
315+
spans: { [spanId]: spanData },
316+
};
317+
318+
const res = await fetch(`${url}/api/traces`, {
319+
method: 'POST',
320+
headers: { 'Content-Type': 'application/json' },
321+
body: JSON.stringify(trace),
322+
});
323+
assert.strictEqual(res.status, 200);
324+
await sleep(1);
325+
}
326+
327+
// Delete the middle trace
328+
const deleteRes = await fetch(`${url}/api/traces/${TRACE_ID}_1`, {
329+
method: 'DELETE',
330+
});
331+
assert.strictEqual(deleteRes.status, 200);
332+
333+
// List should only return 2 traces
334+
const listRes = await fetch(`${url}/api/traces`);
335+
assert.strictEqual(listRes.status, 200);
336+
const tracesResponse = await listRes.json();
337+
assert.strictEqual(tracesResponse.traces.length, 2);
338+
assert.ok(
339+
tracesResponse.traces.every(
340+
(t: TraceData) => t.traceId !== `${TRACE_ID}_1`
341+
)
342+
);
343+
});
344+
345+
it('delete is idempotent for non-existent trace', async () => {
346+
const deleteRes = await fetch(`${url}/api/traces/non-existent-trace`, {
347+
method: 'DELETE',
348+
});
349+
assert.strictEqual(deleteRes.status, 200);
350+
});
351+
272352
async function assertTraceData(traceId: string, traceData: TraceData) {
273353
const getResp = await fetch(`${url}/api/traces/${traceId}`);
274354
assert.strictEqual(getResp.status, 200);

0 commit comments

Comments
 (0)