@@ -14,6 +14,16 @@ import logger from '@/utils/logger';
1414export const PROMETHEUS_MAX_EXECUTION_SEC = 30 ;
1515export const PROMETHEUS_MAX_RESULT_ROWS = 100000 ;
1616
17+ export type TimeSeriesTagsQueryArgs = {
18+ client : ClickhouseClient ;
19+ connectionId : string ;
20+ databaseName : string ;
21+ tableName : string ;
22+ startMs ?: number ;
23+ endMs ?: number ;
24+ limit ?: number ;
25+ } ;
26+
1727/**
1828 * Indicates whether the tags inner table associated with the given TimeSeries table
1929 * includes the optional `min_time` and `max_time` columns.
@@ -49,64 +59,61 @@ async function timeSeriesTagsTableHasTimeBounds({
4959}
5060
5161/**
52- * Queries distinct values for the given label name from the given TimeSeries table,
53- * optionally filtering by a time range (if the table has the optional time range columns) .
62+ * Returns SQL predicates restricting tags rows to series that overlap [startMs, endMs].
63+ * Empty when no bound is given, or when the table stores no time bounds .
5464 */
55- export async function queryLabelValues ( {
65+ async function getTimeFilterCondition ( {
5666 client,
57- databaseName,
5867 connectionId,
68+ databaseName,
5969 tableName,
60- labelName,
6170 startMs,
6271 endMs,
63- limit,
64- } : {
65- client : ClickhouseClient ;
66- connectionId : string ;
67- databaseName : string ;
68- tableName : string ;
69- labelName : string ;
70- startMs ?: number ;
71- endMs ?: number ;
72- limit ?: number ;
73- } ) : Promise < string [ ] > {
74- const isMetricName = labelName === '__name__' ;
75- const value = isMetricName
76- ? chSql `${ { Identifier : 'metric_name' } } `
77- : chSql `${ { Identifier : 'tags' } } [${ { String : labelName } } ]` ;
78-
79- const conditions : ChSql [ ] = [ ] ;
80- if ( ! isMetricName )
81- conditions . push (
82- chSql `mapContains(${ { Identifier : 'tags' } } , ${ { String : labelName } } )` ,
83- ) ;
84-
85- const metadata = new Metadata ( client , new MetadataCache ( ) ) ;
86- const hasStartTime = startMs != null ;
87- const hasEndTime = endMs != null ;
72+ } : Omit < TimeSeriesTagsQueryArgs , 'limit' > ) : Promise < ChSql [ ] > {
73+ if ( startMs == null && endMs == null ) return [ ] ;
8874
8975 // min and max time columns are optional in the tags inner table,
9076 // so we check if they exist before adding time conditions
91- const tableHasTimeBounds =
92- ( hasStartTime || hasEndTime ) && // Short-circuit if no time conditions are needed
93- ( await timeSeriesTagsTableHasTimeBounds ( {
94- metadata ,
95- connectionId ,
96- database : databaseName ,
97- table : tableName ,
98- } ) ) ;
77+ const metadata = new Metadata ( client , new MetadataCache ( ) ) ;
78+ const tableHasTimeBounds = await timeSeriesTagsTableHasTimeBounds ( {
79+ metadata ,
80+ connectionId ,
81+ database : databaseName ,
82+ table : tableName ,
83+ } ) ;
84+ if ( ! tableHasTimeBounds ) return [ ] ;
9985
100- if ( tableHasTimeBounds && endMs != null )
86+ const conditions : ChSql [ ] = [ ] ;
87+ if ( endMs != null )
10188 conditions . push (
10289 chSql `(min_time IS NULL OR min_time <= fromUnixTimestamp64Milli(${ { Int64 : endMs } } ))` ,
10390 ) ;
104-
105- if ( tableHasTimeBounds && startMs != null )
91+ if ( startMs != null )
10692 conditions . push (
10793 chSql `(max_time IS NULL OR max_time >= fromUnixTimestamp64Milli(${ { Int64 : startMs } } ))` ,
10894 ) ;
95+ return conditions ;
96+ }
10997
98+ /**
99+ * Runs `SELECT DISTINCT <value> AS val FROM timeSeriesTags(...)` with the given
100+ * conditions, sorted, and returns the distinct values.
101+ */
102+ async function queryDistinctTagsValues ( {
103+ client,
104+ databaseName,
105+ tableName,
106+ value,
107+ conditions,
108+ limit,
109+ } : {
110+ client : ClickhouseClient ;
111+ databaseName : string ;
112+ tableName : string ;
113+ value : ChSql ;
114+ conditions : ChSql [ ] ;
115+ limit ?: number ;
116+ } ) : Promise < string [ ] > {
110117 const where = conditions . length
111118 ? chSql `WHERE ${ concatChSql ( ' AND ' , ...conditions ) } `
112119 : chSql `` ;
@@ -135,3 +142,44 @@ export async function queryLabelValues({
135142 const json = await resp . json < { val : string } > ( ) ;
136143 return json . data . map ( r => r . val ) ;
137144}
145+
146+ /**
147+ * Queries distinct values for the given label name from the given TimeSeries table,
148+ * optionally filtering by a time range (if the table has the optional time range columns).
149+ */
150+ export async function queryLabelValues ( {
151+ labelName,
152+ limit,
153+ ...args
154+ } : TimeSeriesTagsQueryArgs & { labelName : string } ) : Promise < string [ ] > {
155+ const isMetricName = labelName === '__name__' ;
156+ const value = isMetricName
157+ ? chSql `${ { Identifier : 'metric_name' } } `
158+ : chSql `${ { Identifier : 'tags' } } [${ { String : labelName } } ]` ;
159+
160+ const conditions : ChSql [ ] = [ ] ;
161+ if ( ! isMetricName )
162+ conditions . push (
163+ chSql `mapContains(${ { Identifier : 'tags' } } , ${ { String : labelName } } )` ,
164+ ) ;
165+ conditions . push ( ...( await getTimeFilterCondition ( args ) ) ) ;
166+
167+ return queryDistinctTagsValues ( { ...args , value, conditions, limit } ) ;
168+ }
169+
170+ /**
171+ * Queries the distinct label names carried by any series in the given TimeSeries
172+ * table, optionally filtering by a time range (if the table has the optional time
173+ * range columns).
174+ */
175+ export async function queryLabelNames ( {
176+ limit,
177+ ...args
178+ } : TimeSeriesTagsQueryArgs ) : Promise < string [ ] > {
179+ // `all_tags` is EPHEMERAL by default, so the label names come from `tags`,
180+ // which the engine strips `__name__` out of. It is folded back in explicitly.
181+ const value = chSql `arrayJoin(arrayConcat([${ { String : '__name__' } } ], mapKeys(${ { Identifier : 'tags' } } )))` ;
182+ const conditions = await getTimeFilterCondition ( args ) ;
183+
184+ return queryDistinctTagsValues ( { ...args , value, conditions, limit } ) ;
185+ }
0 commit comments