mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: add support for visualizing histogram counts (#1477)
Adds support for histogram `count` aggregations. This partially resolves https://github.com/hyperdxio/hyperdx/issues/1441, which should probably be split into a new ticket to only address `sum`. As part of this, I also moved the translation functionality for histograms to a new file `histogram.ts` to avoid contributing even more bloat to `renderChartConfig`. Happy to revert this and move that stuff back into the file if that's preferred. I also noticed by doing this that there was actually a SQL error in the snapshots for the tests--the existing quantile test was missing a trailing `,` after the time bucket if no group was provided https://github.com/hyperdxio/hyperdx/blob/main/packages/common-utils/src/__tests__/__snapshots__/renderChartConfig.test.ts.snap#L194 so centralizing like this is probably desirable to keep things consistent. I also personally use webstorm so I added that stuff to the gitignore.
This commit is contained in:
parent
12cd6433b7
commit
ca693c0f56
6 changed files with 542 additions and 228 deletions
5
.changeset/chilled-chairs-design.md
Normal file
5
.changeset/chilled-chairs-design.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/common-utils": minor
|
||||
---
|
||||
|
||||
Add support for visualizing histogram counts
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -73,4 +73,7 @@ docker-compose.prod.yml
|
|||
.volumes
|
||||
|
||||
# NX
|
||||
.nx/
|
||||
.nx/
|
||||
|
||||
# webstorm
|
||||
.idea
|
||||
|
|
@ -26,7 +26,100 @@ exports[`renderChartConfig containing CTE clauses should render a ChSql CTE conf
|
|||
|
||||
exports[`renderChartConfig containing CTE clauses should render a chart config CTE configuration correctly 1`] = `"WITH Parts AS (SELECT _part, _part_offset FROM default.some_table WHERE ((FieldA = 'test')) ORDER BY rand() DESC LIMIT 1000) SELECT * FROM Parts WHERE ((FieldA = 'test') AND (indexHint((_part, _part_offset) IN (SELECT tuple(_part, _part_offset) FROM Parts)))) ORDER BY rand() DESC LIMIT 1000"`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries should generate a query with grouping and time bucketing 1`] = `
|
||||
exports[`renderChartConfig histogram metric queries count should generate a count query with grouping and time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
AggregationTemporality,
|
||||
toStartOfInterval(toDateTime(TimeUnix), INTERVAL 2 minute) AS \`__hdx_time_bucket\`,
|
||||
[ResourceAttributes['host']] AS group,
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
toInt64(Count) AS current_count,
|
||||
lagInFrame(toNullable(current_count), 1, NULL) OVER (
|
||||
PARTITION BY group, attr_hash, bounds_hash, AggregationTemporality
|
||||
ORDER BY TimeUnix
|
||||
) AS prev_count,
|
||||
CASE
|
||||
WHEN AggregationTemporality = 1 THEN current_count
|
||||
WHEN AggregationTemporality = 2 THEN greatest(0, current_count - coalesce(prev_count, 0))
|
||||
ELSE 0
|
||||
END AS delta
|
||||
FROM default.otel_metrics_histogram
|
||||
WHERE (TimeUnix >= toStartOfInterval(fromUnixTimestamp64Milli(1739318400000), INTERVAL 2 minute) - INTERVAL 2 minute AND TimeUnix <= toStartOfInterval(fromUnixTimestamp64Milli(1765670400000), INTERVAL 2 minute) + INTERVAL 2 minute) AND ((MetricName = 'http.server.duration'))
|
||||
),metrics AS (
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
group,
|
||||
sum(delta) AS \\"Value\\"
|
||||
FROM source
|
||||
GROUP BY group, \`__hdx_time_bucket\`
|
||||
) SELECT \`__hdx_time_bucket\`, group, \\"Value\\" FROM metrics WHERE (\`__hdx_time_bucket\` >= fromUnixTimestamp64Milli(1739318400000) AND \`__hdx_time_bucket\` <= fromUnixTimestamp64Milli(1765670400000)) LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
|
||||
`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries count should generate a count query without grouping but time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
AggregationTemporality,
|
||||
toStartOfInterval(toDateTime(TimeUnix), INTERVAL 2 minute) AS \`__hdx_time_bucket\`,
|
||||
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
toInt64(Count) AS current_count,
|
||||
lagInFrame(toNullable(current_count), 1, NULL) OVER (
|
||||
PARTITION BY attr_hash, bounds_hash, AggregationTemporality
|
||||
ORDER BY TimeUnix
|
||||
) AS prev_count,
|
||||
CASE
|
||||
WHEN AggregationTemporality = 1 THEN current_count
|
||||
WHEN AggregationTemporality = 2 THEN greatest(0, current_count - coalesce(prev_count, 0))
|
||||
ELSE 0
|
||||
END AS delta
|
||||
FROM default.otel_metrics_histogram
|
||||
WHERE (TimeUnix >= toStartOfInterval(fromUnixTimestamp64Milli(1739318400000), INTERVAL 2 minute) - INTERVAL 2 minute AND TimeUnix <= toStartOfInterval(fromUnixTimestamp64Milli(1765670400000), INTERVAL 2 minute) + INTERVAL 2 minute) AND ((MetricName = 'http.server.duration'))
|
||||
),metrics AS (
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
|
||||
sum(delta) AS \\"Value\\"
|
||||
FROM source
|
||||
GROUP BY \`__hdx_time_bucket\`
|
||||
) SELECT \`__hdx_time_bucket\`, \\"Value\\" FROM metrics WHERE (\`__hdx_time_bucket\` >= fromUnixTimestamp64Milli(1739318400000) AND \`__hdx_time_bucket\` <= fromUnixTimestamp64Milli(1765670400000)) LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
|
||||
`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries count should generate a count query without grouping or time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
AggregationTemporality,
|
||||
TimeUnix AS \`__hdx_time_bucket\`,
|
||||
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
toInt64(Count) AS current_count,
|
||||
lagInFrame(toNullable(current_count), 1, NULL) OVER (
|
||||
PARTITION BY attr_hash, bounds_hash, AggregationTemporality
|
||||
ORDER BY TimeUnix
|
||||
) AS prev_count,
|
||||
CASE
|
||||
WHEN AggregationTemporality = 1 THEN current_count
|
||||
WHEN AggregationTemporality = 2 THEN greatest(0, current_count - coalesce(prev_count, 0))
|
||||
ELSE 0
|
||||
END AS delta
|
||||
FROM default.otel_metrics_histogram
|
||||
WHERE (TimeUnix >= fromUnixTimestamp64Milli(1739318400000) AND TimeUnix <= fromUnixTimestamp64Milli(1765670400000)) AND ((MetricName = 'http.server.duration'))
|
||||
),metrics AS (
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
|
||||
sum(delta) AS \\"Value\\"
|
||||
FROM source
|
||||
GROUP BY \`__hdx_time_bucket\`
|
||||
) SELECT \`__hdx_time_bucket\`, \\"Value\\" FROM metrics WHERE (\`__hdx_time_bucket\` >= fromUnixTimestamp64Milli(1739318400000) AND \`__hdx_time_bucket\` <= fromUnixTimestamp64Milli(1765670400000)) LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
|
||||
`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries quantile should generate a query with grouping and time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
MetricName,
|
||||
|
|
@ -110,7 +203,7 @@ exports[`renderChartConfig histogram metric queries should generate a query with
|
|||
) SELECT \`__hdx_time_bucket\`, group, \\"Value\\" FROM metrics WHERE (\`__hdx_time_bucket\` >= fromUnixTimestamp64Milli(1739318400000) AND \`__hdx_time_bucket\` <= fromUnixTimestamp64Milli(1765670400000)) LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
|
||||
`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries should generate a query without grouping but time bucketing 1`] = `
|
||||
exports[`renderChartConfig histogram metric queries quantile should generate a query without grouping but time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
MetricName,
|
||||
|
|
@ -194,12 +287,12 @@ exports[`renderChartConfig histogram metric queries should generate a query with
|
|||
) SELECT \`__hdx_time_bucket\`, \\"Value\\" FROM metrics WHERE (\`__hdx_time_bucket\` >= fromUnixTimestamp64Milli(1739318400000) AND \`__hdx_time_bucket\` <= fromUnixTimestamp64Milli(1765670400000)) LIMIT 10 SETTINGS short_circuit_function_evaluation = 'force_enable'"
|
||||
`;
|
||||
|
||||
exports[`renderChartConfig histogram metric queries should generate a query without grouping or time bucketing 1`] = `
|
||||
exports[`renderChartConfig histogram metric queries quantile should generate a query without grouping or time bucketing 1`] = `
|
||||
"WITH source AS (
|
||||
SELECT
|
||||
MetricName,
|
||||
ExplicitBounds,
|
||||
TimeUnix AS \`__hdx_time_bucket\`
|
||||
TimeUnix AS \`__hdx_time_bucket\`,
|
||||
|
||||
sumForEach(deltas) as rates
|
||||
FROM (
|
||||
|
|
|
|||
|
|
@ -167,115 +167,227 @@ describe('renderChartConfig', () => {
|
|||
});
|
||||
|
||||
describe('histogram metric queries', () => {
|
||||
it('should generate a query without grouping or time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
describe('quantile', () => {
|
||||
it('should generate a query without grouping or time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
metricTables: {
|
||||
gauge: 'otel_metrics_gauge',
|
||||
histogram: 'otel_metrics_histogram',
|
||||
sum: 'otel_metrics_sum',
|
||||
summary: 'otel_metrics_summary',
|
||||
'exponential histogram': 'otel_metrics_exponential_histogram',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: '',
|
||||
},
|
||||
select: [
|
||||
{
|
||||
aggFn: 'quantile',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a query without grouping but time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a query with grouping and time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
groupBy: `ResourceAttributes['host']`,
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate a query without grouping but time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
describe('count', () => {
|
||||
it('should generate a count query without grouping or time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
metricTables: {
|
||||
gauge: 'otel_metrics_gauge',
|
||||
histogram: 'otel_metrics_histogram',
|
||||
sum: 'otel_metrics_sum',
|
||||
summary: 'otel_metrics_summary',
|
||||
'exponential histogram': 'otel_metrics_exponential_histogram',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a query with grouping and time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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',
|
||||
level: 0.5,
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: '',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
groupBy: `ResourceAttributes['host']`,
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
select: [
|
||||
{
|
||||
aggFn: 'count',
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a count query without grouping but time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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: 'count',
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a count query with grouping and time bucketing', async () => {
|
||||
const config: ChartConfigWithOptDateRange = {
|
||||
displayType: DisplayType.Line,
|
||||
connection: 'test-connection',
|
||||
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: 'count',
|
||||
valueExpression: 'Value',
|
||||
metricName: 'http.server.duration',
|
||||
metricType: MetricsDataType.Histogram,
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
timestampValueExpression: 'TimeUnix',
|
||||
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
||||
granularity: '2 minute',
|
||||
groupBy: `ResourceAttributes['host']`,
|
||||
limit: { limit: 10 },
|
||||
};
|
||||
|
||||
const generatedSql = await renderChartConfig(config, mockMetadata);
|
||||
const actual = parameterizedQueryToSql(generatedSql);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
189
packages/common-utils/src/core/histogram.ts
Normal file
189
packages/common-utils/src/core/histogram.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { ChSql, chSql } from '@/clickhouse';
|
||||
import { ChartConfig } from '@/types';
|
||||
|
||||
type WithClauses = ChartConfig['with'];
|
||||
type TemplatedInput = ChSql | string;
|
||||
|
||||
export const translateHistogram = ({
|
||||
select,
|
||||
...rest
|
||||
}: {
|
||||
select: Exclude<ChartConfig['select'], string>[number];
|
||||
timeBucketSelect: TemplatedInput;
|
||||
groupBy?: TemplatedInput;
|
||||
from: TemplatedInput;
|
||||
where: TemplatedInput;
|
||||
valueAlias: TemplatedInput;
|
||||
}) => {
|
||||
if (select.aggFn === 'quantile') {
|
||||
if (!('level' in select) || select.level === null)
|
||||
throw new Error('quantile must have a level');
|
||||
return translateHistogramQuantile({
|
||||
...rest,
|
||||
level: select.level,
|
||||
});
|
||||
}
|
||||
if (select.aggFn === 'count') {
|
||||
return translateHistogramCount(rest);
|
||||
}
|
||||
throw new Error(`${select.aggFn} is not supported for histograms currently`);
|
||||
};
|
||||
|
||||
const translateHistogramCount = ({
|
||||
timeBucketSelect,
|
||||
groupBy,
|
||||
from,
|
||||
where,
|
||||
valueAlias,
|
||||
}: {
|
||||
timeBucketSelect: TemplatedInput;
|
||||
groupBy?: TemplatedInput;
|
||||
from: TemplatedInput;
|
||||
where: TemplatedInput;
|
||||
valueAlias: TemplatedInput;
|
||||
}): WithClauses => [
|
||||
{
|
||||
name: 'source',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
TimeUnix,
|
||||
AggregationTemporality,
|
||||
${timeBucketSelect},
|
||||
${groupBy ? chSql`[${groupBy}] AS group,` : ''}
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
toInt64(Count) AS current_count,
|
||||
lagInFrame(toNullable(current_count), 1, NULL) OVER (
|
||||
PARTITION BY ${groupBy ? `group, ` : ''} attr_hash, bounds_hash, AggregationTemporality
|
||||
ORDER BY TimeUnix
|
||||
) AS prev_count,
|
||||
CASE
|
||||
WHEN AggregationTemporality = 1 THEN current_count
|
||||
WHEN AggregationTemporality = 2 THEN greatest(0, current_count - coalesce(prev_count, 0))
|
||||
ELSE 0
|
||||
END AS delta
|
||||
FROM ${from}
|
||||
WHERE ${where}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'metrics',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
${groupBy ? 'group,' : ''}
|
||||
sum(delta) AS "${valueAlias}"
|
||||
FROM source
|
||||
GROUP BY ${groupBy ? 'group, ' : ''}\`__hdx_time_bucket\`
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const translateHistogramQuantile = ({
|
||||
timeBucketSelect,
|
||||
groupBy,
|
||||
from,
|
||||
where,
|
||||
valueAlias,
|
||||
level,
|
||||
}: {
|
||||
timeBucketSelect: TemplatedInput;
|
||||
groupBy?: TemplatedInput;
|
||||
from: TemplatedInput;
|
||||
where: TemplatedInput;
|
||||
valueAlias: TemplatedInput;
|
||||
level: number;
|
||||
}): WithClauses => [
|
||||
{
|
||||
name: 'source',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
MetricName,
|
||||
ExplicitBounds,
|
||||
${timeBucketSelect},
|
||||
${groupBy ? chSql`[${groupBy}] as group,` : ''}
|
||||
sumForEach(deltas) as rates
|
||||
FROM (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
MetricName,
|
||||
ResourceAttributes,
|
||||
Attributes,
|
||||
ExplicitBounds,
|
||||
attr_hash,
|
||||
any(attr_hash) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_attr_hash,
|
||||
any(bounds_hash) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_bounds_hash,
|
||||
any(counts) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_counts,
|
||||
counts,
|
||||
IF(
|
||||
AggregationTemporality = 1 ${'' /* denotes a metric that is not monotonic e.g. already a delta */}
|
||||
OR prev_attr_hash != attr_hash ${'' /* the attributes have changed so this is a different metric */}
|
||||
OR bounds_hash != prev_bounds_hash ${'' /* the bucketing has changed so should be treated as different metric */}
|
||||
OR arrayExists((x) -> x.2 < x.1, arrayZip(prev_counts, counts)), ${'' /* a data point has gone down, probably a reset event */}
|
||||
counts,
|
||||
counts - prev_counts
|
||||
) AS deltas
|
||||
FROM (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
MetricName,
|
||||
AggregationTemporality,
|
||||
ExplicitBounds,
|
||||
ResourceAttributes,
|
||||
Attributes,
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
CAST(BucketCounts AS Array(Int64)) counts
|
||||
FROM ${from}
|
||||
WHERE ${where}
|
||||
ORDER BY attr_hash, TimeUnix ASC
|
||||
)
|
||||
)
|
||||
GROUP BY \`__hdx_time_bucket\`, MetricName, ${groupBy ? 'group, ' : ''}ExplicitBounds
|
||||
ORDER BY \`__hdx_time_bucket\`
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'points',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
MetricName,
|
||||
${groupBy ? 'group,' : ''}
|
||||
arrayZipUnaligned(arrayCumSum(rates), ExplicitBounds) as point,
|
||||
length(point) as n
|
||||
FROM source
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'metrics',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
MetricName,
|
||||
${groupBy ? 'group,' : ''}
|
||||
point[n].1 AS total,
|
||||
${{ Float64: level }} * total AS rank,
|
||||
arrayFirstIndex(x -> if(x.1 > rank, 1, 0), point) AS upper_idx,
|
||||
point[upper_idx].1 AS upper_count,
|
||||
ifNull(point[upper_idx].2, inf) AS upper_bound,
|
||||
CASE
|
||||
WHEN upper_idx > 1 THEN point[upper_idx - 1].2
|
||||
WHEN point[upper_idx].2 > 0 THEN 0
|
||||
ELSE inf
|
||||
END AS lower_bound,
|
||||
if (
|
||||
lower_bound = 0,
|
||||
0,
|
||||
point[upper_idx - 1].1
|
||||
) AS lower_count,
|
||||
CASE
|
||||
WHEN upper_bound = inf THEN point[upper_idx - 1].2
|
||||
WHEN lower_bound = inf THEN point[1].2
|
||||
ELSE lower_bound + (upper_bound - lower_bound) * ((rank - lower_count) / (upper_count - lower_count))
|
||||
END AS "${valueAlias}"
|
||||
FROM points
|
||||
WHERE length(point) > 1 AND total > 0
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
|
@ -3,23 +3,8 @@ import * as SQLParser from 'node-sql-parser';
|
|||
import SqlString from 'sqlstring';
|
||||
|
||||
import { ChSql, chSql, concatChSql, wrapChSqlIfNotEmpty } from '@/clickhouse';
|
||||
import { translateHistogram } from '@/core/histogram';
|
||||
import { Metadata } from '@/core/metadata';
|
||||
import { CustomSchemaSQLSerializerV2, SearchQueryBuilder } from '@/queryParser';
|
||||
|
||||
/**
|
||||
* Helper function to create a MetricName filter condition.
|
||||
* Uses metricNameSql if available (which handles both old and new metric names via OR),
|
||||
* otherwise falls back to a simple equality check.
|
||||
*/
|
||||
function createMetricNameFilter(
|
||||
metricName: string,
|
||||
metricNameSql?: string,
|
||||
): string {
|
||||
if (metricNameSql) {
|
||||
return metricNameSql;
|
||||
}
|
||||
return SqlString.format('MetricName = ?', [metricName]);
|
||||
}
|
||||
import {
|
||||
convertDateRangeToGranularityString,
|
||||
convertGranularityToSeconds,
|
||||
|
|
@ -28,6 +13,7 @@ import {
|
|||
parseToStartOfFunction,
|
||||
splitAndTrimWithBracket,
|
||||
} from '@/core/utils';
|
||||
import { CustomSchemaSQLSerializerV2, SearchQueryBuilder } from '@/queryParser';
|
||||
import {
|
||||
AggregateFunction,
|
||||
AggregateFunctionWithCombinators,
|
||||
|
|
@ -47,6 +33,21 @@ import {
|
|||
SQLInterval,
|
||||
} from '@/types';
|
||||
|
||||
/**
|
||||
* Helper function to create a MetricName filter condition.
|
||||
* Uses metricNameSql if available (which handles both old and new metric names via OR),
|
||||
* otherwise falls back to a simple equality check.
|
||||
*/
|
||||
function createMetricNameFilter(
|
||||
metricName: string,
|
||||
metricNameSql?: string,
|
||||
): string {
|
||||
if (metricNameSql) {
|
||||
return metricNameSql;
|
||||
}
|
||||
return SqlString.format('MetricName = ?', [metricName]);
|
||||
}
|
||||
|
||||
/** The default maximum number of buckets setting when determining a bucket duration for 'auto' granularity */
|
||||
export const DEFAULT_AUTO_GRANULARITY_MAX_BUCKETS = 60;
|
||||
|
||||
|
|
@ -1261,17 +1262,7 @@ async function translateMetricChartConfig(
|
|||
timestampValueExpression: `\`${timeBucketCol}\``,
|
||||
};
|
||||
} else if (metricType === MetricsDataType.Histogram && metricName) {
|
||||
// histograms are only valid for quantile selections
|
||||
const { aggFn, level, alias, ..._selectRest } = _select as {
|
||||
aggFn: string;
|
||||
level?: number;
|
||||
alias?: string;
|
||||
};
|
||||
|
||||
if (aggFn !== 'quantile' || level == null) {
|
||||
throw new Error('quantile must be specified for histogram metrics');
|
||||
}
|
||||
|
||||
const { alias } = _select;
|
||||
// Use the alias from the select, defaulting to 'Value' for backwards compatibility
|
||||
const valueAlias = alias || 'Value';
|
||||
|
||||
|
|
@ -1322,100 +1313,21 @@ async function translateMetricChartConfig(
|
|||
|
||||
return {
|
||||
...restChartConfig,
|
||||
with: [
|
||||
{
|
||||
name: 'source',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
MetricName,
|
||||
ExplicitBounds,
|
||||
${timeBucketSelect.sql ? chSql`${timeBucketSelect},` : 'TimeUnix AS `__hdx_time_bucket`'}
|
||||
${groupBy ? chSql`[${groupBy}] as group,` : ''}
|
||||
sumForEach(deltas) as rates
|
||||
FROM (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
MetricName,
|
||||
ResourceAttributes,
|
||||
Attributes,
|
||||
ExplicitBounds,
|
||||
attr_hash,
|
||||
any(attr_hash) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_attr_hash,
|
||||
any(bounds_hash) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_bounds_hash,
|
||||
any(counts) OVER (ROWS BETWEEN 1 preceding AND 1 preceding) AS prev_counts,
|
||||
counts,
|
||||
IF(
|
||||
AggregationTemporality = 1 ${'' /* denotes a metric that is not monotonic e.g. already a delta */}
|
||||
OR prev_attr_hash != attr_hash ${'' /* the attributes have changed so this is a different metric */}
|
||||
OR bounds_hash != prev_bounds_hash ${'' /* the bucketing has changed so should be treated as different metric */}
|
||||
OR arrayExists((x) -> x.2 < x.1, arrayZip(prev_counts, counts)), ${'' /* a data point has gone down, probably a reset event */}
|
||||
counts,
|
||||
counts - prev_counts
|
||||
) AS deltas
|
||||
FROM (
|
||||
SELECT
|
||||
TimeUnix,
|
||||
MetricName,
|
||||
AggregationTemporality,
|
||||
ExplicitBounds,
|
||||
ResourceAttributes,
|
||||
Attributes,
|
||||
cityHash64(mapConcat(ScopeAttributes, ResourceAttributes, Attributes)) AS attr_hash,
|
||||
cityHash64(ExplicitBounds) AS bounds_hash,
|
||||
CAST(BucketCounts AS Array(Int64)) counts
|
||||
FROM ${renderFrom({ from: { ...from, tableName: metricTables[MetricsDataType.Histogram] } })}
|
||||
WHERE ${where}
|
||||
ORDER BY attr_hash, TimeUnix ASC
|
||||
)
|
||||
)
|
||||
GROUP BY \`__hdx_time_bucket\`, MetricName, ${groupBy ? 'group, ' : ''}ExplicitBounds
|
||||
ORDER BY \`__hdx_time_bucket\`
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'points',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
MetricName,
|
||||
${groupBy ? 'group,' : ''}
|
||||
arrayZipUnaligned(arrayCumSum(rates), ExplicitBounds) as point,
|
||||
length(point) as n
|
||||
FROM source
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'metrics',
|
||||
sql: chSql`
|
||||
SELECT
|
||||
\`__hdx_time_bucket\`,
|
||||
MetricName,
|
||||
${groupBy ? 'group,' : ''}
|
||||
point[n].1 AS total,
|
||||
${{ Float64: level }} * total AS rank,
|
||||
arrayFirstIndex(x -> if(x.1 > rank, 1, 0), point) AS upper_idx,
|
||||
point[upper_idx].1 AS upper_count,
|
||||
ifNull(point[upper_idx].2, inf) AS upper_bound,
|
||||
CASE
|
||||
WHEN upper_idx > 1 THEN point[upper_idx - 1].2
|
||||
WHEN point[upper_idx].2 > 0 THEN 0
|
||||
ELSE inf
|
||||
END AS lower_bound,
|
||||
if (
|
||||
lower_bound = 0,
|
||||
0,
|
||||
point[upper_idx - 1].1
|
||||
) AS lower_count,
|
||||
CASE
|
||||
WHEN upper_bound = inf THEN point[upper_idx - 1].2
|
||||
WHEN lower_bound = inf THEN point[1].2
|
||||
ELSE lower_bound + (upper_bound - lower_bound) * ((rank - lower_count) / (upper_count - lower_count))
|
||||
END AS "${valueAlias}"
|
||||
FROM points
|
||||
WHERE length(point) > 1 AND total > 0
|
||||
`,
|
||||
},
|
||||
],
|
||||
with: translateHistogram({
|
||||
select: _select,
|
||||
timeBucketSelect: timeBucketSelect.sql
|
||||
? chSql`${timeBucketSelect}`
|
||||
: 'TimeUnix AS `__hdx_time_bucket`',
|
||||
groupBy,
|
||||
from: renderFrom({
|
||||
from: {
|
||||
...from,
|
||||
tableName: metricTables[MetricsDataType.Histogram],
|
||||
},
|
||||
}),
|
||||
where,
|
||||
valueAlias,
|
||||
}),
|
||||
select: `\`__hdx_time_bucket\`${groupBy ? ', group' : ''}, "${valueAlias}"`,
|
||||
from: {
|
||||
databaseName: '',
|
||||
|
|
|
|||
Loading…
Reference in a new issue