Skip to content

Commit fdc20f4

Browse files
committed
refactor(webapp,clickhouse,database): make logs search v2-only
Read v2 directly, use the projector enable setting as the operational switch, and store initialization in append-only checkpoints.
1 parent 7c8b416 commit fdc20f4

14 files changed

Lines changed: 149 additions & 307 deletions

apps/webapp/app/env.server.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,9 +2094,6 @@ const EnvironmentSchema = z
20942094
.nonnegative()
20952095
.optional(),
20962096

2097-
// Keep reads on v1 until the scheduled v2 projector has enough history.
2098-
LOGS_SEARCH_TABLE_VERSION: z.enum(["v1", "v2"]).default("v1"),
2099-
21002097
// Scheduled logs-search projection. Disabled by default. LOGS_CLICKHOUSE_URL, or the
21012098
// CLICKHOUSE_URL fallback, must reach both source and destination tables and allow writes.
21022099
LOGS_SEARCH_PROJECTOR_ENABLED: BoolEnv.default(false),

apps/webapp/app/presenters/v3/LogsListPresenter.server.ts

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,7 @@ export class LogsListPresenter extends BasePresenter {
227227
}
228228

229229
const effectivePageSize = Math.min(pageSize, env.LOGS_LIST_MAX_PAGE_SIZE);
230-
const usesV2Search = env.LOGS_SEARCH_TABLE_VERSION === "v2";
231-
const queryLimit = usesV2Search
232-
? (effectivePageSize + 1) * LOGS_SEARCH_RETRY_OVERFETCH_FACTOR
233-
: effectivePageSize + 1;
230+
const queryLimit = (effectivePageSize + 1) * LOGS_SEARCH_RETRY_OVERFETCH_FACTOR;
234231

