Skip to content

Commit f60056e

Browse files
committed
Much better get run details tool output with more detailed trace data
1 parent 599f6a2 commit f60056e

File tree

8 files changed

+687
-47
lines changed

8 files changed

+687
-47
lines changed

apps/webapp/app/routes/api.v1.runs.$runId.trace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const loader = createLoaderApiRoute(
3636
},
3737
},
3838
async ({ resource: run }) => {
39-
const traceSummary = await eventRepository.getTraceSummary(
39+
const traceSummary = await eventRepository.getTraceDetailedSummary(
4040
getTaskEventStoreTableForRun(run),
4141
run.traceId,
4242
run.createdAt,

apps/webapp/app/v3/eventRepository.server.ts

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { createRedisClient, RedisClient, RedisWithClusterOptions } from "~/redis
3333
import { logger } from "~/services/logger.server";
3434
import { singleton } from "~/utils/singleton";
3535
import { DynamicFlushScheduler } from "./dynamicFlushScheduler.server";
36-
import { TaskEventStore, TaskEventStoreTable } from "./taskEventStore.server";
36+
import { DetailedTraceEvent, TaskEventStore, TaskEventStoreTable } from "./taskEventStore.server";
3737
import { startActiveSpan } from "./tracer.server";
3838
import { startSpan } from "./tracing.server";
3939

@@ -146,6 +146,12 @@ export type PreparedEvent = Omit<QueriedEvent, "events" | "style" | "duration">
146146
style: TaskEventStyle;
147147
};
148148

149+
export type PreparedDetailedEvent = Omit<DetailedTraceEvent, "events" | "style" | "duration"> & {
150+
duration: number;
151+
events: SpanEvents;
152+
style: TaskEventStyle;
153+
};
154+
149155
export type RunPreparedEvent = PreparedEvent & {
150156
taskSlug?: string;
151157
};
@@ -186,6 +192,36 @@ export type SpanSummary = {
186192

187193
export type TraceSummary = { rootSpan: SpanSummary; spans: Array<SpanSummary> };
188194

195+
export type SpanDetailedSummary = {
196+
id: string;
197+
parentId: string | undefined;
198+
message: string;
199+
data: {
200+
runId: string;
201+
taskSlug?: string;
202+
taskPath?: string;
203+
events: SpanEvents;
204+
startTime: Date;
205+
duration: number;
206+
isError: boolean;
207+
isPartial: boolean;
208+
isCancelled: boolean;
209+
level: NonNullable<CreatableEvent["level"]>;
210+
environmentType: CreatableEventEnvironmentType;
211+
workerVersion?: string;
212+
queueName?: string;
213+
machinePreset?: string;
214+
properties?: Attributes;
215+
output?: Attributes;
216+
};
217+
children: Array<SpanDetailedSummary>;
218+
};
219+
220+
export type TraceDetailedSummary = {
221+
traceId: string;
222+
rootSpan: SpanDetailedSummary;
223+
};
224+
189225
export type UpdateEventOptions = {
190226
attributes: TraceAttributes;
191227
endTime?: Date;
@@ -589,6 +625,121 @@ export class EventRepository {
589625
});
590626
}
591627

628+
public async getTraceDetailedSummary(
629+
storeTable: TaskEventStoreTable,
630+
traceId: string,
631+
startCreatedAt: Date,
632+
endCreatedAt?: Date,
633+
options?: { includeDebugLogs?: boolean }
634+
): Promise<TraceDetailedSummary | undefined> {
635+
return await startActiveSpan("getTraceDetailedSummary", async (span) => {
636+
const events = await this.taskEventStore.findDetailedTraceEvents(
637+
storeTable,
638+
traceId,
639+
startCreatedAt,
640+
endCreatedAt,
641+
{ includeDebugLogs: options?.includeDebugLogs }
642+
);
643+
644+
let preparedEvents: Array<PreparedDetailedEvent> = [];
645+
let rootSpanId: string | undefined;
646+
const eventsBySpanId = new Map<string, PreparedDetailedEvent>();
647+
648+
for (const event of events) {
649+
preparedEvents.push(prepareDetailedEvent(event));
650+
651+
if (!rootSpanId && !event.parentId) {
652+
rootSpanId = event.spanId;
653+
}
654+
}
655+
656+
for (const event of preparedEvents) {
657+
const existingEvent = eventsBySpanId.get(event.spanId);
658+
659+
if (!existingEvent) {
660+
eventsBySpanId.set(event.spanId, event);
661+
continue;
662+
}
663+
664+
if (event.isCancelled || !event.isPartial) {
665+
eventsBySpanId.set(event.spanId, event);
666+
}
667+
}
668+
669+
preparedEvents = Array.from(eventsBySpanId.values());
670+
671+
if (!rootSpanId) {
672+
return;
673+
}
674+
675+
// Build hierarchical structure
676+
const spanDetailedSummaryMap = new Map<string, SpanDetailedSummary>();
677+
678+
// First pass: create all span detailed summaries
679+
for (const event of preparedEvents) {
680+
const ancestorCancelled = isAncestorCancelled(eventsBySpanId, event.spanId);
681+
const duration = calculateDurationIfAncestorIsCancelled(
682+
eventsBySpanId,
683+
event.spanId,
684+
event.duration
685+
);
686+
687+
const output = event.output ? (event.output as Attributes) : undefined;
688+
const properties = event.properties
689+
? removePrivateProperties(event.properties as Attributes)
690+
: {};
691+
692+
const spanDetailedSummary: SpanDetailedSummary = {
693+
id: event.spanId,
694+
parentId: event.parentId ?? undefined,
695+
message: event.message,
696+
data: {
697+
runId: event.runId,
698+
taskSlug: event.taskSlug ?? undefined,
699+
taskPath: event.taskPath ?? undefined,
700+
events: event.events?.filter((e) => !e.name.startsWith("trigger.dev")),
701+
startTime: getDateFromNanoseconds(event.startTime),
702+
duration: nanosecondsToMilliseconds(duration),
703+
isError: event.isError,
704+
isPartial: ancestorCancelled ? false : event.isPartial,
705+
isCancelled: event.isCancelled === true ? true : event.isPartial && ancestorCancelled,
706+
level: event.level,
707+
environmentType: event.environmentType,
708+
workerVersion: event.workerVersion ?? undefined,
709+
queueName: event.queueName ?? undefined,
710+
machinePreset: event.machinePreset ?? undefined,
711+
properties,
712+
output,
713+
},
714+
children: [],
715+
};
716+
717+
spanDetailedSummaryMap.set(event.spanId, spanDetailedSummary);
718+
}
719+
720+
// Second pass: build parent-child relationships
721+
for (const spanSummary of spanDetailedSummaryMap.values()) {
722+
if (spanSummary.parentId) {
723+
const parent = spanDetailedSummaryMap.get(spanSummary.parentId);
724+
if (parent) {
725+
parent.children.push(spanSummary);
726+
}
727+
}
728+
}
729+
730+
const rootSpan = spanDetailedSummaryMap.get(rootSpanId);
731+
732+
if (!rootSpan) {
733+
return;
734+
}
735+
736+
return {
737+
traceId,
738+
rootSpan,
739+
};
740+
});
741+
}
742+
592743
public async getRunEvents(
593744
storeTable: TaskEventStoreTable,
594745
runId: string,
@@ -1517,6 +1668,15 @@ function prepareEvent(event: QueriedEvent): PreparedEvent {
15171668
};
15181669
}
15191670

1671+
function prepareDetailedEvent(event: DetailedTraceEvent): PreparedDetailedEvent {
1672+
return {
1673+
...event,
1674+
duration: Number(event.duration),
1675+
events: parseEventsField(event.events),
1676+
style: parseStyleField(event.style),
1677+
};
1678+
}
1679+
15201680
function parseEventsField(events: Prisma.JsonValue): SpanEvents {
15211681
const unsafe = events
15221682
? (events as any[]).map((e) => ({
@@ -1548,7 +1708,10 @@ function parseStyleField(style: Prisma.JsonValue): TaskEventStyle {
15481708
return {};
15491709
}
15501710

1551-
function isAncestorCancelled(events: Map<string, PreparedEvent>, spanId: string) {
1711+
function isAncestorCancelled(
1712+
events: Map<string, { isCancelled: boolean; parentId: string | null }>,
1713+
spanId: string
1714+
) {
15521715
const event = events.get(spanId);
15531716

15541717
if (!event) {
@@ -1567,7 +1730,16 @@ function isAncestorCancelled(events: Map<string, PreparedEvent>, spanId: string)
15671730
}
15681731

15691732
function calculateDurationIfAncestorIsCancelled(
1570-
events: Map<string, PreparedEvent>,
1733+
events: Map<
1734+
string,
1735+
{
1736+
isCancelled: boolean;
1737+
parentId: string | null;
1738+
isPartial: boolean;
1739+
startTime: bigint;
1740+
events: SpanEvents;
1741+
}
1742+
>,
15711743
spanId: string,
15721744
defaultDuration: number
15731745
) {
@@ -1603,7 +1775,19 @@ function calculateDurationIfAncestorIsCancelled(
16031775
return defaultDuration;
16041776
}
16051777

1606-
function findFirstCancelledAncestor(events: Map<string, PreparedEvent>, spanId: string) {
1778+
function findFirstCancelledAncestor(
1779+
events: Map<
1780+
string,
1781+
{
1782+
isCancelled: boolean;
1783+
parentId: string | null;
1784+
isPartial: boolean;
1785+
startTime: bigint;
1786+
events: SpanEvents;
1787+
}
1788+
>,
1789+
spanId: string
1790+
) {
16071791
const event = events.get(spanId);
16081792

16091793
if (!event) {
@@ -1711,6 +1895,10 @@ export function getDateFromNanoseconds(nanoseconds: bigint) {
17111895
return new Date(Number(nanoseconds) / 1_000_000);
17121896
}
17131897

1898+
function nanosecondsToMilliseconds(nanoseconds: bigint | number): number {
1899+
return Number(nanoseconds) / 1_000_000;
1900+
}
1901+
17141902
function rehydrateJson(json: Prisma.JsonValue): any {
17151903
if (json === null) {
17161904
return undefined;

apps/webapp/app/v3/taskEventStore.server.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ export type TraceEvent = Pick<
2323
| "kind"
2424
>;
2525

26+
export type DetailedTraceEvent = Pick<
27+
TaskEvent,
28+
| "spanId"
29+
| "parentId"
30+
| "runId"
31+
| "idempotencyKey"
32+
| "message"
33+
| "style"
34+
| "startTime"
35+
| "duration"
36+
| "isError"
37+
| "isPartial"
38+
| "isCancelled"
39+
| "level"
40+
| "events"
41+
| "environmentType"
42+
| "kind"
43+
| "taskSlug"
44+
| "taskPath"
45+
| "workerVersion"
46+
| "queueName"
47+
| "machinePreset"
48+
| "properties"
49+
| "output"
50+
>;
51+
2652
export type TaskEventStoreTable = "taskEvent" | "taskEventPartitioned";
2753

2854
export function getTaskEventStoreTableForRun(run: {
@@ -207,4 +233,95 @@ export class TaskEventStore {
207233
`;
208234
}
209235
}
236+
237+
async findDetailedTraceEvents(
238+
table: TaskEventStoreTable,
239+
traceId: string,
240+
startCreatedAt: Date,
241+
endCreatedAt?: Date,
242+
options?: { includeDebugLogs?: boolean }
243+
) {
244+
const filterDebug =
245+
options?.includeDebugLogs === false || options?.includeDebugLogs === undefined;
246+
247+
if (table === "taskEventPartitioned") {
248+
const createdAtBufferInMillis = env.TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS * 1000;
249+
const startCreatedAtWithBuffer = new Date(startCreatedAt.getTime() - createdAtBufferInMillis);
250+
const $endCreatedAt = endCreatedAt ?? new Date();
251+
const endCreatedAtWithBuffer = new Date($endCreatedAt.getTime() + createdAtBufferInMillis);
252+
253+
return await this.readReplica.$queryRaw<DetailedTraceEvent[]>`
254+
SELECT
255+
"spanId",
256+
"parentId",
257+
"runId",
258+
"idempotencyKey",
259+
message,
260+
style,
261+
"startTime",
262+
duration,
263+
"isError",
264+
"isPartial",
265+
"isCancelled",
266+
level,
267+
events,
268+
"environmentType",
269+
"kind",
270+
"taskSlug",
271+
"taskPath",
272+
"workerVersion",
273+
"queueName",
274+
"machinePreset",
275+
properties,
276+
output
277+
FROM "TaskEventPartitioned"
278+
WHERE
279+
"traceId" = ${traceId}
280+
AND "createdAt" >= ${startCreatedAtWithBuffer.toISOString()}::timestamp
281+
AND "createdAt" < ${endCreatedAtWithBuffer.toISOString()}::timestamp
282+
${
283+
filterDebug
284+
? Prisma.sql`AND \"kind\" <> CAST('LOG'::text AS "public"."TaskEventKind")`
285+
: Prisma.empty
286+
}
287+
ORDER BY "startTime" ASC
288+
LIMIT ${env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT}
289+
`;
290+
} else {
291+
return await this.readReplica.$queryRaw<DetailedTraceEvent[]>`
292+
SELECT
293+
"spanId",
294+
"parentId",
295+
"runId",
296+
"idempotencyKey",
297+
message,
298+
style,
299+
"startTime",
300+
duration,
301+
"isError",
302+
"isPartial",
303+
"isCancelled",
304+
level,
305+
events,
306+
"environmentType",
307+
"kind",
308+
"taskSlug",
309+
"taskPath",
310+
"workerVersion",
311+
"queueName",
312+
"machinePreset",
313+
properties,
314+
output
315+
FROM "TaskEvent"
316+
WHERE "traceId" = ${traceId}
317+
${
318+
filterDebug
319+
? Prisma.sql`AND \"kind\" <> CAST('LOG'::text AS "public"."TaskEventKind")`
320+
: Prisma.empty
321+
}
322+
ORDER BY "startTime" ASC
323+
LIMIT ${env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT}
324+
`;
325+
}
326+
}
210327
}

0 commit comments

Comments
 (0)