Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/honest-pens-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/common-utils": minor
"@hyperdx/app": minor
---

Add delta() function for gauge metrics
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,44 @@ Array [
]
`;

exports[`renderChartConfig Query Metrics - Gauge single max gauge with delta 1`] = `
Array [
Object {
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
"max(toFloat64OrDefault(toString(LastValue)))": 5,
},
Object {
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
"max(toFloat64OrDefault(toString(LastValue)))": -1.6666666666666667,
},
]
`;

exports[`renderChartConfig Query Metrics - Gauge single max gauge with delta and group by 1`] = `
Array [
Object {
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
"arrayElement(ResourceAttributes, 'host')": "host2",
"max(toFloat64OrDefault(toString(LastValue)))": 5,
},
Object {
"__hdx_time_bucket": "2022-01-05T00:00:00Z",
"arrayElement(ResourceAttributes, 'host')": "host1",
"max(toFloat64OrDefault(toString(LastValue)))": -72.91666666666667,
},
Object {
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
"arrayElement(ResourceAttributes, 'host')": "host2",
"max(toFloat64OrDefault(toString(LastValue)))": -1.6666666666666667,
},
Object {
"__hdx_time_bucket": "2022-01-05T00:05:00Z",
"arrayElement(ResourceAttributes, 'host')": "host1",
"max(toFloat64OrDefault(toString(LastValue)))": -33.333333333333336,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually validated that these numbers are as expected for the test data.

Calculation is (last value - first value) / (time of last value - time of first value) * 5 minutes

},
]
`;

exports[`renderChartConfig Query Metrics - Gauge single max/avg/sum gauge 1`] = `
Array [
Object {
Expand Down
51 changes: 51 additions & 0 deletions packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,57 @@ describe('renderChartConfig', () => {
);
expect(await queryData(query)).toMatchSnapshot();
});

it('single max gauge with delta', async () => {
const query = await renderChartConfig(
{
select: [
{
aggFn: 'max',
metricName: 'test.cpu',
metricType: MetricsDataType.Gauge,
valueExpression: 'Value',
isDelta: true,
},
],
from: metricSource.from,
where: '',
metricTables: TEST_METRIC_TABLES,
dateRange: [new Date(now), new Date(now + ms('10m'))],
granularity: '5 minute',
timestampValueExpression: metricSource.timestampValueExpression,
connection: connection.id,
},
metadata,
);
expect(await queryData(query)).toMatchSnapshot();
});

it('single max gauge with delta and group by', async () => {
const query = await renderChartConfig(
{
select: [
{
aggFn: 'max',
metricName: 'test.cpu',
metricType: MetricsDataType.Gauge,
valueExpression: 'Value',
isDelta: true,
},
],
from: metricSource.from,
where: '',
metricTables: TEST_METRIC_TABLES,
dateRange: [new Date(now), new Date(now + ms('10m'))],
granularity: '5 minute',
groupBy: `ResourceAttributes['host']`,
timestampValueExpression: metricSource.timestampValueExpression,
connection: connection.id,
},
metadata,
);
expect(await queryData(query)).toMatchSnapshot();
});
});

