Skip to content
Closed
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
98 changes: 58 additions & 40 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 @@ -214,6 +218,18 @@ function ChartSeriesEditorComponent({
defaultValue={AGG_FNS[0].value}
control={control}
/>
{tableSource?.kind === SourceKind.Metric &&
metricType === 'gauge' && (
<Flex justify="end">
<CheckBoxControlled
control={control}
name={`${namePrefix}isDelta`}
label="Delta"
size="xs"
className="mt-2"
/>
</Flex>
)}
</div>
{tableSource?.kind === SourceKind.Metric && (
<MetricNameSelect
Expand Down Expand Up @@ -243,44 +259,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 +321,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 @@ -287,6 +287,41 @@ exports[`renderChartConfig should generate sql for a single gauge metric 1`] = `
) 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"
`;

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,
last_value(Value) 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))
) - lag(max(
toFloat64OrDefault(toString(LastValue))
)) OVER ( ORDER BY toStartOfInterval(toDateTime(__hdx_time_bucket2), INTERVAL 1 minute) AS \`__hdx_time_bucket\`),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"
`;

exports[`renderChartConfig should generate sql for a single sum metric 1`] = `
"WITH Source AS (
SELECT
Expand Down
91 changes: 57 additions & 34 deletions packages/common-utils/src/__tests__/renderChartConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,65 @@ describe('renderChartConfig', () => {
} 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',
summary: 'otel_metrics_summary',
'exponential histogram': 'otel_metrics_exponential_histogram',
const gaugeConfiguration: 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',
summary: 'otel_metrics_summary',
'exponential histogram': 'otel_metrics_exponential_histogram',
},
from: {
databaseName: 'default',
tableName: '',
},
select: [
{
aggFn: 'quantile',
aggCondition: '',
aggConditionLanguage: 'lucene',
valueExpression: 'Value',
level: 0.95,
metricName: 'nodejs.event_loop.utilization',
metricType: MetricsDataType.Gauge,
},
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 },
};
],
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);
it('should generate sql for a single gauge metric', async () => {
const generatedSql = await renderChartConfig(
gaugeConfiguration,
mockMetadata,
);
const actual = parameterizedQueryToSql(generatedSql);
expect(actual).toMatchSnapshot();
});

it('should generate sql for a single gauge metric with a delta() function applied', async () => {
const generatedSql = await renderChartConfig(
{
...gaugeConfiguration,
select: [
{
aggFn: 'max',
valueExpression: 'Value',
metricName: 'nodejs.event_loop.utilization',
metricType: MetricsDataType.Gauge,
isDelta: true,
},
],
},
mockMetadata,
);
const actual = parameterizedQueryToSql(generatedSql);
expect(actual).toMatchSnapshot();
});
Expand Down
Loading
Loading