Skip to content

Commit d45a030

Browse files
committed
fix(webapp): keep log projector lag metrics fresh
1 parent 00719bf commit d45a030

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

apps/webapp/app/services/logsSearchProjector.server.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,14 @@ export class LogsSearchProjector {
110110
return { finalized: 0, preview: false };
111111
}
112112

113-
const finalized = await this.processFinalizedWindows(finalizedCutoff);
113+
let finalized: number;
114+
try {
115+
finalized = await this.processFinalizedWindows(finalizedCutoff);
116+
} catch (error) {
117+
await this.updateTelemetryStateAfterFailure(now, control);
118+
throw error;
119+
}
120+
114121
const refreshedControl = await this.stateStore.getControl();
115122
const currentNow = await this.clock();
116123
const currentFinalizedCutoff = finalizedSafeCutoff(currentNow);
@@ -355,6 +362,19 @@ export class LogsSearchProjector {
355362
};
356363
}
357364

365+
private async updateTelemetryStateAfterFailure(
366+
now: Date,
367+
control: LogsSearchProjectorControl
368+
): Promise<void> {
369+
try {
370+
await this.updateTelemetryState(now, control);
371+
} catch (error) {
372+
this.logger.warn("Failed to refresh logs search projector telemetry after tick failure", {
373+
error,
374+
});
375+
}
376+
}
377+
358378
private async updateTelemetryState(
359379
now: Date,
360380
control: LogsSearchProjectorControl

apps/webapp/test/logsSearchProjector.test.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1-
import { describe, expect, it } from "vitest";
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import {
33
finalizedSafeCutoff,
4+
LogsSearchProjector,
45
previewSafeCutoff,
56
selectFinalizedWindow,
67
selectPreviewWindow,
8+
type LogsSearchProjectorRedisStore,
9+
type LogsSearchProjectorStateStore,
710
} from "~/services/logsSearchProjector.server";
811

12+
const telemetry = vi.hoisted(() => ({
13+
recordWindow: vi.fn(),
14+
recordLeaseContention: vi.fn(),
15+
recordCheckpointConflict: vi.fn(),
16+
recordPreviewSkipped: vi.fn(),
17+
updateState: vi.fn(),
18+
}));
19+
20+
vi.mock("~/services/logsSearchProjectorTelemetry.server", () => ({
21+
logsSearchProjectorTelemetry: telemetry,
22+
}));
23+
924
const at = (value: string) => new Date(value);
1025

26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
});
29+
1130
describe("logs search projector window selection", () => {
1231
it("floors preview work to a closed five-second boundary", () => {
1332
expect(previewSafeCutoff(at("2026-08-14T12:10:09.999Z")).toISOString()).toBe(
@@ -67,3 +86,59 @@ describe("logs search projector window selection", () => {
6786
});
6887
});
6988
});
89+
90+
describe("logs search projector telemetry", () => {
91+
it("refreshes lag after a finalized projection failure", async () => {
92+
const now = at("2026-08-14T12:10:59.999Z");
93+
const watermark = at("2026-08-14T12:05:00.000Z");
94+
const control = {
95+
id: "task_events_search_v2",
96+
initialWatermark: watermark,
97+
paused: false,
98+
};
99+
const stateStore = {
100+
initialize: vi.fn(async () => control),
101+
findControl: vi.fn(async () => control),
102+
getControl: vi.fn(async () => control),
103+
getFinalizedWatermark: vi.fn(async () => watermark),
104+
appendFinalizedCheckpoint: vi.fn(),
105+
pause: vi.fn(),
106+
resume: vi.fn(),
107+
} satisfies LogsSearchProjectorStateStore;
108+
const redisStore = {
109+
acquireLease: vi.fn(async () => true),
110+
releaseLease: vi.fn(),
111+
readLeaseStatus: vi.fn(async () => null),
112+
initializePreviewWatermark: vi.fn(async () => watermark),
113+
getPreviewWatermark: vi.fn(async () => null),
114+
advancePreviewWatermark: vi.fn(async () => true),
115+
} satisfies LogsSearchProjectorRedisStore;
116+
const projectionError = new Error("projection failed");
117+
const projector = new LogsSearchProjector(
118+
{
119+
previewEnabled: true,
120+
maxFinalizedWindowsPerTick: 1,
121+
leaseDurationMs: 60_000,
122+
},
123+
stateStore,
124+
redisStore,
125+
vi.fn(async () => {
126+
throw projectionError;
127+
}),
128+
() => now,
129+
{
130+
debug: vi.fn(),
131+
info: vi.fn(),
132+
warn: vi.fn(),
133+
error: vi.fn(),
134+
}
135+
);
136+
137+
await expect(projector.processTick()).rejects.toBe(projectionError);
138+
expect(telemetry.updateState).toHaveBeenCalledWith({
139+
previewLagMs: null,
140+
finalizedLagMs: 180_000,
141+
paused: false,
142+
});
143+
});
144+
});

0 commit comments

Comments
 (0)