describe('Query Metrics - Sum', () => {
Expand Down
123 changes: 72 additions & 51 deletions packages/app/src/components/DBEditTimeChartForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ import HDXMarkdownChart from '../HDXMarkdownChart';

import { AggFnSelectControlled } from './AggFnSelect';
import DBNumberChart from './DBNumberChart';
import { InputControlled, TextInputControlled } from './InputControlled';
import {
CheckBoxControlled,
InputControlled,
TextInputControlled,
} from './InputControlled';
import { MetricNameSelect } from './MetricNameSelect';
import { NumberFormatInput } from './NumberFormat';
import { SourceSelectControlled } from './SourceSelect';
Expand Down Expand Up @@ -202,7 +206,7 @@ function ChartSeriesEditorComponent({
mb={8}
mt="sm"
/>
<Flex gap="sm" mt="xs" align="center">
<Flex gap="sm" mt="xs" align="start">
<div
style={{
minWidth: 200,
Expand All @@ -216,17 +220,32 @@ function ChartSeriesEditorComponent({
/>
</div>
{tableSource?.kind === SourceKind.Metric && (
<MetricNameSelect
metricName={metricName}
dateRange={dateRange}
metricType={metricType}
setMetricName={value => {
setValue(`${namePrefix}metricName`, value);
setValue(`${namePrefix}valueExpression`, 'Value');
}}
setMetricType={value => setValue(`${namePrefix}metricType`, value)}
metricSource={tableSource}
/>
<div style={{ minWidth: 220 }}>
<MetricNameSelect
metricName={metricName}
dateRange={dateRange}
metricType={metricType}
setMetricName={value => {
setValue(`${namePrefix}metricName`, value);
setValue(`${namePrefix}valueExpression`, 'Value');
}}
setMetricType={value =>
setValue(`${namePrefix}metricType`, value)
}
metricSource={tableSource}
/>
{metricType === 'gauge' && (
<Flex justify="end">
<CheckBoxControlled
control={control}
name={`${namePrefix}isDelta`}
label="Delta"
size="xs"
className="mt-2"
/>
Comment on lines +239 to +245
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thought is that eventually this could be a drop-down with various other functions that could be applied, if we implement other functions. I kept it simple for now though.

</Flex>
)}
</div>
)}
{tableSource?.kind !== SourceKind.Metric && aggFn !== 'count' && (
<div style={{ minWidth: 220 }}>
Expand All @@ -243,44 +262,46 @@ function ChartSeriesEditorComponent({
/>
</div>
)}
<Text size="sm">Where</Text>
{aggConditionLanguage === 'sql' ? (
<SQLInlineEditorControlled
tableConnections={{
databaseName,
tableName: tableName ?? '',
connectionId: connectionId ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
placeholder="SQL WHERE clause (ex. column = 'foo')"
onLanguageChange={lang =>
setValue(`${namePrefix}aggConditionLanguage`, lang)
}
additionalSuggestions={attributeKeys}
language="sql"
onSubmit={onSubmit}
/>
) : (
<SearchInputV2
tableConnections={{
connectionId: connectionId ?? '',
databaseName: databaseName ?? '',
tableName: tableName ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
onLanguageChange={lang =>
setValue(`${namePrefix}aggConditionLanguage`, lang)
}
language="lucene"
placeholder="Search your events w/ Lucene ex. column:foo"
onSubmit={onSubmit}
additionalSuggestions={attributeKeys}
/>
)}
<Flex align={'center'} gap={'xs'} className="flex-grow-1">
<Text size="sm">Where</Text>
{aggConditionLanguage === 'sql' ? (
<SQLInlineEditorControlled
tableConnections={{
databaseName,
tableName: tableName ?? '',
connectionId: connectionId ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
placeholder="SQL WHERE clause (ex. column = 'foo')"
onLanguageChange={lang =>
setValue(`${namePrefix}aggConditionLanguage`, lang)
}
additionalSuggestions={attributeKeys}
language="sql"
onSubmit={onSubmit}
/>
) : (
<SearchInputV2
tableConnections={{
connectionId: connectionId ?? '',
databaseName: databaseName ?? '',
tableName: tableName ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
onLanguageChange={lang =>
setValue(`${namePrefix}aggConditionLanguage`, lang)
}
language="lucene"
placeholder="Search your events w/ Lucene ex. column:foo"
onSubmit={onSubmit}
additionalSuggestions={attributeKeys}
/>
)}
</Flex>
{showGroupBy && (
<>
<Flex align={'center'} gap={'xs'}>
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
Group By
</Text>
Expand All @@ -303,7 +324,7 @@ function ChartSeriesEditorComponent({
onSubmit={onSubmit}
/>
</div>
</>
</Flex>
)}
</Flex>
</>
Expand Down
36 changes: 36 additions & 0 deletions packages/app/src/components/InputControlled.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { Control, Controller, FieldValues, Path } from 'react-hook-form';
import {
Checkbox,
CheckboxProps,
Input,
InputProps,
PasswordInput,
Expand Down Expand Up @@ -33,6 +35,17 @@ interface TextInputControlledProps<T extends FieldValues>
rules?: Parameters<Control<T>['register']>[1];
}

interface CheckboxControlledProps<T extends FieldValues>
extends Omit<CheckboxProps, 'name' | 'style'>,
Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'name' | 'size' | 'color'
> {
name: Path<T>;
control: Control<T>;
rules?: Parameters<Control<T>['register']>[1];
}

export function TextInputControlled<T extends FieldValues>({
name,
control,
Expand Down Expand Up @@ -86,3 +99,26 @@ export function PasswordInputControlled<T extends FieldValues>({
/>
);
}

export function CheckBoxControlled<T extends FieldValues>({
name,
control,
rules,
...props
}: CheckboxControlledProps<T>) {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field: { value, ...field }, fieldState: { error } }) => (
<Checkbox
{...props}
{...field}
checked={value}
error={error?.message}
/>
)}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,40 @@ exports[`renderChartConfig should generate sql for a single gauge metric 1`] = `
FROM Source
GROUP BY AttributesHash, __hdx_time_bucket2
ORDER BY AttributesHash, __hdx_time_bucket2
) 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"
) 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'"
`;

exports[`renderChartConfig should generate sql for a single gauge metric with a delta() function applied 1`] = `
"WITH Source AS (
SELECT
*,
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS AttributesHash
FROM default.otel_metrics_gauge
WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000)) AND ((MetricName = 'nodejs.event_loop.utilization'))
),Bucketed AS (
SELECT
toStartOfInterval(toDateTime(TimeUnix), INTERVAL 1 minute) AS \`__hdx_time_bucket2\`,
AttributesHash,
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,
any(ScopeAttributes) AS ScopeAttributes,
any(ResourceAttributes) AS ResourceAttributes,
any(Attributes) AS Attributes,
any(ResourceSchemaUrl) AS ResourceSchemaUrl,
any(ScopeName) AS ScopeName,
any(ScopeVersion) AS ScopeVersion,
any(ScopeDroppedAttrCount) AS ScopeDroppedAttrCount,
any(ScopeSchemaUrl) AS ScopeSchemaUrl,
any(ServiceName) AS ServiceName,
any(MetricDescription) AS MetricDescription,
any(MetricUnit) AS MetricUnit,
any(StartTimeUnix) AS StartTimeUnix,
any(Flags) AS Flags
FROM Source
GROUP BY AttributesHash, __hdx_time_bucket2
ORDER BY AttributesHash, __hdx_time_bucket2
) SELECT max(
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'"
`;

exports[`renderChartConfig should generate sql for a single sum metric 1`] = `
Expand Down
Loading