Skip to content

Commit 7c04e4b

Browse files
committed
feat: pagination
1 parent 9969d4b commit 7c04e4b

File tree

5 files changed

+222
-70
lines changed

5 files changed

+222
-70
lines changed

packages/services/api/src/modules/operations/providers/traces.ts

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,31 @@ export class Traces {
114114
first: number | null,
115115
filter: TraceFilter,
116116
sort: GraphQLSchema.TracesSortInput | null,
117+
cursorStr: string | null,
117118
) {
119+
function createCursor(trace: Trace) {
120+
return Buffer.from(
121+
JSON.stringify({
122+
timestamp: trace.timestamp,
123+
traceId: trace.traceId,
124+
duration: sort?.sort === 'DURATION' ? trace.duration : undefined,
125+
} satisfies z.TypeOf<typeof PaginatedTraceCursorModel>),
126+
).toString('base64');
127+
}
128+
129+
function parseCursor(cursor: string) {
130+
const data = PaginatedTraceCursorModel.parse(
131+
JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')),
132+
);
133+
if (sort?.sort === 'DURATION' && !data.duration) {
134+
throw new HiveError('Invalid cursor provided.');
135+
}
136+
return data;
137+
}
138+
118139
await this._guardViewerCanAccessTraces(organizationId);
119-
const limit = (first ?? 10) + 1;
120-
const sqlConditions = buildTraceFilterSQLConditions(filter);
121-
const filterSQLFragment = sqlConditions.length
122-
? sql`AND ${sql.join(sqlConditions, ' AND ')}`
123-
: sql``;
140+
const limit = first ?? 50;
141+
const cursor = cursorStr ? parseCursor(cursorStr) : null;
124142

125143
// By default we order by timestamp DESC
126144
// In case a custom sort is provided, we order by duration asc/desc or timestamp asc
@@ -130,6 +148,46 @@ export class Traces {
130148
, "trace_id" DESC
131149
`;
132150

151+
let paginationSQLFragmentPart = sql``;
152+
153+
if (cursor) {
154+
if (sort?.sort === 'DURATION') {
155+
const operator = sort.direction === 'ASC' ? sql`>` : sql`<`;
156+
const durationStr = String(cursor.duration);
157+
paginationSQLFragmentPart = sql`
158+
AND (
159+
"duration" ${operator} ${durationStr}
160+
OR (
161+
"duration" = ${durationStr}
162+
AND "timestamp" < ${cursor.timestamp}
163+
)
164+
OR (
165+
"duration" = ${durationStr}
166+
AND "timestamp" = ${cursor.timestamp}
167+
AND "trace_id" < ${cursor.traceId}
168+
)
169+
)
170+
`;
171+
} /* TIMESTAMP */ else {
172+
const operator = sort?.direction === 'ASC' ? sql`>` : sql`<`;
173+
paginationSQLFragmentPart = sql`
174+
AND (
175+
(
176+
"timestamp" = ${cursor.timestamp}
177+
AND "trace_id" < ${cursor.traceId}
178+
)
179+
OR "timestamp" ${operator} ${cursor.timestamp}
180+
)
181+
`;
182+
}
183+
}
184+
185+
const sqlConditions = buildTraceFilterSQLConditions(filter, false);
186+
187+
const filterSQLFragment = sqlConditions.length
188+
? sql`AND ${sql.join(sqlConditions, ' AND ')}`
189+
: sql``;
190+
133191
const tracesQuery = await this.clickHouse.query<unknown>({
134192
query: sql`
135193
SELECT
@@ -138,41 +196,30 @@ export class Traces {
138196
"otel_traces_normalized"
139197
WHERE
140198
target_id = ${targetId}
199+
${paginationSQLFragmentPart}
141200
${filterSQLFragment}
142201
ORDER BY
143202
${orderByFragment}
144-
LIMIT ${sql.raw(String(limit))}
203+
LIMIT ${sql.raw(String(limit + 1))}
145204
`,
146205
queryId: 'traces',
147206
timeout: 10_000,
148207
});
149208

150-
const traces = TraceListModel.parse(tracesQuery.data);
151-
let hasNext = false;
152-
153-
if (traces.length == limit) {
154-
hasNext = true;
155-
(traces as any).pop();
156-
}
209+
let traces = TraceListModel.parse(tracesQuery.data);
210+
const hasNext = traces.length > limit;
211+
traces = traces.slice(0, limit);
157212

158213
return {
159-
edges: traces.map(trace => {
160-
return {
161-
node: trace,
162-
cursor: Buffer.from(`${trace.timestamp}|${trace.traceId}`).toString('base64'),
163-
};
164-
}),
214+
edges: traces.map(trace => ({
215+
node: trace,
216+
cursor: createCursor(trace),
217+
})),
165218
pageInfo: {
166219
hasNextPage: hasNext,
167220
hasPreviousPage: false,
168-
endCursor: traces.length
169-
? Buffer.from(
170-
`${traces[traces.length - 1].timestamp}|${traces[traces.length - 1].traceId}`,
171-
).toString('base64')
172-
: '',
173-
startCursor: traces.length
174-
? Buffer.from(`${traces[0].timestamp}|${traces[0].traceId}`).toString('base64')
175-
: '',
221+
endCursor: traces.length ? createCursor(traces[traces.length - 1]) : '',
222+
startCursor: traces.length ? createCursor(traces[0]) : '',
176223
},
177224
};
178225
}
@@ -325,7 +372,7 @@ type TraceFilter = {
325372
httpUrls: ReadonlyArray<string> | null;
326373
};
327374

328-
function buildTraceFilterSQLConditions(filter: TraceFilter, skipPeriod = false) {
375+
function buildTraceFilterSQLConditions(filter: TraceFilter, skipPeriod: boolean) {
329376
const ANDs: SqlValue[] = [];
330377

331378
if (filter?.period && !skipPeriod) {
@@ -512,3 +559,12 @@ function getBucketUnitAndCount(startDate: Date, endDate: Date): BucketResult {
512559

513560
return { unit, count };
514561
}
562+
563+
/**
564+
* All sortable fields (duration, timestamp), must be part of the cursor
565+
*/
566+
const PaginatedTraceCursorModel = z.object({
567+
traceId: z.string(),
568+
timestamp: z.string(),
569+
duration: z.number().optional(),
570+
});

packages/services/api/src/modules/operations/resolvers/Target.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const Target: Pick<
7878
schemaCoordinate: args.schemaCoordinate,
7979
};
8080
},
81-
traces: async (target, { first, filter, sort }, { injector }) => {
81+
traces: async (target, { first, filter, sort, after }, { injector }) => {
8282
return injector.get(Traces).findTracesForTargetId(
8383
target.orgId,
8484
target.id,
@@ -105,6 +105,7 @@ export const Target: Pick<
105105
httpUrls: filter?.httpUrls ?? null,
106106
},
107107
sort ?? null,
108+
after ?? null,
108109
);
109110
},
110111
tracesFilterOptions: async (target, { filter }, { injector }) => {

packages/web/app/src/lib/urql.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const urqlClient = createClient({
3535
resolvers: {
3636
Target: {
3737
appDeployments: relayPagination(),
38+
traces: relayPagination(),
3839
},
3940
AppDeployment: {
4041
documents: relayPagination(),

0 commit comments

Comments
 (0)