Skip to content

Commit 0b0df07

Browse files
authored
logs-page-fixes (#2889)
* Removed EVENT_REPOSITORY_CLICKHOUSE_ROLLOUT_PERCENT * Added hasLogsPageAccess featureFlag for logs page * Replaced attributes with attributes_text for logs to reduce memory usage and improve query performance * Added support for event_v1 for logs, now depending on the settings the logs are fetched either from `task_events_v1` or `task_events_v2` * Show an error in the interface in cast the repository store is `postgres`
1 parent 936bddf commit 0b0df07

File tree

11 files changed

+287
-137
lines changed

11 files changed

+287
-137
lines changed

apps/webapp/app/components/logs/LogsTable.tsx

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { PopoverMenuItem } from "~/components/primitives/Popover";
2727

2828
type LogsTableProps = {
2929
logs: LogEntry[];
30-
hasFilters: boolean;
3130
searchTerm?: string;
3231
isLoading?: boolean;
3332
isLoadingMore?: boolean;
@@ -59,7 +58,6 @@ function getLevelBorderColor(level: LogEntry["level"]): string {
5958

6059
export function LogsTable({
6160
logs,
62-
hasFilters,
6361
searchTerm,
6462
isLoading = false,
6563
isLoadingMore = false,
@@ -126,11 +124,7 @@ export function LogsTable({
126124
</TableRow>
127125
</TableHeader>
128126
<TableBody>
129-
{logs.length === 0 && !hasFilters ? (
130-
<TableBlankRow colSpan={6}>
131-
{!isLoading && <NoLogs title="No logs found" />}
132-
</TableBlankRow>
133-
) : logs.length === 0 ? (
127+
{logs.length === 0 ? (
134128
<BlankState isLoading={isLoading} onRefresh={() => window.location.reload()} />
135129
) : (
136130
logs.map((log) => {
@@ -214,14 +208,6 @@ export function LogsTable({
214208
);
215209
}
216210

217-
function NoLogs({ title }: { title: string }) {
218-
return (
219-
<div className="flex items-center justify-center">
220-
<Paragraph className="w-auto">{title}</Paragraph>
221-
</div>
222-
);
223-
}
224-
225211
function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) {
226212
if (isLoading) return <TableBlankRow colSpan={6}></TableBlankRow>;
227213

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export function SideMenu({
269269
to={v3DeploymentsPath(organization, project, environment)}
270270
data-action="deployments"
271271
/>
272-
{(isAdmin || user.isImpersonating) && (
272+
{(user.admin || user.isImpersonating || featureFlags.hasLogsPageAccess) && (
273273
<SideMenuItem
274274
name="Logs"
275275
icon={LogsIcon}

apps/webapp/app/env.server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,6 @@ const EnvironmentSchema = z
12201220
.number()
12211221
.int()
12221222
.default(60_000 * 5), // 5 minutes
1223-
EVENT_REPOSITORY_CLICKHOUSE_ROLLOUT_PERCENT: z.coerce.number().optional(),
12241223
EVENT_REPOSITORY_DEFAULT_STORE: z
12251224
.enum(["postgres", "clickhouse", "clickhouse_v2"])
12261225
.default("postgres"),

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

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { type ClickHouse } from "@internal/clickhouse";
22
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
33
import { convertClickhouseDateTime64ToJsDate } from "~/v3/eventRepository/clickhouseEventRepository.server";
44
import { kindToLevel } from "~/utils/logUtils";
5+
import { getConfiguredEventRepository } from "~/v3/eventRepository/index.server";
6+
import { ServiceValidationError } from "~/v3/services/baseService.server";
57

68
export type LogDetailOptions = {
79
environmentId: string;
@@ -24,8 +26,20 @@ export class LogDetailPresenter {
2426
public async call(options: LogDetailOptions) {
2527
const { environmentId, organizationId, projectId, spanId, traceId, startTime } = options;
2628

27-
// Build ClickHouse query
28-
const queryBuilder = this.clickhouse.taskEventsV2.logDetailQueryBuilder();
29+
// Determine which store to use based on organization configuration
30+
const { store } = await getConfiguredEventRepository(organizationId);
31+
32+
// Throw error if postgres is detected
33+
if (store === "postgres") {
34+
throw new ServiceValidationError(
35+
"Log details are not available for PostgreSQL event store. Please contact support."
36+
);
37+
}
38+
39+
const isClickhouseV2 = store === "clickhouse_v2";
40+
const queryBuilder = isClickhouseV2
41+
? this.clickhouse.taskEventsV2.logDetailQueryBuilder()
42+
: this.clickhouse.taskEvents.logDetailQueryBuilder();
2943

3044
// Required filters - spanId, traceId, and startTime uniquely identify the log
3145
// Multiple events can share the same spanId (span, span events, logs), so startTime is needed
@@ -55,29 +69,16 @@ export class LogDetailPresenter {
5569

5670
const log = records[0];
5771

58-
// Parse metadata and attributes
59-
let parsedMetadata: Record<string, unknown> = {};
72+
6073
let parsedAttributes: Record<string, unknown> = {};
6174
let rawAttributesString = "";
6275

63-
try {
64-
if (log.metadata) {
65-
parsedMetadata = JSON.parse(log.metadata) as Record<string, unknown>;
66-
}
67-
} catch {
68-
// Ignore parse errors
69-
}
7076

7177
try {
72-
// Handle attributes which could be a JSON object or string
73-
if (log.attributes) {
74-
if (typeof log.attributes === "string") {
75-
parsedAttributes = JSON.parse(log.attributes) as Record<string, unknown>;
76-
rawAttributesString = log.attributes;
77-
} else if (typeof log.attributes === "object") {
78-
parsedAttributes = log.attributes as Record<string, unknown>;
79-
rawAttributesString = JSON.stringify(log.attributes);
80-
}
78+
// Handle attributes_text which is a string
79+
if (log.attributes_text) {
80+
parsedAttributes = JSON.parse(log.attributes_text) as Record<string, unknown>;
81+
rawAttributesString = log.attributes_text;
8182
}
8283
} catch {
8384
// Ignore parse errors
@@ -97,10 +98,8 @@ export class LogDetailPresenter {
9798
status: log.status,
9899
duration: typeof log.duration === "number" ? log.duration : Number(log.duration),
99100
level: kindToLevel(log.kind, log.status),
100-
metadata: parsedMetadata,
101101
attributes: parsedAttributes,
102102
// Raw strings for display
103-
rawMetadata: log.metadata,
104103
rawAttributes: rawAttributesString,
105104
};
106105
}

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

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { z } from "zod";
22
import { type ClickHouse, type LogsListResult } from "@internal/clickhouse";
33
import { MachinePresetName } from "@trigger.dev/core/v3";
44
import {
5-
type PrismaClient,
65
type PrismaClientOrTransaction,
76
type TaskRunStatus,
87
TaskRunStatus as TaskRunStatusEnum,
9-
TaskTriggerSource,
108
} from "@trigger.dev/database";
9+
import { getConfiguredEventRepository } from "~/v3/eventRepository/index.server";
1110

1211
// Create a schema that validates TaskRunStatus enum values
1312
const TaskRunStatusSchema = z.array(z.nativeEnum(TaskRunStatusEnum));
@@ -18,11 +17,12 @@ import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
1817
import { getAllTaskIdentifiers } from "~/models/task.server";
1918
import { RunsRepository } from "~/services/runsRepository/runsRepository.server";
2019
import { ServiceValidationError } from "~/v3/services/baseService.server";
20+
import { kindToLevel, type LogLevel, LogLevelSchema } from "~/utils/logUtils";
21+
import { BasePresenter } from "~/presenters/v3/basePresenter.server";
2122
import {
2223
convertDateToClickhouseDateTime,
2324
convertClickhouseDateTime64ToJsDate,
2425
} from "~/v3/eventRepository/clickhouseEventRepository.server";
25-
import { kindToLevel, type LogLevel, LogLevelSchema } from "~/utils/logUtils";
2626

2727
export type { LogLevel };
2828

@@ -131,9 +131,7 @@ function decodeCursor(cursor: string): LogCursor | null {
131131
}
132132

133133
// Convert display level to ClickHouse kinds and statuses
134-
function levelToKindsAndStatuses(
135-
level: LogLevel
136-
): { kinds?: string[]; statuses?: string[] } {
134+
function levelToKindsAndStatuses(level: LogLevel): { kinds?: string[]; statuses?: string[] } {
137135
switch (level) {
138136
case "DEBUG":
139137
return { kinds: ["DEBUG_EVENT", "LOG_DEBUG"] };
@@ -150,7 +148,6 @@ function levelToKindsAndStatuses(
150148
}
151149
}
152150

153-
154151
function convertDateToNanoseconds(date: Date): bigint {
155152
return BigInt(date.getTime()) * 1_000_000n;
156153
}
@@ -168,11 +165,13 @@ function formatNanosecondsForClickhouse(ns: bigint): string {
168165
return padded.slice(0, 10) + "." + padded.slice(10);
169166
}
170167

171-
export class LogsListPresenter {
168+
export class LogsListPresenter extends BasePresenter {
172169
constructor(
173170
private readonly replica: PrismaClientOrTransaction,
174171
private readonly clickhouse: ClickHouse
175-
) {}
172+
) {
173+
super(undefined, replica);
174+
}
176175

177176
public async call(
178177
organizationId: string,
@@ -242,10 +241,7 @@ export class LogsListPresenter {
242241
(search !== undefined && search !== "") ||
243242
!time.isDefault;
244243

245-
const possibleTasksAsync = getAllTaskIdentifiers(
246-
this.replica,
247-
environmentId
248-
);
244+
const possibleTasksAsync = getAllTaskIdentifiers(this.replica, environmentId);
249245

250246
const bulkActionsAsync = this.replica.bulkActionGroup.findMany({
251247
select: {
@@ -264,31 +260,26 @@ export class LogsListPresenter {
264260
take: 20,
265261
});
266262

267-
const [possibleTasks, bulkActions, displayableEnvironment] =
268-
await Promise.all([
269-
possibleTasksAsync,
270-
bulkActionsAsync,
271-
findDisplayableEnvironment(environmentId, userId),
272-
]);
273-
274-
if (
275-
bulkId &&
276-
!bulkActions.some((bulkAction) => bulkAction.friendlyId === bulkId)
277-
) {
278-
const selectedBulkAction =
279-
await this.replica.bulkActionGroup.findFirst({
280-
select: {
281-
friendlyId: true,
282-
type: true,
283-
createdAt: true,
284-
name: true,
285-
},
286-
where: {
287-
friendlyId: bulkId,
288-
projectId,
289-
environmentId,
290-
},
291-
});
263+
const [possibleTasks, bulkActions, displayableEnvironment] = await Promise.all([
264+
possibleTasksAsync,
265+
bulkActionsAsync,
266+
findDisplayableEnvironment(environmentId, userId),
267+
]);
268+
269+
if (bulkId && !bulkActions.some((bulkAction) => bulkAction.friendlyId === bulkId)) {
270+
const selectedBulkAction = await this.replica.bulkActionGroup.findFirst({
271+
select: {
272+
friendlyId: true,
273+
type: true,
274+
createdAt: true,
275+
name: true,
276+
},
277+
where: {
278+
friendlyId: bulkId,
279+
projectId,
280+
environmentId,
281+
},
282+
});
292283

293284
if (selectedBulkAction) {
294285
bulkActions.push(selectedBulkAction);
@@ -371,7 +362,22 @@ export class LogsListPresenter {
371362
}
372363
}
373364

374-
const queryBuilder = this.clickhouse.taskEventsV2.logsListQueryBuilder();
365+
// Determine which store to use based on organization configuration
366+
const { store } = await getConfiguredEventRepository(organizationId);
367+
368+
// Throw error if postgres is detected
369+
if (store === "postgres") {
370+
throw new ServiceValidationError(
371+
"Logs are not available for PostgreSQL event store. Please contact support."
372+
);
373+
}
374+
375+
// Get the appropriate query builder based on store type
376+
const isClickhouseV2 = store === "clickhouse_v2";
377+
378+
const queryBuilder = isClickhouseV2
379+
? this.clickhouse.taskEventsV2.logsListQueryBuilder()
380+
: this.clickhouse.taskEvents.logsListQueryBuilder();
375381

376382
queryBuilder.prewhere("environment_id = {environmentId: String}", {
377383
environmentId,
@@ -382,12 +388,17 @@ export class LogsListPresenter {
382388
});
383389
queryBuilder.where("project_id = {projectId: String}", { projectId });
384390

385-
// Time filters - inserted_at in PREWHERE for partition pruning, start_time in WHERE
391+
// Time filters - inserted_at in PREWHERE only for v2, start_time in WHERE for both
386392
if (effectiveFrom) {
387393
const fromNs = convertDateToNanoseconds(effectiveFrom);
388-
queryBuilder.prewhere("inserted_at >= {insertedAtStart: DateTime64(3)}", {
389-
insertedAtStart: convertDateToClickhouseDateTime(effectiveFrom),
390-
});
394+
395+
// Only use inserted_at for partition pruning if v2
396+
if (isClickhouseV2) {
397+
queryBuilder.prewhere("inserted_at >= {insertedAtStart: DateTime64(3)}", {
398+
insertedAtStart: convertDateToClickhouseDateTime(effectiveFrom),
399+
});
400+
}
401+
391402
queryBuilder.where("start_time >= {fromTime: String}", {
392403
fromTime: formatNanosecondsForClickhouse(fromNs),
393404
});
@@ -396,9 +407,14 @@ export class LogsListPresenter {
396407
if (effectiveTo) {
397408
const clampedTo = effectiveTo > new Date() ? new Date() : effectiveTo;
398409
const toNs = convertDateToNanoseconds(clampedTo);
399-
queryBuilder.prewhere("inserted_at <= {insertedAtEnd: DateTime64(3)}", {
400-
insertedAtEnd: convertDateToClickhouseDateTime(clampedTo),
401-
});
410+
411+
// Only use inserted_at for partition pruning if v2
412+
if (isClickhouseV2) {
413+
queryBuilder.prewhere("inserted_at <= {insertedAtEnd: DateTime64(3)}", {
414+
insertedAtEnd: convertDateToClickhouseDateTime(clampedTo),
415+
});
416+
}
417+
402418
queryBuilder.where("start_time <= {toTime: String}", {
403419
toTime: formatNanosecondsForClickhouse(toNs),
404420
});
@@ -428,7 +444,6 @@ export class LogsListPresenter {
428444
);
429445
}
430446

431-
432447
if (levels && levels.length > 0) {
433448
const conditions: string[] = [];
434449
const params: Record<string, string[]> = {};
@@ -477,7 +492,6 @@ export class LogsListPresenter {
477492

478493
queryBuilder.where("NOT (kind = 'SPAN' AND status = 'PARTIAL')");
479494

480-
481495
// Cursor pagination
482496
const decodedCursor = cursor ? decodeCursor(cursor) : null;
483497
if (decodedCursor) {
@@ -525,11 +539,11 @@ export class LogsListPresenter {
525539
let displayMessage = log.message;
526540

527541
// For error logs with status ERROR, try to extract error message from attributes
528-
if (log.status === "ERROR" && log.attributes) {
542+
if (log.status === "ERROR" && log.attributes_text) {
529543
try {
530-
let attributes = log.attributes as ErrorAttributes;
544+
const attributes = JSON.parse(log.attributes_text) as ErrorAttributes;
531545

532-
if (attributes?.error?.message && typeof attributes.error.message === 'string') {
546+
if (attributes?.error?.message && typeof attributes.error.message === "string") {
533547
displayMessage = attributes.error.message;
534548
}
535549
} catch {

0 commit comments

Comments
 (0)