Skip to content

Commit 8cb0eac

Browse files
authored
Add rate aggFn support for sum metrics (#77)
1 parent 4d24bfa commit 8cb0eac

File tree

7 files changed

+376
-98
lines changed

7 files changed

+376
-98
lines changed

.changeset/empty-ears-kneel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@hyperdx/api': patch
3+
'@hyperdx/app': patch
4+
---
5+
6+
Add rate function for sum metrics

packages/api/src/clickhouse/index.ts

Lines changed: 114 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,23 @@ export type SortOrder = 'asc' | 'desc' | null;
4444

4545
export enum AggFn {
4646
Avg = 'avg',
47+
AvgRate = 'avg_rate',
4748
Count = 'count',
4849
CountDistinct = 'count_distinct',
4950
Max = 'max',
51+
MaxRate = 'max_rate',
5052
Min = 'min',
53+
MinRate = 'min_rate',
5154
P50 = 'p50',
55+
P50Rate = 'p50_rate',
5256
P90 = 'p90',
57+
P90Rate = 'p90_rate',
5358
P95 = 'p95',
59+
P95Rate = 'p95_rate',
5460
P99 = 'p99',
61+
P99Rate = 'p99_rate',
5562
Sum = 'sum',
63+
SumRate = 'sum_rate',
5664
}
5765

5866
export enum Granularity {
@@ -621,6 +629,19 @@ export const getMetricsTags = async (teamId: string) => {
621629
return result;
622630
};
623631

632+
const isRateAggFn = (aggFn: AggFn) => {
633+
return (
634+
aggFn === AggFn.SumRate ||
635+
aggFn === AggFn.AvgRate ||
636+
aggFn === AggFn.MaxRate ||
637+
aggFn === AggFn.MinRate ||
638+
aggFn === AggFn.P50Rate ||
639+
aggFn === AggFn.P90Rate ||
640+
aggFn === AggFn.P95Rate ||
641+
aggFn === AggFn.P99Rate
642+
);
643+
};
644+
624645
export const getMetricsChart = async ({
625646
aggFn,
626647
dataType,
@@ -663,78 +684,100 @@ export const getMetricsChart = async ({
663684
: 'name AS group',
664685
];
665686

666-
switch (dataType) {
667-
case 'Gauge':
668-
selectClause.push(
669-
aggFn === AggFn.Count
670-
? 'COUNT(value) as data'
671-
: aggFn === AggFn.Sum
672-
? `SUM(value) as data`
673-
: aggFn === AggFn.Avg
674-
? `AVG(value) as data`
675-
: aggFn === AggFn.Max
676-
? `MAX(value) as data`
677-
: aggFn === AggFn.Min
678-
? `MIN(value) as data`
679-
: `quantile(${
680-
aggFn === AggFn.P50
681-
? '0.5'
682-
: aggFn === AggFn.P90
683-
? '0.90'
684-
: aggFn === AggFn.P95
685-
? '0.95'
686-
: '0.99'
687-
})(value) as data`,
688-
);
689-
break;
690-
case 'Sum':
691-
selectClause.push(
692-
aggFn === AggFn.Count
693-
? 'COUNT(delta) as data'
694-
: aggFn === AggFn.Sum
695-
? `SUM(delta) as data`
696-
: aggFn === AggFn.Avg
697-
? `AVG(delta) as data`
698-
: aggFn === AggFn.Max
699-
? `MAX(delta) as data`
700-
: aggFn === AggFn.Min
701-
? `MIN(delta) as data`
702-
: `quantile(${
703-
aggFn === AggFn.P50
704-
? '0.5'
705-
: aggFn === AggFn.P90
706-
? '0.90'
707-
: aggFn === AggFn.P95
708-
? '0.95'
709-
: '0.99'
710-
})(delta) as data`,
711-
);
712-
break;
713-
default:
714-
logger.error(`Unsupported data type: ${dataType}`);
715-
break;
687+
const isRate = isRateAggFn(aggFn);
688+
689+
if (dataType === 'Gauge' || dataType === 'Sum') {
690+
selectClause.push(
691+
aggFn === AggFn.Count
692+
? 'COUNT(value) as data'
693+
: aggFn === AggFn.Sum
694+
? `SUM(value) as data`
695+
: aggFn === AggFn.Avg
696+
? `AVG(value) as data`
697+
: aggFn === AggFn.Max
698+
? `MAX(value) as data`
699+
: aggFn === AggFn.Min
700+
? `MIN(value) as data`
701+
: aggFn === AggFn.SumRate
702+
? `SUM(rate) as data`
703+
: aggFn === AggFn.AvgRate
704+
? `AVG(rate) as data`
705+
: aggFn === AggFn.MaxRate
706+
? `MAX(rate) as data`
707+
: aggFn === AggFn.MinRate
708+
? `MIN(rate) as data`
709+
: `quantile(${
710+
aggFn === AggFn.P50 || aggFn === AggFn.P50Rate
711+
? '0.5'
712+
: aggFn === AggFn.P90 || aggFn === AggFn.P90Rate
713+
? '0.90'
714+
: aggFn === AggFn.P95 || aggFn === AggFn.P95Rate
715+
? '0.95'
716+
: '0.99'
717+
})(${isRate ? 'rate' : 'value'}) as data`,
718+
);
719+
} else {
720+
logger.error(`Unsupported data type: ${dataType}`);
716721
}
717722

723+
const rateMetricSource = SqlString.format(
724+
`
725+
SELECT
726+
if(
727+
runningDifference(value) < 0
728+
OR neighbor(_string_attributes, -1, _string_attributes) != _string_attributes,
729+
nan,
730+
runningDifference(value)
731+
) AS rate,
732+
ts_bucket as timestamp,
733+
_string_attributes,
734+
min_name as name
735+
FROM
736+
(
737+
SELECT
738+
toStartOfInterval(timestamp, INTERVAL ?) as ts_bucket,
739+
min(value) as value,
740+
_string_attributes,
741+
min(name) as min_name
742+
FROM
743+
??
744+
WHERE
745+
name = ?
746+
AND data_type = ?
747+
AND (?)
748+
GROUP BY
749+
_string_attributes,
750+
ts_bucket
751+
ORDER BY
752+
_string_attributes,
753+
ts_bucket ASC
754+
)
755+
`.trim(),
756+
[granularity, tableName, name, dataType, SqlString.raw(whereClause)],
757+
);
758+
759+
const gaugeMetricSource = SqlString.format(
760+
`
761+
SELECT
762+
timestamp,
763+
name,
764+
value,
765+
_string_attributes
766+
FROM ??
767+
WHERE name = ?
768+
AND data_type = ?
769+
AND (?)
770+
ORDER BY _timestamp_sort_key ASC
771+
`.trim(),
772+
[tableName, name, dataType, SqlString.raw(whereClause)],
773+
);
774+
718775
// TODO: support other data types like Sum, Histogram, etc.
719776
const query = SqlString.format(
720777
`
721-
WITH metrcis AS (
722-
SELECT *, runningDifference(value) AS delta
723-
FROM (
724-
SELECT
725-
timestamp,
726-
name,
727-
value,
728-
_string_attributes
729-
FROM ??
730-
WHERE name = ?
731-
AND data_type = ?
732-
AND (?)
733-
ORDER BY _timestamp_sort_key ASC
734-
)
735-
)
778+
WITH metrics AS (?)
736779
SELECT ?
737-
FROM metrcis
780+
FROM metrics
738781
GROUP BY group, ts_bucket
739782
ORDER BY ts_bucket ASC
740783
WITH FILL
@@ -743,10 +786,7 @@ export const getMetricsChart = async ({
743786
STEP ?
744787
`,
745788
[
746-
tableName,
747-
name,
748-
dataType,
749-
SqlString.raw(whereClause),
789+
SqlString.raw(isRate ? rateMetricSource : gaugeMetricSource),
750790
SqlString.raw(selectClause.join(',')),
751791
startTime / 1000,
752792
granularity,
@@ -798,6 +838,10 @@ export const getLogsChart = async ({
798838
tableVersion: number | undefined;
799839
teamId: string;
800840
}) => {
841+
if (isRateAggFn(aggFn)) {
842+
throw new Error('Rate is not supported in logs chart');
843+
}
844+
801845
const tableName = getLogStreamTableName(tableVersion, teamId);
802846
const whereClause = await buildSearchQueryWhereCondition({
803847
endTime,

packages/app/src/ChartPage.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Head from 'next/head';
22
import { Button } from 'react-bootstrap';
33
import { useQueryParam, StringParam, withDefault } from 'use-query-params';
4-
import { useState } from 'react';
4+
import { useCallback, useEffect, useState } from 'react';
55
import { encodeArray, decodeArray } from 'serialize-query-params';
66
import produce from 'immer';
77

@@ -86,6 +86,20 @@ export default function GraphPage() {
8686
}),
8787
);
8888
};
89+
const setFieldAndAggFn = (
90+
index: number,
91+
field: string | undefined,
92+
fn: AggFn,
93+
) => {
94+
setChartSeries(
95+
produce(chartSeries, series => {
96+
if (series?.[index] != null) {
97+
series[index].field = field;
98+
series[index].aggFn = fn;
99+
}
100+
}),
101+
);
102+
};
89103
const setWhere = (index: number, where: string) => {
90104
setChartSeries(
91105
produce(chartSeries, series => {
@@ -132,7 +146,7 @@ export default function GraphPage() {
132146
],
133147
});
134148

135-
const onRunQuery = () => {
149+
const onRunQuery = useCallback(() => {
136150
onSearch(displayedTimeInputValue);
137151
const dateRange = parseTimeRangeInput(displayedTimeInputValue);
138152

@@ -150,7 +164,7 @@ export default function GraphPage() {
150164
} else {
151165
toast.error('Invalid time range');
152166
}
153-
};
167+
}, [chartSeries, displayedTimeInputValue, granularity, onSearch]);
154168

155169
return (
156170
<div className="LogViewerPage d-flex" style={{ height: '100vh' }}>
@@ -173,6 +187,19 @@ export default function GraphPage() {
173187
where={series.where}
174188
groupBy={series.groupBy[0]}
175189
field={series.field}
190+
setFieldAndAggFn={(field, aggFn) =>
191+
setFieldAndAggFn(index, field, aggFn)
192+
}
193+
setTableAndAggFn={(table, aggFn) => {
194+
setChartSeries(
195+
produce(chartSeries, series => {
196+
if (series?.[index] != null) {
197+
series[index].table = table;
198+
series[index].aggFn = aggFn;
199+
}
200+
}),
201+
);
202+
}}
176203
setTable={table => setTable(index, table)}
177204
setWhere={where => setWhere(index, where)}
178205
setAggFn={fn => setAggFn(index, fn)}

0 commit comments

Comments
 (0)