44// DSL-based query definitions for logs timeseries and breakdown.
55// ---------------------------------------------------------------------------
66
7+ import { compileCH } from "../compile"
78import * as CH from "../expr"
89import { param } from "../param"
910import { from , type ColumnAccessor } from "../query"
@@ -187,7 +188,50 @@ export interface LogsListOutput {
187188 readonly resourceAttributes : string
188189}
189190
191+ /**
192+ * Two-stage list query. The `logs` sort key is
193+ * `(OrgId, ServiceName, TimestampTime, Timestamp)` — `ServiceName` sits between
194+ * `OrgId` and the timestamps, so `ORDER BY Timestamp DESC` is not a sort-key
195+ * prefix and ClickHouse cannot read-in-order. A single-stage query therefore
196+ * scans the whole window and materializes the heavy `Body` / attribute-map
197+ * columns for every matching row *before* `LIMIT` discards all but N, which
198+ * OOMs on busy orgs.
199+ *
200+ * Stage 1 reads only `Timestamp` to find the cutoff (the Nth-newest matching
201+ * timestamp). Stage 2 gates on `Timestamp >= cutoff`, so the heavy columns are
202+ * materialized only for the small slice of rows at/after the cutoff. The outer
203+ * `LIMIT` trims any ties at the cutoff timestamp.
204+ */
190205export function logsListQuery ( opts : LogsListOpts ) {
206+ const limit = opts . limit ?? 50
207+
208+ const baseWhere = ( $ : ColumnAccessor < typeof Logs . columns > ) : Array < CH . Condition | undefined > => [
209+ $ . OrgId . eq ( param . string ( "orgId" ) ) ,
210+ $ . TimestampTime . gte ( param . dateTime ( "startTime" ) ) ,
211+ $ . TimestampTime . lte ( param . dateTime ( "endTime" ) ) ,
212+ $ . Timestamp . gte ( param . dateTime ( "startTime" ) ) ,
213+ $ . Timestamp . lte ( param . dateTime ( "endTime" ) ) ,
214+ CH . when ( opts . serviceName , ( v : string ) => $ . ServiceName . eq ( v ) ) ,
215+ CH . when ( opts . severity , ( v : string ) => $ . SeverityText . eq ( v ) ) ,
216+ CH . when ( opts . minSeverity , ( v : number ) => $ . SeverityNumber . gte ( v ) ) ,
217+ CH . when ( opts . traceId , ( v : string ) => $ . TraceId . eq ( v ) ) ,
218+ CH . when ( opts . spanId , ( v : string ) => $ . SpanId . eq ( v ) ) ,
219+ CH . when ( opts . cursor , ( v : string ) => $ . Timestamp . lt ( v ) ) ,
220+ CH . when ( opts . search , ( v : string ) => $ . Body . ilike ( `%${ v } %` ) ) ,
221+ environmentCondition ( $ , opts ) ,
222+ ]
223+
224+ // Stage 1: cheap scan — only `Timestamp` is read. Compiled with placeholders
225+ // intact ({} params) so the outer `CH.compile()` substitutes them once.
226+ const cutoffInner = from ( Logs )
227+ . select ( ( $ ) => ( { ts : $ . Timestamp } ) )
228+ . where ( baseWhere )
229+ . orderBy ( [ "ts" , "desc" ] )
230+ . limit ( limit )
231+ const cutoffSql = compileCH ( cutoffInner , { } , { skipFormat : true } ) . sql
232+ const cutoff = CH . rawExpr < string > ( `(SELECT min(ts) FROM (${ cutoffSql } ))` )
233+
234+ // Stage 2: heavy columns read only for rows at/after the cutoff timestamp.
191235 return from ( Logs )
192236 . select ( ( $ ) => ( {
193237 timestamp : $ . Timestamp ,
@@ -200,23 +244,9 @@ export function logsListQuery(opts: LogsListOpts) {
200244 logAttributes : CH . toJSONString ( $ . LogAttributes ) ,
201245 resourceAttributes : CH . toJSONString ( $ . ResourceAttributes ) ,
202246 } ) )
203- . where ( ( $ ) => [
204- $ . OrgId . eq ( param . string ( "orgId" ) ) ,
205- $ . TimestampTime . gte ( param . dateTime ( "startTime" ) ) ,
206- $ . TimestampTime . lte ( param . dateTime ( "endTime" ) ) ,
207- $ . Timestamp . gte ( param . dateTime ( "startTime" ) ) ,
208- $ . Timestamp . lte ( param . dateTime ( "endTime" ) ) ,
209- CH . when ( opts . serviceName , ( v : string ) => $ . ServiceName . eq ( v ) ) ,
210- CH . when ( opts . severity , ( v : string ) => $ . SeverityText . eq ( v ) ) ,
211- CH . when ( opts . minSeverity , ( v : number ) => $ . SeverityNumber . gte ( v ) ) ,
212- CH . when ( opts . traceId , ( v : string ) => $ . TraceId . eq ( v ) ) ,
213- CH . when ( opts . spanId , ( v : string ) => $ . SpanId . eq ( v ) ) ,
214- CH . when ( opts . cursor , ( v : string ) => $ . Timestamp . lt ( v ) ) ,
215- CH . when ( opts . search , ( v : string ) => $ . Body . ilike ( `%${ v } %` ) ) ,
216- environmentCondition ( $ , opts ) ,
217- ] )
247+ . where ( ( $ ) => [ ...baseWhere ( $ ) , $ . Timestamp . gte ( cutoff ) ] )
218248 . orderBy ( [ "timestamp" , "desc" ] )
219- . limit ( opts . limit ?? 50 )
249+ . limit ( limit )
220250 . format ( "JSON" )
221251}
222252
0 commit comments