-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathLogDetailPresenter.server.ts
More file actions
107 lines (89 loc) · 3.69 KB
/
LogDetailPresenter.server.ts
File metadata and controls
107 lines (89 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { type ClickHouse } from "@internal/clickhouse";
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
import { convertClickhouseDateTime64ToJsDate } from "~/v3/eventRepository/clickhouseEventRepository.server";
import { kindToLevel } from "~/utils/logUtils";
import { getConfiguredEventRepository } from "~/v3/eventRepository/index.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
export type LogDetailOptions = {
environmentId: string;
organizationId: string;
projectId: string;
spanId: string;
traceId: string;
// The exact start_time from the log id - used to uniquely identify the event
startTime: string;
};
export type LogDetail = Awaited<ReturnType<LogDetailPresenter["call"]>>;
export class LogDetailPresenter {
constructor(
private readonly replica: PrismaClientOrTransaction,
private readonly clickhouse: ClickHouse
) {}
public async call(options: LogDetailOptions) {
const { environmentId, organizationId, projectId, spanId, traceId, startTime } = options;
// Determine which store to use based on organization configuration
const { store } = await getConfiguredEventRepository(organizationId);
// Throw error if postgres is detected
if (store === "postgres") {
throw new ServiceValidationError(
"Log details are not available for PostgreSQL event store. Please contact support."
);
}
// Build ClickHouse query - only v2 is supported for log details
const isClickhouseV2 = store === "clickhouse_v2";
const queryBuilder = isClickhouseV2
? this.clickhouse.taskEventsV2.logDetailQueryBuilder()
: this.clickhouse.taskEvents.logDetailQueryBuilder();
// Required filters - spanId, traceId, and startTime uniquely identify the log
// Multiple events can share the same spanId (span, span events, logs), so startTime is needed
queryBuilder.where("environment_id = {environmentId: String}", {
environmentId,
});
queryBuilder.where("organization_id = {organizationId: String}", {
organizationId,
});
queryBuilder.where("project_id = {projectId: String}", { projectId });
queryBuilder.where("span_id = {spanId: String}", { spanId });
queryBuilder.where("trace_id = {traceId: String}", { traceId });
queryBuilder.where("start_time = {startTime: String}", { startTime });
queryBuilder.limit(1);
// Execute query
const [queryError, records] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
if (!records || records.length === 0) {
return null;
}
const log = records[0];
let parsedAttributes: Record<string, unknown> = {};
let rawAttributesString = "";
try {
// Handle attributes_text which is a string
if (log.attributes_text) {
parsedAttributes = JSON.parse(log.attributes_text) as Record<string, unknown>;
rawAttributesString = log.attributes_text;
}
} catch {
// Ignore parse errors
}
return {
// Use :: separator to match LogsListPresenter format
id: `${log.trace_id}::${log.span_id}::${log.run_id}::${log.start_time}`,
runId: log.run_id,
taskIdentifier: log.task_identifier,
startTime: convertClickhouseDateTime64ToJsDate(log.start_time).toISOString(),
traceId: log.trace_id,
spanId: log.span_id,
parentSpanId: log.parent_span_id || null,
message: log.message,
kind: log.kind,
status: log.status,
duration: typeof log.duration === "number" ? log.duration : Number(log.duration),
level: kindToLevel(log.kind, log.status),
attributes: parsedAttributes,
// Raw strings for display
rawAttributes: rawAttributesString,
};
}
}