Skip to content

Commit 29ac4c2

Browse files
committed
feat: Add delta() function for gauge metrics
1 parent 7df2d0f commit 29ac4c2

File tree

7 files changed

+238
-88
lines changed

7 files changed

+238
-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/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`] = `

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

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,42 +22,65 @@ describe('renderChartConfig', () => {
2222
} as unknown as Metadata;
2323
});
2424

25-
it('should generate sql for a single gauge metric', async () => {
26-
const config: ChartConfigWithOptDateRange = {
27-
displayType: DisplayType.Line,
28-
connection: 'test-connection',
29-
// metricTables is added from the Source object via spread operator
30-
metricTables: {
31-
gauge: 'otel_metrics_gauge',
32-
histogram: 'otel_metrics_histogram',
33-
sum: 'otel_metrics_sum',
34-
summary: 'otel_metrics_summary',
35-
'exponential histogram': 'otel_metrics_exponential_histogram',
25+
const gaugeConfiguration: ChartConfigWithOptDateRange = {
26+
displayType: DisplayType.Line,
27+
connection: 'test-connection',
28+
// metricTables is added from the Source object via spread operator
29+
metricTables: {
30+
gauge: 'otel_metrics_gauge',
31+
histogram: 'otel_metrics_histogram',
32+
sum: 'otel_metrics_sum',
33+
summary: 'otel_metrics_summary',
34+
'exponential histogram': 'otel_metrics_exponential_histogram',
35+
},
36+
from: {
37+
databaseName: 'default',
38+
tableName: '',
39+
},
40+
select: [
41+
{
42+
aggFn: 'quantile',
43+
aggCondition: '',
44+
aggConditionLanguage: 'lucene',
45+
valueExpression: 'Value',
46+
level: 0.95,
47+
metricName: 'nodejs.event_loop.utilization',
48+
metricType: MetricsDataType.Gauge,
3649
},
37-
from: {
38-
databaseName: 'default',
39-
tableName: '',
40-
},
41-
select: [
42-
{
43-
aggFn: 'quantile',
44-
aggCondition: '',
45-
aggConditionLanguage: 'lucene',
46-
valueExpression: 'Value',
47-
level: 0.95,
48-
metricName: 'nodejs.event_loop.utilization',
49-
metricType: MetricsDataType.Gauge,
50-
},
51-
],
52-
where: '',
53-
whereLanguage: 'lucene',
54-
timestampValueExpression: 'TimeUnix',
55-
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
56-
granularity: '1 minute',
57-
limit: { limit: 10 },
58-
};
50+
],
51+
where: '',
52+
whereLanguage: 'lucene',
53+
timestampValueExpression: 'TimeUnix',
54+
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
55+
granularity: '1 minute',
56+
limit: { limit: 10 },
57+
};
5958

60-
const generatedSql = await renderChartConfig(config, mockMetadata);
59+
it('should generate sql for a single gauge metric', async () => {
60+
const generatedSql = await renderChartConfig(
61+
gaugeConfiguration,
62+
mockMetadata,
63+
);
64+
const actual = parameterizedQueryToSql(generatedSql);
65+
expect(actual).toMatchSnapshot();
66+
});
67+
68+
it('should generate sql for a single gauge metric with a delta() function applied', async () => {
69+
const generatedSql = await renderChartConfig(
70+
{
71+
...gaugeConfiguration,
72+
select: [
73+
{
74+
aggFn: 'max',
75+
valueExpression: 'Value',
76+
metricName: 'nodejs.event_loop.utilization',
77+
metricType: MetricsDataType.Gauge,
78+
isDelta: true,
79+
},
80+
],
81+
},
82+
mockMetadata,
83+
);
6184
const actual = parameterizedQueryToSql(generatedSql);
6285
expect(actual).toMatchSnapshot();
6386
});

0 commit comments

Comments
 (0)