2025-02-26 00:00:48 +00:00
import { parameterizedQueryToSql } from '@/clickhouse' ;
import { Metadata } from '@/metadata' ;
import {
ChartConfigWithOptDateRange ,
DisplayType ,
MetricsDataType ,
} from '@/types' ;
import { renderChartConfig } from '../renderChartConfig' ;
describe ( 'renderChartConfig' , ( ) = > {
let mockMetadata : Metadata ;
beforeEach ( ( ) = > {
mockMetadata = {
getColumns : jest.fn ( ) . mockResolvedValue ( [
{ name : 'timestamp' , type : 'DateTime' } ,
{ name : 'value' , type : 'Float64' } ,
] ) ,
getMaterializedColumnsLookupTable : jest.fn ( ) . mockResolvedValue ( { } ) ,
getColumn : jest.fn ( ) . mockResolvedValue ( { type : 'DateTime' } ) ,
} as unknown as Metadata ;
} ) ;
it ( 'should generate sql for a single gauge metric' , async ( ) = > {
const config : ChartConfigWithOptDateRange = {
displayType : DisplayType.Line ,
connection : 'test-connection' ,
// metricTables is added from the Source object via spread operator
metricTables : {
gauge : 'otel_metrics_gauge' ,
histogram : 'otel_metrics_histogram' ,
sum : 'otel_metrics_sum' ,
} ,
from : {
databaseName : 'default' ,
tableName : '' ,
} ,
select : [
{
aggFn : 'quantile' ,
aggCondition : '' ,
aggConditionLanguage : 'lucene' ,
valueExpression : 'Value' ,
level : 0.95 ,
metricName : 'nodejs.event_loop.utilization' ,
metricType : MetricsDataType.Gauge ,
} ,
] ,
where : '' ,
whereLanguage : 'lucene' ,
timestampValueExpression : 'TimeUnix' ,
dateRange : [ new Date ( '2025-02-12' ) , new Date ( '2025-12-14' ) ] ,
granularity : '1 minute' ,
limit : { limit : 10 } ,
} ;
const generatedSql = await renderChartConfig ( config , mockMetadata ) ;
const actual = parameterizedQueryToSql ( generatedSql ) ;
expect ( actual ) . toBe (
'SELECT quantile(0.95)(toFloat64OrNull(toString(Value))),toStartOfInterval(toDateTime(TimeUnix), INTERVAL 1 minute) AS `__hdx_time_bucket`' +
' FROM default.otel_metrics_gauge WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000)) AND' +
2025-02-27 16:55:36 +00:00
" (MetricName = 'nodejs.event_loop.utilization') GROUP BY toStartOfInterval(toDateTime(TimeUnix), INTERVAL 1 minute) AS `__hdx_time_bucket`" +
' ORDER BY toStartOfInterval(toDateTime(TimeUnix), INTERVAL 1 minute) AS `__hdx_time_bucket`' +
' WITH FILL FROM toUnixTimestamp(toStartOfInterval(fromUnixTimestamp64Milli(1739318400000), INTERVAL 1 minute))\n' +
' TO toUnixTimestamp(toStartOfInterval(fromUnixTimestamp64Milli(1765670400000), INTERVAL 1 minute))\n' +
' STEP 60' +
' LIMIT 10' ,
2025-02-26 00:00:48 +00:00
) ;
} ) ;
it ( 'should generate sql for a single sum metric' , async ( ) = > {
const config : ChartConfigWithOptDateRange = {
displayType : DisplayType.Line ,
connection : 'test-connection' ,
// metricTables is added from the Source object via spread operator
metricTables : {
gauge : 'otel_metrics_gauge' ,
histogram : 'otel_metrics_histogram' ,
sum : 'otel_metrics_sum' ,
} ,
from : {
databaseName : 'default' ,
tableName : '' ,
} ,
select : [
{
aggFn : 'avg' ,
aggCondition : '' ,
aggConditionLanguage : 'lucene' ,
valueExpression : 'Value' ,
metricName : 'db.client.connections.usage' ,
metricType : MetricsDataType.Sum ,
} ,
] ,
where : '' ,
whereLanguage : 'sql' ,
timestampValueExpression : 'TimeUnix' ,
dateRange : [ new Date ( '2025-02-12' ) , new Date ( '2025-12-14' ) ] ,
2025-02-27 16:55:36 +00:00
granularity : '5 minute' ,
2025-02-26 00:00:48 +00:00
limit : { limit : 10 } ,
} ;
const generatedSql = await renderChartConfig ( config , mockMetadata ) ;
const actual = parameterizedQueryToSql ( generatedSql ) ;
expect ( actual ) . toBe (
2025-02-27 15:51:23 +00:00
'WITH RawSum AS (SELECT *,\n' +
2025-02-26 00:00:48 +00:00
' any(Value) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS PrevValue,\n' +
' any(AttributesHash) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS PrevAttributesHash,\n' +
' IF(AggregationTemporality = 1,\n' +
' Value,IF(Value - PrevValue < 0 AND AttributesHash = PrevAttributesHash, Value,\n' +
' IF(AttributesHash != PrevAttributesHash, 0, Value - PrevValue))) as Rate\n' +
' FROM (\n' +
2025-02-27 15:51:23 +00:00
' SELECT *, \n' +
' cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS AttributesHash\n' +
2025-02-26 00:00:48 +00:00
' FROM default.otel_metrics_sum\n' +
" WHERE MetricName = 'db.client.connections.usage'\n" +
2025-02-27 15:51:23 +00:00
' ORDER BY AttributesHash, TimeUnix ASC\n' +
2025-02-27 16:55:36 +00:00
' ) ) SELECT avg(\n' +
2025-02-26 00:00:48 +00:00
' toFloat64OrNull(toString(Rate))\n' +
2025-02-27 16:55:36 +00:00
' ),toStartOfInterval(toDateTime(TimeUnix), INTERVAL 5 minute) AS `__hdx_time_bucket`' +
' FROM RawSum WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000))' +
' GROUP BY toStartOfInterval(toDateTime(TimeUnix), INTERVAL 5 minute) AS `__hdx_time_bucket`' +
' ORDER BY toStartOfInterval(toDateTime(TimeUnix), INTERVAL 5 minute) AS `__hdx_time_bucket`' +
' WITH FILL FROM toUnixTimestamp(toStartOfInterval(fromUnixTimestamp64Milli(1739318400000), INTERVAL 5 minute))\n' +
' TO toUnixTimestamp(toStartOfInterval(fromUnixTimestamp64Milli(1765670400000), INTERVAL 5 minute))\n' +
' STEP 300' +
' LIMIT 10' ,
) ;
} ) ;
it ( 'should generate sql for a single histogram metric' , async ( ) = > {
const config : ChartConfigWithOptDateRange = {
displayType : DisplayType.Line ,
connection : 'test-connection' ,
// metricTables is added from the Source object via spread operator
metricTables : {
gauge : 'otel_metrics_gauge' ,
histogram : 'otel_metrics_histogram' ,
sum : 'otel_metrics_sum' ,
} ,
from : {
databaseName : 'default' ,
tableName : '' ,
} ,
select : [
{
aggFn : 'quantile' ,
level : 0.5 ,
valueExpression : 'Value' ,
metricName : 'http.server.duration' ,
metricType : MetricsDataType.Histogram ,
} ,
] ,
where : '' ,
whereLanguage : 'sql' ,
timestampValueExpression : 'TimeUnix' ,
dateRange : [ new Date ( '2025-02-12' ) , new Date ( '2025-12-14' ) ] ,
limit : { limit : 10 } ,
} ;
const generatedSql = await renderChartConfig ( config , mockMetadata ) ;
const actual = parameterizedQueryToSql ( generatedSql ) ;
expect ( actual ) . toBe (
'WITH HistRate AS (SELECT *, any(BucketCounts) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS PrevBucketCounts,\n' +
' any(CountLength) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS PrevCountLength,\n' +
' any(AttributesHash) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS PrevAttributesHash,\n' +
' IF(AggregationTemporality = 1,\n' +
' BucketCounts,\n' +
' IF(AttributesHash = PrevAttributesHash AND CountLength = PrevCountLength,\n' +
' arrayMap((prev, curr) -> IF(curr < prev, curr, toUInt64(toInt64(curr) - toInt64(prev))), PrevBucketCounts, BucketCounts),\n' +
' BucketCounts)) as BucketRates\n' +
' FROM (\n' +
' SELECT *, cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS AttributesHash,\n' +
' length(BucketCounts) as CountLength\n' +
' FROM default.otel_metrics_histogram)\n' +
" WHERE MetricName = 'http.server.duration'\n " +
' ORDER BY Attributes, TimeUnix ASC\n' +
' ),RawHist AS (\n' +
' SELECT *, toUInt64( 0.5 * arraySum(BucketRates)) AS Rank,\n' +
' arrayCumSum(BucketRates) as CumRates,\n' +
' arrayFirstIndex(x -> if(x > Rank, 1, 0), CumRates) AS BucketLowIdx,\n' +
' IF(BucketLowIdx = length(BucketRates),\n' +
' ExplicitBounds[length(ExplicitBounds)], -- if the low bound is the last bucket, use the last bound value\n' +
' IF(BucketLowIdx > 1, -- indexes are 1-based\n' +
' ExplicitBounds[BucketLowIdx] + (ExplicitBounds[BucketLowIdx + 1] - ExplicitBounds[BucketLowIdx]) *\n' +
' intDivOrZero(\n' +
' Rank - CumRates[BucketLowIdx - 1],\n' +
' CumRates[BucketLowIdx] - CumRates[BucketLowIdx - 1]),\n' +
' arrayElement(ExplicitBounds, BucketLowIdx + 1) * intDivOrZero(Rank, CumRates[BucketLowIdx]))) as Rate\n' +
' FROM HistRate) SELECT sum(\n' +
' toFloat64OrNull(toString(Rate))\n' +
' )' +
' FROM RawHist' +
' WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000))' +
' LIMIT 10' ,
2025-02-26 00:00:48 +00:00
) ;
} ) ;
} ) ;