235232
// Only honor a cursor scoped to this org+env; one copied from another scope would shift the
236233
// pagination anchor instead of resetting to the first page.
@@ -247,9 +244,7 @@ export class LogsListPresenter extends BasePresenter {
247244
const clampedTo = effectiveTo !== undefined ? (effectiveTo > now ? now : effectiveTo) : now;
248245

249246
const rawSearchTerm = search?.trim() ?? "";
250-
const normalizedSearchTerm = usesV2Search
251-
? normalizeLogsSearchTerm(rawSearchTerm)
252-
: rawSearchTerm.toLowerCase();
247+
const normalizedSearchTerm = normalizeLogsSearchTerm(rawSearchTerm);
253248
if (rawSearchTerm !== "" && !hasMinimumLogsSearchLength(normalizedSearchTerm)) {
254249
throw new ServiceValidationError(
255250
`Log searches must be at least ${MIN_LOGS_SEARCH_LENGTH} characters.`
@@ -261,11 +256,9 @@ export class LogsListPresenter extends BasePresenter {
261256
// Run exactly one bounded query. Broadening a search window is an explicit user action;
262257
// silently rescanning the same recent rows makes absence queries needlessly expensive.
263258
const runQuery = () => {
264-
const queryBuilder = this.clickhouse.taskEventsSearch.logsListQueryBuilder(
265-
env.LOGS_SEARCH_TABLE_VERSION
266-
);
259+
const queryBuilder = this.clickhouse.taskEventsSearch.logsListQueryBuilder();
267260

268-
// The materialized view excludes events without a trace_id; this guards the legacy tail.
261+
// The projector excludes events without a trace_id.
269262
queryBuilder.where("trace_id != ''");
270263
queryBuilder.where("environment_id = {environmentId: String}", { environmentId });
271264
queryBuilder.where("organization_id = {organizationId: String}", { organizationId });
@@ -294,18 +287,9 @@ export class LogsListPresenter extends BasePresenter {
294287
}
295288

296289
if (searchTerm !== undefined) {
297-
if (usesV2Search) {
298-
// One predicate lets the text index answer substring searches without an OR across
299-
// independently indexed columns.
300-
queryBuilder.where("search_text LIKE {searchPattern: String}", {
301-
searchPattern: `%${searchTerm}%`,
302-
});
303-
} else {
304-
queryBuilder.where(
305-
"(lower(message) LIKE {searchPattern: String} OR lower(attributes_text) LIKE {searchPattern: String})",
306-
{ searchPattern: `%${searchTerm}%` }
307-
);
308-
}
290+
queryBuilder.where("search_text LIKE {searchPattern: String}", {
291+
searchPattern: `%${searchTerm}%`,
292+
});
309293
}
310294

311295
if (levels && levels.length > 0) {
@@ -343,12 +327,12 @@ export class LogsListPresenter extends BasePresenter {
343327
cursorTriggeredTimestamp: decodedCursor.triggeredTimestamp,
344328
cursorTraceId: decodedCursor.traceId,
345329
cursorSpanId: decodedCursor.spanId,
346-
...(usesV2Search && decodedCursor.projectionFingerprint
330+
...(decodedCursor.projectionFingerprint
347331
? { cursorProjectionFingerprint: decodedCursor.projectionFingerprint }
348332
: {}),
349333
};
350334
queryBuilder.where(
351-
usesV2Search && decodedCursor.projectionFingerprint
335+
decodedCursor.projectionFingerprint
352336
? `(triggered_timestamp < {cursorTriggeredTimestamp: String}
353337
OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id < {cursorTraceId: String})
354338
OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id = {cursorTraceId: String} AND span_id < {cursorSpanId: String})
@@ -361,9 +345,7 @@ export class LogsListPresenter extends BasePresenter {
361345
}
362346

363347
queryBuilder.orderBy(
364-
usesV2Search
365-
? "triggered_timestamp DESC, trace_id DESC, span_id DESC, projection_fingerprint DESC"
366-
: "triggered_timestamp DESC, trace_id DESC, span_id DESC"
348+
"triggered_timestamp DESC, trace_id DESC, span_id DESC, projection_fingerprint DESC"
367349
);
368350
queryBuilder.limit(queryLimit);
369351

@@ -379,12 +361,7 @@ export class LogsListPresenter extends BasePresenter {
379361
// marker. Keep the default throw behavior so the product never presents truncated results as
380362
// complete.
381363
const results = queryResult ?? [];
382-
const page = usesV2Search
383-
? prepareLogsSearchPage(results, effectivePageSize, queryLimit)
384-
: {
385-
rows: results.slice(0, effectivePageSize),
386-
hasMore: results.length > effectivePageSize,
387-
};
364+
const page = prepareLogsSearchPage(results, effectivePageSize, queryLimit);
388365
const hasMore = page.hasMore;
389366
const logs = page.rows;
390367

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,8 @@
1-
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
2-
import { z } from "zod";
3-
import { LogsSearchProjectorConflictError } from "~/services/logsSearchProjector.server";
1+
import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
42
import { getLogsSearchProjector } from "~/services/logsSearchProjectorInstance.server";
5-
import { logger } from "~/services/logger.server";
63
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
74

8-
const Body = z.discriminatedUnion("action", [
9-
z.object({ action: z.literal("pause") }),
10-
z.object({ action: z.literal("resume") }),
11-
]);
12-
135
export async function loader({ request }: LoaderFunctionArgs) {
146
await requireAdminApiRequest(request);
157
return json(await getLogsSearchProjector().status());
168
}
17-
18-
export async function action({ request }: ActionFunctionArgs) {
19-
const user = await requireAdminApiRequest(request);
20-
21-
try {
22-
const body = Body.parse(await request.json());
23-
const logsSearchProjector = getLogsSearchProjector();
24-
logger.info("Updating logs search projector", { userId: user.id, action: body.action });
25-
26-
switch (body.action) {
27-
case "pause":
28-
return json(await logsSearchProjector.pause());
29-
case "resume":
30-
return json(await logsSearchProjector.resume());
31-
}
32-
} catch (error) {
33-
if (error instanceof LogsSearchProjectorConflictError) {
34-
return json({ error: error.message }, { status: 409 });
35-
}
36-
if (error instanceof z.ZodError || error instanceof SyntaxError) {
37-
return json(
38-
{ error: error instanceof Error ? error.message : String(error) },
39-
{ status: 400 }
40-
);
41-
}
42-
throw error;
43-
}
44-
}

0 commit comments

Comments
 (0)