Skip to content

Commit fa45875

Browse files
authored
feat: Add delta() function for gauge metrics (#1147)
1 parent 5d567b9 commit fa45875

File tree

9 files changed

+327
-88
lines changed

9 files changed

+327
-88
lines changed

.changeset/honest-pens-bathe.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": minor
3+
"@hyperdx/app": minor
4+
---
5+
6+
Add delta() function for gauge metrics

packages/api/src/clickhouse/__tests__/__snapshots__/renderChartConfig.test.ts.snap

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,44 @@ Array [
5959
]
6060
`;
6161

62+
exports[`renderChartConfig Query Metrics - Gauge single max gauge with delta 1`] = `
63+
Array [
64+
Object {
65+
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
66+
"max(toFloat64OrDefault(toString(LastValue)))": 5,
67+
},
68+
Object {
69+
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
70+
"max(toFloat64OrDefault(toString(LastValue)))": -1.6666666666666667,
71+
},
72+
]
73+
`;
74+
75+
exports[`renderChartConfig Query Metrics - Gauge single max gauge with delta and group by 1`] = `
76+
Array [
77+
Object {
78+
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
79+
"arrayElement(ResourceAttributes, 'host')": "host2",
80+
"max(toFloat64OrDefault(toString(LastValue)))": 5,
81+
},
82+
Object {
83+
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
84+
"arrayElement(ResourceAttributes, 'host')": "host1",
85+
"max(toFloat64OrDefault(toString(LastValue)))": -72.91666666666667,
86+
},
87+
Object {
88+
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
89+
"arrayElement(ResourceAttributes, 'host')": "host2",
90+
"max(toFloat64OrDefault(toString(LastValue)))": -1.6666666666666667,
91+
},
92+
Object {
93+
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
94+
"arrayElement(ResourceAttributes, 'host')": "host1",
95+
"max(toFloat64OrDefault(toString(LastValue)))": -33.333333333333336,
96+
},
97+
]
98+
`;
99+
62100
exports[`renderChartConfig Query Metrics - Gauge single max/avg/sum gauge 1`] = `
63101
Array [
64102
Object {

packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,57 @@ describe('renderChartConfig', () => {
438438
);
439439
expect(await queryData(query)).toMatchSnapshot();
440440
});
441+
442+
it('single max gauge with delta', async () => {
443+
const query = await renderChartConfig(
444+
{
445+
select: [
446+
{
447+
aggFn: 'max',
448+
metricName: 'test.cpu',
449+
metricType: MetricsDataType.Gauge,
450+
valueExpression: 'Value',
451+
isDelta: true,
452+
},
453+
],
454+
from: metricSource.from,
455+
where: '',
456+
metricTables: TEST_METRIC_TABLES,
457+
dateRange: [new Date(now), new Date(now + ms('10m'))],
458+
granularity: '5 minute',
459+
timestampValueExpression: metricSource.timestampValueExpression,
460+
connection: connection.id,
461+
},
462+
metadata,
463+
);
464+
expect(await queryData(query)).toMatchSnapshot();
465+
});
466+
467+
it('single max gauge with delta and group by', async () => {
468+
const query = await renderChartConfig(
469+
{
470+
select: [
471+
{
472+
aggFn: 'max',
473+
metricName: 'test.cpu',
474+
metricType: MetricsDataType.Gauge,
475+
valueExpression: 'Value',
476+
isDelta: true,
477+
},
478+
],
479+
from: metricSource.from,
480+
where: '',
481+
metricTables: TEST_METRIC_TABLES,
482+
dateRange: [new Date(now), new Date(now + ms('10m'))],
483+
granularity: '5 minute',
484+
groupBy: `ResourceAttributes['host']`,
485+
timestampValueExpression: metricSource.timestampValueExpression,
486+
connection: connection.id,
487+
},
488+
metadata,
489+
);
490+
expect(await queryData(query)).toMatchSnapshot();
491+
});
441492
});
442493

443494
describe('Query Metrics - Sum', () => {

packages/app/src/components/DBEditTimeChartForm.tsx

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ import HDXMarkdownChart from '../HDXMarkdownChart';
7575

7676
import { AggFnSelectControlled } from './AggFnSelect';
7777
import DBNumberChart from './DBNumberChart';
78-
import { InputControlled, TextInputControlled } from './InputControlled';
78+
import {
79+
CheckBoxControlled,
80+
InputControlled,
81+
TextInputControlled,
82+
} from './InputControlled';
7983
import { MetricNameSelect } from './MetricNameSelect';
8084
import { NumberFormatInput } from './NumberFormat';
8185
import { SourceSelectControlled } from './SourceSelect';
@@ -202,7 +206,7 @@ function ChartSeriesEditorComponent({
202206
mb={8}
203207
mt="sm"
204208
/>
205-
<Flex gap="sm" mt="xs" align="center">
209+
<Flex gap="sm" mt="xs" align="start">
206210
<div
207211
style={{
208212
minWidth: 200,
@@ -216,17 +220,32 @@ function ChartSeriesEditorComponent({
216220
/>
217221
</div>
218222
{tableSource?.kind === SourceKind.Metric && (
219-
<MetricNameSelect
220-
metricName={metricName}
221-
dateRange={dateRange}
222-
metricType={metricType}
223-
setMetricName={value => {
224-
setValue(`${namePrefix}metricName`, value);
225-
setValue(`${namePrefix}valueExpression`, 'Value');
226-
}}
227-
setMetricType={value => setValue(`${namePrefix}metricType`, value)}
228-
metricSource={tableSource}
229-
/>
223+
<div style={{ minWidth: 220 }}>
224+
<MetricNameSelect
225+
metricName={metricName}
226+
dateRange={dateRange}
227+
metricType={metricType}
228+
setMetricName={value => {
229+
setValue(`${namePrefix}metricName`, value);
230+
setValue(`${namePrefix}valueExpression`, 'Value');
231+
}}
232+
setMetricType={value =>
233+
setValue(`${namePrefix}metricType`, value)
234+
}
235+
metricSource={tableSource}
236+
/>
237+
{metricType === 'gauge' && (
238+
<Flex justify="end">
239+
<CheckBoxControlled
240+
control={control}
241+
name={`${namePrefix}isDelta`}
242+
label="Delta"
243+
size="xs"
244+
className="mt-2"
245+
/>
246+
</Flex>
247+
)}
248+
</div>
230249
)}
231250
{tableSource?.kind !== SourceKind.Metric && aggFn !== 'count' && (
232251
<div style={{ minWidth: 220 }}>
@@ -243,44 +262,46 @@ function ChartSeriesEditorComponent({
243262
/>
244263
</div>
245264
)}
246-
<Text size="sm">Where</Text>
247-
{aggConditionLanguage === 'sql' ? (
248-
<SQLInlineEditorControlled
249-
tableConnections={{
250-
databaseName,
251-
tableName: tableName ?? '',
252-
connectionId: connectionId ?? '',
253-
}}
254-
control={control}
255-
name={`${namePrefix}aggCondition`}
256-
placeholder="SQL WHERE clause (ex. column = 'foo')"
257-
onLanguageChange={lang =>
258-
setValue(`${namePrefix}aggConditionLanguage`, lang)
259-
}
260-
additionalSuggestions={attributeKeys}
261-
language="sql"
262-
onSubmit={onSubmit}
263-
/>
264-
) : (
265-
<SearchInputV2
266-
tableConnections={{
267-
connectionId: connectionId ?? '',
268-
databaseName: databaseName ?? '',
269-
tableName: tableName ?? '',
270-
}}
271-
control={control}
272-
name={`${namePrefix}aggCondition`}
273-
onLanguageChange={lang =>
274-
setValue(`${namePrefix}aggConditionLanguage`, lang)
275-
}
276-
language="lucene"
277-
placeholder="Search your events w/ Lucene ex. column:foo"
278-
onSubmit={onSubmit}
279-
additionalSuggestions={attributeKeys}
280-
/>
281-
)}
265+
<Flex align={'center'} gap={'xs'} className="flex-grow-1">
266+
<Text size="sm">Where</Text>
267+
{aggConditionLanguage === 'sql' ? (
268+
<SQLInlineEditorControlled
269+
tableConnections={{
270+
databaseName,
271+
tableName: tableName ?? '',
272+
connectionId: connectionId ?? '',
273+
}}
274+
control={control}
275+
name={`${namePrefix}aggCondition`}
276+
placeholder="SQL WHERE clause (ex. column = 'foo')"
277+
onLanguageChange={lang =>
278+
setValue(`${namePrefix}aggConditionLanguage`, lang)
279+
}
280+
additionalSuggestions={attributeKeys}
281+
language="sql"
282+
onSubmit={onSubmit}
283+
/>
284+
) : (
285+
<SearchInputV2
286+
tableConnections={{
287+
connectionId: connectionId ?? '',
288+
databaseName: databaseName ?? '',
289+
tableName: tableName ?? '',
290+
}}
291+
control={control}
292+
name={`${namePrefix}aggCondition`}
293+
onLanguageChange={lang =>
294+
setValue(`${namePrefix}aggConditionLanguage`, lang)
295+
}
296+
language="lucene"
297+
placeholder="Search your events w/ Lucene ex. column:foo"
298+
onSubmit={onSubmit}
299+
additionalSuggestions={attributeKeys}
300+
/>
301+
)}
302+
</Flex>
282303
{showGroupBy && (
283-
<>
304+
<Flex align={'center'} gap={'xs'}>
284305
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
285306
Group By
286307
</Text>
@@ -303,7 +324,7 @@ function ChartSeriesEditorComponent({
303324
onSubmit={onSubmit}
304325
/>
305326
</div>
306-
</>
327+
</Flex>
307328
)}
308329
</Flex>
309330
</>

packages/app/src/components/InputControlled.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
22
import { Control, Controller, FieldValues, Path } from 'react-hook-form';
33
import {
4+
Checkbox,
5+
CheckboxProps,
46
Input,
57
InputProps,
68
PasswordInput,
@@ -33,6 +35,17 @@ interface TextInputControlledProps<T extends FieldValues>
3335
rules?: Parameters<Control<T>['register']>[1];
3436
}
3537

38+
interface CheckboxControlledProps<T extends FieldValues>
39+
extends Omit<CheckboxProps, 'name' | 'style'>,
40+
Omit<
41+
React.InputHTMLAttributes<HTMLInputElement>,
42+
'name' | 'size' | 'color'
43+
> {
44+
name: Path<T>;
45+
control: Control<T>;
46+
rules?: Parameters<Control<T>['register']>[1];
47+
}
48+
3649
export function TextInputControlled<T extends FieldValues>({
3750
name,
3851
control,
@@ -86,3 +99,26 @@ export function PasswordInputControlled<T extends FieldValues>({
8699
/>
87100
);
88101
}
102+
103+
export function CheckBoxControlled<T extends FieldValues>({
104+
name,
105+
control,
106+
rules,
107+
...props
108+
}: CheckboxControlledProps<T>) {
109+
return (
110+
<Controller
111+
name={name}
112+
control={control}
113+
rules={rules}
114+
render={({ field: { value, ...field }, fieldState: { error } }) => (
115+
<Checkbox
116+
{...props}
117+
{...field}
118+
checked={value}
119+
error={error?.message}
120+
/>
121+
)}
122+
/>
123+
);
124+
}

packages/common-utils/src/__tests__/__snapshots__/renderChartConfig.test.ts.snap

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,40 @@ exports[`renderChartConfig should generate sql for a single gauge metric 1`] = `
284284
FROM Source
285285
GROUP BY AttributesHash, __hdx_time_bucket2
286286
ORDER BY AttributesHash, __hdx_time_bucket2
287-
) SELECT quantile(0.95)(toFloat64OrDefault(toString(LastValue))),toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` FROM Bucketed WHERE (__hdx_time_bucket2 >= fromUnixTimestamp64Milli(1739318400000) AND __hdx_time_bucket2 <= fromUnixTimestamp64Milli(1765670400000)) GROUP BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` ORDER BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` LIMIT 10"
287+
) SELECT quantile(0.95)(toFloat64OrDefault(toString(LastValue))),toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` FROM Bucketed WHERE (__hdx_time_bucket2 >= fromUnixTimestamp64Milli(1739318400000) AND __hdx_time_bucket2 <= fromUnixTimestamp64Milli(1765670400000)) GROUP BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` ORDER BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
288+
`;
289+
290+
exports[`renderChartConfig should generate sql for a single gauge metric with a delta() function applied 1`] = `
291+
"WITH Source AS (
292+
SELECT
293+
*,
294+
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS AttributesHash
295+
FROM default.otel_metrics_gauge
296+
WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000)) AND ((MetricName = 'nodejs.event_loop.utilization'))
297+
),Bucketed AS (
298+
SELECT
299+
toStartOfInterval(toDateTime(TimeUnix), INTERVAL 1 minute) AS \`__hdx_time_bucket2\`,
300+
AttributesHash,
301+
IF(date_diff('second', min(toDateTime(TimeUnix)), max(toDateTime(TimeUnix))) > 0, (argMax(Value, TimeUnix) - argMin(Value, TimeUnix)) * 60 / date_diff('second', min(toDateTime(TimeUnix)), max(toDateTime(TimeUnix))), 0) AS LastValue,
302+
any(ScopeAttributes) AS ScopeAttributes,
303+
any(ResourceAttributes) AS ResourceAttributes,
304+
any(Attributes) AS Attributes,
305+
any(ResourceSchemaUrl) AS ResourceSchemaUrl,
306+
any(ScopeName) AS ScopeName,
307+
any(ScopeVersion) AS ScopeVersion,
308+
any(ScopeDroppedAttrCount) AS ScopeDroppedAttrCount,
309+
any(ScopeSchemaUrl) AS ScopeSchemaUrl,
310+
any(ServiceName) AS ServiceName,
311+
any(MetricDescription) AS MetricDescription,
312+
any(MetricUnit) AS MetricUnit,
313+
any(StartTimeUnix) AS StartTimeUnix,
314+
any(Flags) AS Flags
315+
FROM Source
316+
GROUP BY AttributesHash, __hdx_time_bucket2
317+
ORDER BY AttributesHash, __hdx_time_bucket2
318+
) SELECT max(
319+
toFloat64OrDefault(toString(LastValue))
320+
),toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` FROM Bucketed WHERE (__hdx_time_bucket2 >= fromUnixTimestamp64Milli(1739318400000) AND __hdx_time_bucket2 <= fromUnixTimestamp64Milli(1765670400000)) GROUP BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` ORDER BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\` LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
288321
`;
289322

290323
exports[`renderChartConfig should generate sql for a single sum metric 1`] = `

0 commit comments

Comments
 (0)