2026-01-21 16:07:30 +00:00
import React , {
Fragment ,
useCallback ,
useEffect ,
useRef ,
useState ,
} from 'react' ;
2024-11-12 12:53:15 +00:00
import {
Control ,
Controller ,
2025-11-18 19:34:07 +00:00
useFieldArray ,
2024-11-12 12:53:15 +00:00
useForm ,
UseFormSetValue ,
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
useWatch ,
2024-11-12 12:53:15 +00:00
} from 'react-hook-form' ;
2025-06-25 16:41:42 +00:00
import { z } from 'zod' ;
2026-01-15 09:21:27 +00:00
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse' ;
2025-02-12 19:04:50 +00:00
import {
MetricsDataType ,
SourceKind ,
2025-06-25 16:41:42 +00:00
sourceSchemaWithout ,
2025-02-12 19:04:50 +00:00
TSource ,
2025-06-25 16:41:42 +00:00
TSourceUnion ,
2025-02-12 19:04:50 +00:00
} from '@hyperdx/common-utils/dist/types' ;
2024-11-12 12:53:15 +00:00
import {
2025-11-18 19:34:07 +00:00
ActionIcon ,
2024-11-12 12:53:15 +00:00
Anchor ,
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
Badge ,
2024-11-12 12:53:15 +00:00
Box ,
Button ,
2025-12-15 17:06:40 +00:00
Center ,
2024-11-12 12:53:15 +00:00
Divider ,
Flex ,
2025-11-18 19:34:07 +00:00
Grid ,
2024-11-12 12:53:15 +00:00
Group ,
Radio ,
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
Select ,
2024-11-12 12:53:15 +00:00
Slider ,
Stack ,
Text ,
Tooltip ,
} from '@mantine/core' ;
2026-01-05 17:59:39 +00:00
import { DateInput } from '@mantine/dates' ;
2024-11-12 12:53:15 +00:00
import { notifications } from '@mantine/notifications' ;
2025-12-15 17:06:40 +00:00
import {
2026-01-15 09:21:27 +00:00
IconCheck ,
2025-12-15 17:06:40 +00:00
IconCirclePlus ,
IconHelpCircle ,
IconSettings ,
IconTrash ,
} from '@tabler/icons-react' ;
2024-11-12 12:53:15 +00:00
import { SourceSelectControlled } from '@/components/SourceSelect' ;
2025-02-11 19:59:12 +00:00
import { IS_METRICS_ENABLED , IS_SESSIONS_ENABLED } from '@/config' ;
2024-11-12 12:53:15 +00:00
import { useConnections } from '@/connection' ;
2026-01-15 09:21:27 +00:00
import { useExplainQuery } from '@/hooks/useExplainQuery' ;
2026-01-13 12:55:12 +00:00
import { useMetadataWithSettings } from '@/hooks/useMetadata' ;
2024-11-12 12:53:15 +00:00
import {
inferTableSourceConfig ,
2025-02-12 19:04:50 +00:00
isValidMetricTable ,
2025-09-03 22:24:05 +00:00
isValidSessionsTable ,
2024-11-12 12:53:15 +00:00
useCreateSource ,
useDeleteSource ,
useSource ,
2025-07-24 15:41:56 +00:00
useSources ,
2024-11-12 12:53:15 +00:00
useUpdateSource ,
} from '@/source' ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
import {
inferMaterializedViewConfig ,
MV_AGGREGATE_FUNCTIONS ,
feat: Add MV granularities and infer config from SummingMergeTree (#1550)
Closes HDX-3064
# Summary
This PR makes two improvements to MV configuration
1. Additional granularities are now available
2. Configuration can now be inferred from SummingMergeTree MVs (in addition to AggregatingMergeTree, which were already supported)
3. Source column inference has been improved to support cases where the target table column name does not follow the `aggFn__sourceColumn` convention.
Also, tests have been updated and additional tests have been backported from the EE repo.
## Demo
https://github.com/user-attachments/assets/7ba6ef0c-2187-4e07-bfda-7b4b690b7a73
## Testing
To test the Summing Merge Tree inference, create a materialized view like so:
```sql
CREATE TABLE default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
ENGINE = SummingMergeTree((count, sumDuration))
ORDER BY (ServiceName, StatusCode, Timestamp)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW default.traces_summed_mv_2 TO default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
AS SELECT
toStartOfInterval(Timestamp, toIntervalMinute(30)) AS Timestamp,
ServiceName,
StatusCode,
count() AS count,
sum(Duration) AS sumDuration,
quantileState(0.5)(Duration) AS quantileDuration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
StatusCode;
```
2026-01-06 19:45:52 +00:00
MV_GRANULARITY_OPTIONS ,
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
} from '@/utils/materializedViews' ;
2024-11-12 12:53:15 +00:00
2026-01-07 14:02:36 +00:00
import ConfirmDeleteMenu from '../ConfirmDeleteMenu' ;
import { ConnectionSelectControlled } from '../ConnectionSelect' ;
import { DatabaseSelectControlled } from '../DatabaseSelect' ;
import { DBTableSelectControlled } from '../DBTableSelect' ;
2026-01-15 09:21:27 +00:00
import { ErrorCollapse } from '../Error/ErrorCollapse' ;
2026-01-07 14:02:36 +00:00
import { InputControlled } from '../InputControlled' ;
import SelectControlled from '../SelectControlled' ;
import { SQLInlineEditorControlled } from '../SQLInlineEditor' ;
2024-11-12 12:53:15 +00:00
2025-02-09 01:25:23 +00:00
const DEFAULT_DATABASE = 'default' ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
const MV_AGGREGATE_FUNCTION_OPTIONS = MV_AGGREGATE_FUNCTIONS . map ( fn = > ( {
value : fn ,
label : fn ,
} ) ) ;
2025-02-26 00:00:48 +00:00
// TODO: maybe otel clickhouse export migrate the schema?
const OTEL_CLICKHOUSE_EXPRESSIONS = {
timestampValueExpression : 'TimeUnix' ,
resourceAttributesExpression : 'ResourceAttributes' ,
} ;
2025-07-24 15:41:56 +00:00
const CORRELATION_FIELD_MAP : Record <
SourceKind ,
Record < string , { targetKind : SourceKind ; targetField : keyof TSource } [ ] >
> = {
[ SourceKind . Log ] : {
metricSourceId : [
{ targetKind : SourceKind.Metric , targetField : 'logSourceId' } ,
] ,
traceSourceId : [
{ targetKind : SourceKind.Trace , targetField : 'logSourceId' } ,
] ,
} ,
[ SourceKind . Trace ] : {
logSourceId : [ { targetKind : SourceKind.Log , targetField : 'traceSourceId' } ] ,
sessionSourceId : [
{ targetKind : SourceKind.Session , targetField : 'traceSourceId' } ,
] ,
metricSourceId : [
{ targetKind : SourceKind.Metric , targetField : 'logSourceId' } ,
] ,
} ,
[ SourceKind . Session ] : {
traceSourceId : [
{ targetKind : SourceKind.Trace , targetField : 'sessionSourceId' } ,
] ,
} ,
[ SourceKind . Metric ] : {
logSourceId : [
{ targetKind : SourceKind.Log , targetField : 'metricSourceId' } ,
] ,
} ,
} ;
2024-11-12 12:53:15 +00:00
function FormRow ( {
label ,
children ,
helpText ,
} : {
label : React.ReactNode ;
children : React.ReactNode ;
helpText? : string ;
} ) {
return (
// <Group grow preventGrowOverflow={false}>
2025-11-18 19:34:07 +00:00
< Flex align = "flex-start" >
< Flex align = "center" >
< Stack
justify = "center"
style = { {
maxWidth : 220 ,
minWidth : 220 ,
height : '36px' ,
} }
>
{ typeof label === 'string' ? (
< Text tt = "capitalize" size = "sm" >
{ label }
< / Text >
) : (
label
) }
< / Stack >
2025-12-15 17:06:40 +00:00
< Center
2025-11-18 19:34:07 +00:00
me = "sm"
2025-12-15 17:06:40 +00:00
ms = "sm"
2025-11-18 19:34:07 +00:00
style = { {
. . . ( ! helpText ? { opacity : 0 , pointerEvents : 'none' } : { } ) ,
} }
>
< Tooltip label = { helpText } color = "dark" c = "white" multiline maw = { 600 } >
2025-12-15 17:06:40 +00:00
< IconHelpCircle size = { 20 } className = "cursor-pointer" / >
2025-11-18 19:34:07 +00:00
< / Tooltip >
2025-12-15 17:06:40 +00:00
< / Center >
2025-11-18 19:34:07 +00:00
< / Flex >
2024-11-12 12:53:15 +00:00
< Box
w = "100%"
style = { {
minWidth : 0 ,
} }
>
{ children }
< / Box >
< / Flex >
) ;
}
2026-01-15 09:21:27 +00:00
type HighlightedAttributeRowProps = Omit < TableModelProps , ' setValue ' > & {
id : string ;
index : number ;
databaseName : string ;
name :
| 'highlightedTraceAttributeExpressions'
| 'highlightedRowAttributeExpressions' ;
tableName : string ;
connectionId : string ;
removeHighlightedAttribute : ( index : number ) = > void ;
} ;
function HighlightedAttributeRow ( {
id ,
index ,
control ,
databaseName ,
name ,
tableName ,
connectionId ,
removeHighlightedAttribute ,
} : HighlightedAttributeRowProps ) {
const expression = useWatch ( {
control ,
name : ` ${ name } . ${ index } .sqlExpression ` ,
} ) ;
const alias = useWatch ( {
control ,
name : ` ${ name } . ${ index } .alias ` ,
} ) ;
const {
data : explainData ,
error : explainError ,
isLoading : explainLoading ,
refetch : explainExpression ,
} = useExplainQuery (
{
from : { databaseName , tableName } ,
connection : connectionId ,
select : [ { alias , valueExpression : expression } ] ,
where : '' ,
} ,
{ enabled : false } ,
) ;
const runExpression = ( ) = > {
if ( expression ) {
explainExpression ( ) ;
}
} ;
const isExpressionValid = ! ! explainData ? . length ;
const isExpressionInvalid = explainError instanceof ClickHouseQueryError ;
return (
< React.Fragment key = { id } >
< Grid.Col span = { 3 } pe = { 0 } >
< div
style = { { display : 'contents' } }
data - name = { ` ${ name } . ${ index } .sqlExpression ` }
>
< SQLInlineEditorControlled
tableConnection = { {
databaseName ,
tableName ,
connectionId ,
} }
control = { control }
name = { ` ${ name } . ${ index } .sqlExpression ` }
disableKeywordAutocomplete
placeholder = "ResourceAttributes['http.host']"
/ >
< / div >
< / Grid.Col >
< Grid.Col span = { 2 } ps = "xs" >
< Flex align = "center" gap = "sm" >
< Text c = "gray" > AS < / Text >
< SQLInlineEditorControlled
control = { control }
name = { ` ${ name } . ${ index } .alias ` }
placeholder = "Optional Alias"
disableKeywordAutocomplete
/ >
< Tooltip label = "Validate expression" >
< ActionIcon
size = "xs"
variant = "subtle"
color = "gray"
loading = { explainLoading }
disabled = { ! expression || explainLoading }
onClick = { runExpression }
>
< IconCheck size = { 16 } / >
< / ActionIcon >
< / Tooltip >
< ActionIcon
size = "xs"
variant = "subtle"
color = "gray"
onClick = { ( ) = > removeHighlightedAttribute ( index ) }
>
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Flex >
< / Grid.Col >
{ ( isExpressionValid || isExpressionInvalid ) && (
< Grid.Col span = { 5 } pe = { 0 } pt = { 0 } >
{ isExpressionValid && (
< Text c = "green" size = "xs" >
Expression is valid .
< / Text >
) }
{ isExpressionInvalid && (
< ErrorCollapse
summary = "Expression is invalid"
details = { explainError ? . message }
/ >
) }
< / Grid.Col >
) }
< Grid.Col span = { 3 } pe = { 0 } >
< InputControlled
control = { control }
name = { ` ${ name } . ${ index } .luceneExpression ` }
placeholder = "ResourceAttributes.http.host (Optional) "
/ >
< / Grid.Col >
< Grid.Col span = { 1 } pe = { 0 } >
< Text me = "sm" mt = { 6 } >
< Tooltip
label = {
'An optional, Lucene version of the above expression. If provided, it is used when searching for this attribute value.'
}
color = "dark"
c = "white"
multiline
maw = { 600 }
>
< IconHelpCircle size = { 14 } className = "cursor-pointer" / >
< / Tooltip >
< / Text >
< / Grid.Col >
< / React.Fragment >
) ;
}
2025-11-18 19:34:07 +00:00
function HighlightedAttributeExpressionsFormRow ( {
control ,
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
name ,
label ,
helpText ,
2025-12-23 16:34:48 +00:00
} : Omit < TableModelProps , ' setValue ' > & {
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
name :
| 'highlightedTraceAttributeExpressions'
| 'highlightedRowAttributeExpressions' ;
label : string ;
helpText? : string ;
} ) {
2025-12-23 16:34:48 +00:00
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const tableName = useWatch ( { control , name : 'from.tableName' } ) ;
const connectionId = useWatch ( { control , name : 'connection' } ) ;
2025-11-18 19:34:07 +00:00
const {
fields : highlightedAttributes ,
append : appendHighlightedAttribute ,
remove : removeHighlightedAttribute ,
} = useFieldArray ( {
control ,
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
name ,
2025-11-18 19:34:07 +00:00
} ) ;
return (
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
< FormRow label = { label } helpText = { helpText } >
2025-11-18 19:34:07 +00:00
< Grid columns = { 5 } >
2026-01-15 09:21:27 +00:00
{ highlightedAttributes . map ( ( { id } , index ) = > (
< HighlightedAttributeRow
key = { id }
{ . . . {
id ,
index ,
name ,
control ,
databaseName ,
tableName ,
connectionId ,
removeHighlightedAttribute ,
} }
/ >
2025-11-18 19:34:07 +00:00
) ) }
< / Grid >
< Button
2026-01-16 14:42:16 +00:00
variant = "secondary"
2025-11-18 19:34:07 +00:00
size = "sm"
className = "align-self-start"
mt = { highlightedAttributes . length ? 'sm' : 'md' }
onClick = { ( ) = > {
2026-01-15 09:21:27 +00:00
appendHighlightedAttribute (
{
sqlExpression : '' ,
luceneExpression : '' ,
alias : '' ,
} ,
{ shouldFocus : false } ,
) ;
2025-11-18 19:34:07 +00:00
} }
>
2025-12-15 17:06:40 +00:00
< IconCirclePlus size = { 14 } className = "me-2" / >
2025-11-18 19:34:07 +00:00
Add expression
< / Button >
< / FormRow >
) ;
}
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
/** Component for configuring one or more materialized views */
2025-12-23 16:34:48 +00:00
function MaterializedViewsFormSection ( { control , setValue } : TableModelProps ) {
2025-12-29 21:29:05 +00:00
const databaseName = useWatch ( {
control ,
name : ` from.databaseName ` ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
const {
fields : materializedViews ,
append : appendMaterializedView ,
remove : removeMaterializedView ,
} = useFieldArray ( {
control ,
name : 'materializedViews' ,
} ) ;
return (
< Stack gap = "md" >
< FormRow
2026-01-20 16:00:17 +00:00
label = "Materialized Views"
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
helpText = "Configure materialized views for query optimization. These pre-aggregated views can significantly improve query performance on aggregation queries."
>
< Stack gap = "md" >
{ materializedViews . map ( ( field , index ) = > (
< MaterializedViewFormSection
key = { field . id }
control = { control }
mvIndex = { index }
setValue = { setValue }
onRemove = { ( ) = > removeMaterializedView ( index ) }
/ >
) ) }
< Button
2026-01-16 14:42:16 +00:00
variant = "secondary"
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
onClick = { ( ) = > {
appendMaterializedView ( {
databaseName : databaseName ,
tableName : '' ,
dimensionColumns : '' ,
minGranularity : '' ,
timestampColumn : '' ,
aggregatedColumns : [ ] ,
} ) ;
} }
>
< Group >
< IconCirclePlus size = { 16 } / >
Add Materialized View
< / Group >
< / Button >
< / Stack >
< / FormRow >
< / Stack >
) ;
}
/** Component for configuring a single materialized view */
function MaterializedViewFormSection ( {
control ,
mvIndex ,
onRemove ,
setValue ,
} : { mvIndex : number ; onRemove : ( ) = > void } & TableModelProps ) {
const connection = useWatch ( { control , name : ` connection ` } ) ;
2025-12-29 21:29:05 +00:00
const sourceDatabaseName = useWatch ( {
control ,
name : ` from.databaseName ` ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const mvDatabaseName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .databaseName ` ,
defaultValue : sourceDatabaseName ,
} ) ;
const mvTableName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .tableName ` ,
defaultValue : '' ,
} ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
return (
< Stack gap = "sm" >
< Grid columns = { 2 } flex = { 1 } >
< Grid.Col span = { 1 } >
< DatabaseSelectControlled
control = { control }
name = { ` materializedViews. ${ mvIndex } .databaseName ` }
connectionId = { connection }
/ >
< / Grid.Col >
< Grid.Col span = { 1 } >
< Group >
< Box flex = { 1 } >
< DBTableSelectControlled
database = { mvDatabaseName }
control = { control }
name = { ` materializedViews. ${ mvIndex } .tableName ` }
connectionId = { connection }
/ >
< / Box >
< ActionIcon size = "sm" onClick = { onRemove } >
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Group >
< / Grid.Col >
2026-01-05 17:59:39 +00:00
< Grid.Col span = { 2 } >
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
< Text size = "xs" fw = { 500 } mb = { 4 } >
Timestamp Column
< / Text >
< SQLInlineEditorControlled
tableConnection = { {
databaseName : mvDatabaseName ,
tableName : mvTableName ,
connectionId : connection ,
} }
control = { control }
placeholder = "Timestamp"
name = { ` materializedViews. ${ mvIndex } .timestampColumn ` }
disableKeywordAutocomplete
/ >
< / Grid.Col >
< Grid.Col span = { 1 } >
< Text size = "xs" fw = { 500 } mb = { 4 } >
Granularity
< Tooltip
label = { 'The granularity of the timestamp column' }
color = "dark"
c = "white"
multiline
maw = { 600 }
>
< IconHelpCircle size = { 14 } className = "cursor-pointer ms-1" / >
< / Tooltip >
< / Text >
< Controller
control = { control }
name = { ` materializedViews. ${ mvIndex } .minGranularity ` }
render = { ( { field } ) = > (
< Select
{ . . . field }
data = { MV_GRANULARITY_OPTIONS }
placeholder = "Granularity"
size = "sm"
/ >
) }
/ >
< / Grid.Col >
2026-01-05 17:59:39 +00:00
< Grid.Col span = { 1 } >
< Text size = "xs" fw = { 500 } mb = { 4 } >
Minimum Date
< Tooltip
label = "(Optional) The earliest date and time (in the local timezone) for which the materialized view contains data. If not provided, then HyperDX will assume that the materialized view contains data for all dates for which the source table contains data."
color = "dark"
c = "white"
multiline
maw = { 600 }
>
< IconHelpCircle size = { 14 } className = "cursor-pointer ms-1" / >
< / Tooltip >
< / Text >
< Controller
control = { control }
name = { ` materializedViews. ${ mvIndex } .minDate ` }
render = { ( { field } ) = > (
< DateInput
{ . . . field }
value = { field . value ? new Date ( field . value ) : undefined }
onChange = { dateStr = >
field . onChange ( dateStr ? dateStr . toISOString ( ) : null )
}
clearable
highlightToday
placeholder = "YYYY-MM-DD HH:mm:ss"
valueFormat = "YYYY-MM-DD HH:mm:ss"
/ >
) }
/ >
< / Grid.Col >
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
< / Grid >
< Box >
< Text size = "xs" fw = { 500 } mb = { 4 } >
Dimension Columns ( comma - separated )
< Tooltip
label = {
'Columns which are not pre-aggregated in the materialized view and can be used for filtering and grouping.'
}
color = "dark"
c = "white"
multiline
maw = { 600 }
>
< IconHelpCircle size = { 14 } className = "cursor-pointer ms-1" / >
< / Tooltip >
< / Text >
< SQLInlineEditorControlled
tableConnection = { {
databaseName : mvDatabaseName ,
tableName : mvTableName ,
connectionId : connection ,
} }
control = { control }
name = { ` materializedViews. ${ mvIndex } .dimensionColumns ` }
placeholder = "ServiceName, StatusCode"
disableKeywordAutocomplete
/ >
< / Box >
< AggregatedColumnsFormSection
control = { control }
mvIndex = { mvIndex }
setValue = { setValue }
/ >
< Divider / >
< / Stack >
) ;
}
/** Component for configuring the Aggregated Columns list for a single materialized view */
function AggregatedColumnsFormSection ( {
control ,
setValue ,
mvIndex ,
} : TableModelProps & { mvIndex : number } ) {
const {
fields : aggregates ,
append : appendAggregate ,
remove : removeAggregate ,
replace : replaceAggregates ,
} = useFieldArray ( {
control ,
name : ` materializedViews. ${ mvIndex } .aggregatedColumns ` ,
} ) ;
const addAggregate = useCallback ( ( ) = > {
appendAggregate ( { sourceColumn : '' , aggFn : 'avg' , mvColumn : '' } ) ;
} , [ appendAggregate ] ) ;
2025-12-23 16:34:48 +00:00
const kind = useWatch ( { control , name : 'kind' } ) ;
const connection = useWatch ( { control , name : 'connection' } ) ;
const mvTableName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .tableName ` ,
} ) ;
const mvDatabaseName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .databaseName ` ,
} ) ;
const fromDatabaseName = useWatch ( { control , name : 'from.databaseName' } ) ;
const fromTableName = useWatch ( { control , name : 'from.tableName' } ) ;
const prevMvTableNameRef = useRef ( mvTableName ) ;
2026-01-13 12:55:12 +00:00
const metadata = useMetadataWithSettings ( ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
useEffect ( ( ) = > {
2025-12-23 16:34:48 +00:00
( async ( ) = > {
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
try {
2025-12-23 16:34:48 +00:00
if ( mvTableName !== prevMvTableNameRef . current ) {
prevMvTableNameRef . current = mvTableName ;
if (
( kind === SourceKind . Log || kind === SourceKind . Trace ) &&
connection &&
mvDatabaseName &&
mvTableName &&
fromDatabaseName &&
fromTableName
) {
const config = await inferMaterializedViewConfig (
{
databaseName : mvDatabaseName ,
tableName : mvTableName ,
connectionId : connection ,
} ,
{
databaseName : fromDatabaseName ,
tableName : fromTableName ,
connectionId : connection ,
} ,
2026-01-13 12:55:12 +00:00
metadata ,
2025-12-23 16:34:48 +00:00
) ;
if ( config ) {
setValue ( ` materializedViews. ${ mvIndex } ` , config ) ;
replaceAggregates ( config . aggregatedColumns ? ? [ ] ) ;
notifications . show ( {
color : 'green' ,
feat: Add MV granularities and infer config from SummingMergeTree (#1550)
Closes HDX-3064
# Summary
This PR makes two improvements to MV configuration
1. Additional granularities are now available
2. Configuration can now be inferred from SummingMergeTree MVs (in addition to AggregatingMergeTree, which were already supported)
3. Source column inference has been improved to support cases where the target table column name does not follow the `aggFn__sourceColumn` convention.
Also, tests have been updated and additional tests have been backported from the EE repo.
## Demo
https://github.com/user-attachments/assets/7ba6ef0c-2187-4e07-bfda-7b4b690b7a73
## Testing
To test the Summing Merge Tree inference, create a materialized view like so:
```sql
CREATE TABLE default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
ENGINE = SummingMergeTree((count, sumDuration))
ORDER BY (ServiceName, StatusCode, Timestamp)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW default.traces_summed_mv_2 TO default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
AS SELECT
toStartOfInterval(Timestamp, toIntervalMinute(30)) AS Timestamp,
ServiceName,
StatusCode,
count() AS count,
sum(Duration) AS sumDuration,
quantileState(0.5)(Duration) AS quantileDuration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
StatusCode;
```
2026-01-06 19:45:52 +00:00
id : 'mv-infer-success' ,
2025-12-23 16:34:48 +00:00
message :
'Partially inferred materialized view configuration from view schema.' ,
} ) ;
} else {
notifications . show ( {
color : 'yellow' ,
feat: Add MV granularities and infer config from SummingMergeTree (#1550)
Closes HDX-3064
# Summary
This PR makes two improvements to MV configuration
1. Additional granularities are now available
2. Configuration can now be inferred from SummingMergeTree MVs (in addition to AggregatingMergeTree, which were already supported)
3. Source column inference has been improved to support cases where the target table column name does not follow the `aggFn__sourceColumn` convention.
Also, tests have been updated and additional tests have been backported from the EE repo.
## Demo
https://github.com/user-attachments/assets/7ba6ef0c-2187-4e07-bfda-7b4b690b7a73
## Testing
To test the Summing Merge Tree inference, create a materialized view like so:
```sql
CREATE TABLE default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
ENGINE = SummingMergeTree((count, sumDuration))
ORDER BY (ServiceName, StatusCode, Timestamp)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW default.traces_summed_mv_2 TO default.traces_summed_2
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sumDuration` UInt64,
`quantileDuration` AggregateFunction(quantile(0.5), UInt64)
)
AS SELECT
toStartOfInterval(Timestamp, toIntervalMinute(30)) AS Timestamp,
ServiceName,
StatusCode,
count() AS count,
sum(Duration) AS sumDuration,
quantileState(0.5)(Duration) AS quantileDuration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
StatusCode;
```
2026-01-06 19:45:52 +00:00
id : 'mv-infer-failure' ,
2025-12-23 16:34:48 +00:00
message : 'Unable to infer materialized view configuration.' ,
} ) ;
}
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
}
}
} catch ( e ) {
console . error ( e ) ;
}
2025-12-23 16:34:48 +00:00
} ) ( ) ;
} , [
mvTableName ,
kind ,
connection ,
mvDatabaseName ,
fromDatabaseName ,
fromTableName ,
mvIndex ,
replaceAggregates ,
setValue ,
2026-01-13 12:55:12 +00:00
metadata ,
2025-12-23 16:34:48 +00:00
] ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
return (
< Box >
< Text size = "xs" mb = { 4 } >
Pre - aggregated Columns
< Tooltip
label = { 'Columns which are pre-aggregated by the materialized view' }
color = "dark"
c = "white"
multiline
maw = { 600 }
>
< IconHelpCircle size = { 14 } className = "cursor-pointer ms-1" / >
< / Tooltip >
< / Text >
< Grid columns = { 10 } >
{ aggregates . map ( ( field , colIndex ) = > (
< AggregatedColumnRow
key = { field . id }
setValue = { setValue }
control = { control }
mvIndex = { mvIndex }
colIndex = { colIndex }
onRemove = { ( ) = > removeAggregate ( colIndex ) }
/ >
) ) }
< / Grid >
2026-01-16 14:42:16 +00:00
< Button size = "sm" variant = "secondary" onClick = { addAggregate } mt = "lg" >
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
< Group >
< IconCirclePlus size = { 16 } / >
Add Column
< / Group >
< / Button >
< / Box >
) ;
}
/** Component to render one row in the MV Aggregated Columns section */
function AggregatedColumnRow ( {
control ,
mvIndex ,
colIndex ,
onRemove ,
} : TableModelProps & {
mvIndex : number ;
colIndex : number ;
onRemove : ( ) = > void ;
} ) {
const connectionId = useWatch ( { control , name : ` connection ` } ) ;
2025-12-29 21:29:05 +00:00
const sourceDatabaseName = useWatch ( {
control ,
name : ` from.databaseName ` ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
const sourceTableName = useWatch ( { control , name : ` from.tableName ` } ) ;
2025-12-29 21:29:05 +00:00
const mvDatabaseName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .databaseName ` ,
defaultValue : sourceDatabaseName ,
} ) ;
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
const mvTableName = useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .tableName ` ,
} ) ;
const isCount =
useWatch ( {
control ,
name : ` materializedViews. ${ mvIndex } .aggregatedColumns. ${ colIndex } .aggFn ` ,
} ) === 'count' ;
return (
< >
< Grid.Col span = { 2 } >
< SelectControlled
control = { control }
name = { ` materializedViews. ${ mvIndex } .aggregatedColumns. ${ colIndex } .aggFn ` }
data = { MV_AGGREGATE_FUNCTION_OPTIONS }
size = "sm"
/ >
< / Grid.Col >
{ ! isCount && (
< Grid.Col span = { 4 } >
< SQLInlineEditorControlled
tableConnection = { {
databaseName : sourceDatabaseName ,
tableName : sourceTableName ,
connectionId ,
} }
control = { control }
name = { ` materializedViews. ${ mvIndex } .aggregatedColumns. ${ colIndex } .sourceColumn ` }
placeholder = "Source Column"
disableKeywordAutocomplete
/ >
< / Grid.Col >
) }
< Grid.Col span = { ! isCount ? 4 : 8 } >
< Group wrap = "nowrap" >
< Box flex = { 1 } >
< SQLInlineEditorControlled
tableConnection = { {
databaseName : mvDatabaseName ,
tableName : mvTableName ,
connectionId ,
} }
control = { control }
name = { ` materializedViews. ${ mvIndex } .aggregatedColumns. ${ colIndex } .mvColumn ` }
placeholder = "View Column"
disableKeywordAutocomplete
/ >
< / Box >
< ActionIcon size = "sm" onClick = { onRemove } >
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Group >
< / Grid.Col >
< / >
) ;
}
2024-11-12 12:53:15 +00:00
// traceModel= ...
// logModel=....
// traceModel.logModel = 'custom'
// will pop open the custom trace model form as well
// need to make sure we don't recursively render them :joy:
// OR traceModel.logModel = 'log_id_blah'
// custom always points towards the url param
2025-11-18 19:34:07 +00:00
export function LogTableModelForm ( props : TableModelProps ) {
2025-12-23 16:34:48 +00:00
const { control } = props ;
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const tableName = useWatch ( { control , name : 'from.tableName' } ) ;
const connectionId = useWatch ( { control , name : 'connection' } ) ;
2024-11-12 12:53:15 +00:00
const [ showOptionalFields , setShowOptionalFields ] = useState ( false ) ;
return (
< >
< Stack gap = "sm" >
< FormRow
label = { 'Timestamp Column' }
helpText = "DateTime column or expression that is part of your table's primary key."
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "timestampValueExpression"
disableKeywordAutocomplete
/ >
< / FormRow >
< FormRow
label = { 'Default Select' }
helpText = "Default columns selected in search results (this can be customized per search later)"
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "defaultTableSelectExpression"
placeholder = "Timestamp, Body"
/ >
< / FormRow >
< Box >
{ ! showOptionalFields && (
< Anchor
underline = "always"
onClick = { ( ) = > setShowOptionalFields ( true ) }
size = "xs"
>
2025-12-15 17:06:40 +00:00
< Group gap = "xs" >
< IconSettings size = { 14 } / >
Configure Optional Fields
< / Group >
2024-11-12 12:53:15 +00:00
< / Anchor >
) }
{ showOptionalFields && (
< Button
onClick = { ( ) = > setShowOptionalFields ( false ) }
size = "xs"
variant = "subtle"
>
Hide Optional Fields
< / Button >
) }
< / Box >
< / Stack >
< Stack
gap = "sm"
style = { {
display : showOptionalFields ? 'flex' : 'none' ,
} }
>
< Divider / >
< FormRow label = { 'Service Name Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "serviceNameExpression"
placeholder = "ServiceName"
/ >
< / FormRow >
< FormRow label = { 'Log Level Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "severityTextExpression"
placeholder = "SeverityText"
/ >
< / FormRow >
2025-03-25 18:40:16 +00:00
< FormRow label = { 'Body Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2025-03-25 18:40:16 +00:00
control = { control }
name = "bodyExpression"
placeholder = "Body"
/ >
< / FormRow >
2024-11-12 12:53:15 +00:00
< FormRow label = { 'Log Attributes Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "eventAttributesExpression"
placeholder = "LogAttributes"
/ >
< / FormRow >
< FormRow label = { 'Resource Attributes Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "resourceAttributesExpression"
placeholder = "ResourceAttributes"
/ >
< / FormRow >
2025-03-04 23:13:46 +00:00
< FormRow
label = { 'Displayed Timestamp Column' }
2025-10-27 13:44:48 +00:00
helpText = "This DateTime column is used to display and order search results."
2025-03-04 23:13:46 +00:00
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2025-03-04 23:13:46 +00:00
control = { control }
name = "displayedTimestampValueExpression"
disableKeywordAutocomplete
/ >
< / FormRow >
2024-11-12 12:53:15 +00:00
< Divider / >
2025-03-25 18:40:16 +00:00
< FormRow
label = { 'Correlated Metric Source' }
helpText = "HyperDX Source for metrics associated with logs. Optional"
>
< SourceSelectControlled control = { control } name = "metricSourceId" / >
< / FormRow >
2024-11-12 12:53:15 +00:00
< FormRow
label = { 'Correlated Trace Source' }
helpText = "HyperDX Source for traces associated with logs. Optional"
>
< SourceSelectControlled control = { control } name = "traceSourceId" / >
< / FormRow >
< FormRow label = { 'Trace Id Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "traceIdExpression"
placeholder = "TraceId"
/ >
< / FormRow >
< FormRow label = { 'Span Id Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "spanIdExpression"
placeholder = "SpanId"
/ >
< / FormRow >
2025-03-25 18:40:16 +00:00
2024-11-12 12:53:15 +00:00
< Divider / >
{ / * < F o r m R o w
label = { 'Unique Row ID Expression' }
helpText = "Unique identifier for a given row, will be primary key if not specified. Used for showing full row details in search results."
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "uniqueRowIdExpression"
placeholder = "Timestamp, ServiceName, Body"
/ >
< / FormRow > * / }
{ / * < F o r m R o w l a b e l = { ' T a b l e F i l t e r E x p r e s s i o n ' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "tableFilterExpression"
placeholder = "ServiceName = 'only_this_service'"
/ >
< / FormRow > * / }
< FormRow
label = { 'Implicit Column Expression' }
helpText = "Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "implicitColumnExpression"
placeholder = "Body"
/ >
< / FormRow >
2025-11-18 19:34:07 +00:00
< Divider / >
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
< HighlightedAttributeExpressionsFormRow
{ . . . props }
name = "highlightedRowAttributeExpressions"
label = "Highlighted Attributes"
2025-11-20 21:30:20 +00:00
helpText = "Expressions defining row-level attributes which are displayed in the row side panel for the selected row."
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
/ >
< HighlightedAttributeExpressionsFormRow
{ . . . props }
name = "highlightedTraceAttributeExpressions"
label = "Highlighted Trace Attributes"
2025-11-20 21:30:20 +00:00
helpText = "Expressions defining trace-level attributes which are displayed in the trace view for the selected trace."
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
/ >
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
< Divider / >
< MaterializedViewsFormSection { ...props } / >
2024-11-12 12:53:15 +00:00
< / Stack >
< / >
) ;
}
2025-11-18 19:34:07 +00:00
export function TraceTableModelForm ( props : TableModelProps ) {
2025-12-23 16:34:48 +00:00
const { control } = props ;
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const tableName = useWatch ( { control , name : 'from.tableName' } ) ;
const connectionId = useWatch ( { control , name : 'connection' } ) ;
2024-11-12 12:53:15 +00:00
return (
< Stack gap = "sm" >
2025-05-07 16:08:21 +00:00
< FormRow
label = { 'Timestamp Column' }
helpText = "DateTime column or expression defines the start of the span"
>
2024-11-12 12:53:15 +00:00
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "timestampValueExpression"
placeholder = "Timestamp"
disableKeywordAutocomplete
/ >
< / FormRow >
2024-11-22 05:44:33 +00:00
< FormRow
label = { 'Default Select' }
helpText = "Default columns selected in search results (this can be customized per search later)"
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-22 05:44:33 +00:00
control = { control }
name = "defaultTableSelectExpression"
placeholder = "Timestamp, ServiceName, StatusCode, Duration, SpanName"
/ >
< / FormRow >
2024-11-12 12:53:15 +00:00
< Divider / >
< FormRow label = { 'Duration Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "durationExpression"
placeholder = "Duration Column"
/ >
< / FormRow >
< FormRow label = { 'Duration Precision' } >
< Box mx = "xl" >
< Controller
control = { control }
name = "durationPrecision"
2025-06-25 16:41:42 +00:00
render = { ( { field : { onChange , value } } ) = > (
2024-11-12 12:53:15 +00:00
< div style = { { width : '90%' , marginBottom : 8 } } >
< Slider
color = "green"
defaultValue = { 0 }
min = { 0 }
max = { 9 }
marks = { [
{ value : 0 , label : 'Seconds' } ,
{ value : 3 , label : 'Millisecond' } ,
{ value : 6 , label : 'Microsecond' } ,
{ value : 9 , label : 'Nanosecond' } ,
] }
value = { value }
onChange = { onChange }
/ >
< / div >
) }
/ >
< / Box >
< / FormRow >
< FormRow label = { 'Trace Id Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "traceIdExpression"
placeholder = "TraceId"
/ >
< / FormRow >
< FormRow label = { 'Span Id Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "spanIdExpression"
placeholder = "SpanId"
/ >
< / FormRow >
< FormRow label = { 'Parent Span Id Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "parentSpanIdExpression"
placeholder = "ParentSpanId"
/ >
< / FormRow >
< FormRow label = { 'Span Name Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "spanNameExpression"
placeholder = "SpanName"
/ >
< / FormRow >
2024-11-22 05:44:33 +00:00
< FormRow label = { 'Span Kind Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-22 05:44:33 +00:00
control = { control }
name = "spanKindExpression"
placeholder = "SpanKind"
/ >
< / FormRow >
2024-11-12 12:53:15 +00:00
< Divider / >
< FormRow
label = { 'Correlated Log Source' }
helpText = "HyperDX Source for logs associated with traces. Optional"
>
< SourceSelectControlled control = { control } name = "logSourceId" / >
< / FormRow >
2025-02-04 02:00:40 +00:00
< FormRow
label = { 'Correlated Session Source' }
helpText = "HyperDX Source for sessions associated with traces. Optional"
>
< SourceSelectControlled control = { control } name = "sessionSourceId" / >
< / FormRow >
2025-04-01 13:05:03 +00:00
< FormRow
label = { 'Correlated Metric Source' }
helpText = "HyperDX Source for metrics associated with traces. Optional"
>
< SourceSelectControlled control = { control } name = "metricSourceId" / >
< / FormRow >
2024-11-12 12:53:15 +00:00
< FormRow label = { 'Status Code Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "statusCodeExpression"
placeholder = "StatusCode"
/ >
< / FormRow >
< FormRow label = { 'Status Message Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "statusMessageExpression"
placeholder = "StatusMessage"
/ >
< / FormRow >
< FormRow label = { 'Service Name Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-12 12:53:15 +00:00
control = { control }
name = "serviceNameExpression"
placeholder = "ServiceName"
/ >
< / FormRow >
2024-11-22 05:44:33 +00:00
< FormRow label = { 'Resource Attributes Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-22 05:44:33 +00:00
control = { control }
name = "resourceAttributesExpression"
placeholder = "ResourceAttributes"
/ >
< / FormRow >
< FormRow label = { 'Event Attributes Expression' } >
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-11-22 05:44:33 +00:00
control = { control }
name = "eventAttributesExpression"
placeholder = "SpanAttributes"
/ >
< / FormRow >
2025-05-07 16:08:21 +00:00
< FormRow
label = { 'Span Events Expression' }
helpText = "Expression to extract span events. Used to capture events associated with spans. Expected to be Nested ( Timestamp DateTime64(9), Name LowCardinality(String), Attributes Map(LowCardinality(String), String)"
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-05-07 16:08:21 +00:00
databaseName ,
tableName ,
connectionId ,
} }
control = { control }
name = "spanEventsValueExpression"
placeholder = "Events"
/ >
< / FormRow >
2024-12-11 03:26:02 +00:00
< FormRow
label = { 'Implicit Column Expression' }
helpText = "Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."
>
< SQLInlineEditorControlled
2025-10-02 21:33:53 +00:00
tableConnection = { {
2025-03-27 19:17:29 +00:00
databaseName ,
tableName ,
connectionId ,
} }
2024-12-11 03:26:02 +00:00
control = { control }
name = "implicitColumnExpression"
placeholder = "SpanName"
/ >
< / FormRow >
2025-10-27 13:44:48 +00:00
< FormRow
label = { 'Displayed Timestamp Column' }
helpText = "This DateTime column is used to display and order search results."
>
< SQLInlineEditorControlled
tableConnection = { {
databaseName ,
tableName ,
connectionId ,
} }
control = { control }
name = "displayedTimestampValueExpression"
disableKeywordAutocomplete
/ >
< / FormRow >
2025-11-18 19:34:07 +00:00
< Divider / >
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
< HighlightedAttributeExpressionsFormRow
{ . . . props }
name = "highlightedRowAttributeExpressions"
label = "Highlighted Attributes"
2025-11-20 21:30:20 +00:00
helpText = "Expressions defining row-level attributes which are displayed in the row side panel for the selected row"
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
/ >
< HighlightedAttributeExpressionsFormRow
{ . . . props }
name = "highlightedTraceAttributeExpressions"
label = "Highlighted Trace Attributes"
2025-11-20 21:30:20 +00:00
helpText = "Expressions defining trace-level attributes which are displayed in the trace view for the selected trace."
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776
Closes HDX-2752
# Summary
This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data.
## Demo
Source Configuration:
<img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" />
Side panel:
<img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" />
Inferred link:
<img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" />
<details>
<summary>logview attribute definition</summary>
```sql
if(
NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName),
concat('https://logview.com?q=',
'trace_id=', TraceId,
'&span_id=', SpanId,
'&service=', ServiceName,
'&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'),
'&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ')
),
''
)
```
</details>
2025-11-19 21:38:07 +00:00
/ >
feat: Add materialized view support (Beta) (#1507)
Closes HDX-3082
# Summary
This PR back-ports support for materialized views from the EE repo. Note that this feature is in **Beta**, and is subject to significant changes.
This feature is intended to support:
1. Configuring AggregatingMergeTree (or SummingMergeTree) Materialized Views which are associated with a Source
2. Automatically selecting and querying an associated materialized view when a query supports it, in Chart Explorer, Custom Dashboards, the Services Dashboard, and the Search Page Histogram.
3. A UX for understanding what materialized views are available for a source, and whether (and why) it is or is not being used for a particular visualization.
## Note to Reviewer(s)
This is a large PR, but the code has largely already been reviewed.
- For net-new files, types, components, and utility functions, the code does not differ from the EE repo
- Changes to the various services dashboard pages do not differ from the EE repo
- Changes to `useOffsetPaginatedQuery`, `useChartConfig`, and `DBEditTimeChart` differ slightly due to unrelated (to MVs) drift between this repo and the EE repo, and due to the lack of feature toggles in this repo. **This is where slightly closer review would be most valuable.**
## Demo
<details>
<summary>Demo: MV Configuration</summary>
https://github.com/user-attachments/assets/fedf3bcf-892c-4b8d-a788-7e231e23bcc3
</details>
<details>
<summary>Demo: Chart Explorer</summary>
https://github.com/user-attachments/assets/fc8d1efa-7edc-42fc-98f0-75431cc056b8
</details>
<details>
<summary>Demo: Dashboards</summary>
https://github.com/user-attachments/assets/f3cb247e-711f-4d90-95b8-cf977e94f065
</details>
## Known Limitations
This feature is in Beta due to the following known limitations, which will be addressed in subsequent PRs:
1. Visualization start and end time, when not aligned with the granularity of MVs, will result in statistics based on the MV "time buckets" which fall inside the date range. This may not align exactly with the source table data which is in the selected date range.
2. Alerts do not make use of MVs, even if the associated visualization does. Due to (1), this means that alert values may not exactly match the values shown in the associated visualization.
## Differences in OSS vs EE Support
- In OSS, there is a beta label on the MV configurations section
- In EE there are feature toggles to enable MV support, in OSS the feature is enabled for all teams, but will only run for sources with MVs configured.
## Testing
To test, a couple of MVs can be created on the default `otel_traces` table, directly in ClickHouse:
<details>
<summary>Example MVs DDL</summary>
```sql
CREATE TABLE default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` SimpleAggregateFunction(sum, UInt64),
`sum__Duration` SimpleAggregateFunction(sum, UInt64),
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName);
CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`version` LowCardinality(String),
`StatusCode` LowCardinality(String),
`count` UInt64,
`sum__Duration` Int64,
`avg__Duration` AggregateFunction(avg, UInt64),
`quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64),
`min__Duration` SimpleAggregateFunction(min, UInt64),
`max__Duration` SimpleAggregateFunction(max, UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
StatusCode,
count() AS count,
sum(Duration) AS sum__Duration,
avgState(Duration) AS avg__Duration,
quantileTDigestState(0.5)(Duration) AS quantile__Duration,
minSimpleState(Duration) AS min__Duration,
maxSimpleState(Duration) AS max__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind,
StatusCode;
```
```sql
CREATE TABLE default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
ENGINE = AggregatingMergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (Timestamp, ServiceName, SpanKind);
CREATE MATERIALIZED VIEW default.span_kind_rollup_1m_mv TO default.span_kind_rollup_1m
(
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`SpanKind` LowCardinality(String),
`histogram__Duration` AggregateFunction(histogram(20), UInt64)
)
AS SELECT
toStartOfMinute(Timestamp) AS Timestamp,
ServiceName,
SpanKind,
histogramState(20)(Duration) AS histogram__Duration
FROM default.otel_traces
GROUP BY
Timestamp,
ServiceName,
SpanKind;
```
</details>
Then you'll need to configure the materialized views in your source settings:
<details>
<summary>Source Configuration (should auto-infer when MVs are selected)</summary>
<img width="949" height="1011" alt="Screenshot 2025-12-19 at 10 26 54 AM" src="https://github.com/user-attachments/assets/fc46a1b9-de8b-4b95-a8ef-ba5fee905685" />
</details>
2025-12-19 16:17:23 +00:00
< Divider / >
< MaterializedViewsFormSection { ...props } / >
2024-11-12 12:53:15 +00:00
< / Stack >
) ;
}
2025-12-29 21:29:05 +00:00
export function SessionTableModelForm ( { control } : TableModelProps ) {
2025-12-23 16:34:48 +00:00
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const connectionId = useWatch ( { control , name : 'connection' } ) ;
const tableName = useWatch ( { control , name : 'from.tableName' } ) ;
const prevTableNameRef = useRef ( tableName ) ;
2026-01-13 12:55:12 +00:00
const metadata = useMetadataWithSettings ( ) ;
2025-02-11 19:59:12 +00:00
2025-09-03 22:24:05 +00:00
useEffect ( ( ) = > {
2025-12-23 16:34:48 +00:00
( async ( ) = > {
2025-09-03 22:24:05 +00:00
try {
2025-12-23 16:34:48 +00:00
if ( tableName && tableName !== prevTableNameRef . current ) {
prevTableNameRef . current = tableName ;
2025-09-03 22:24:05 +00:00
const isValid = await isValidSessionsTable ( {
databaseName ,
tableName ,
connectionId ,
2026-01-13 12:55:12 +00:00
metadata ,
2025-09-03 22:24:05 +00:00
} ) ;
if ( ! isValid ) {
notifications . show ( {
color : 'red' ,
message : ` ${ tableName } is not a valid Sessions schema. ` ,
} ) ;
}
}
} catch ( e ) {
console . error ( e ) ;
notifications . show ( {
color : 'red' ,
message : e.message ,
} ) ;
}
2025-12-23 16:34:48 +00:00
} ) ( ) ;
2026-01-13 12:55:12 +00:00
} , [ tableName , databaseName , connectionId , metadata ] ) ;
2025-09-03 22:24:05 +00:00
2025-02-11 19:59:12 +00:00
return (
< >
< Stack gap = "sm" >
< FormRow
label = { 'Correlated Trace Source' }
helpText = "HyperDX Source for traces associated with sessions. Required"
>
< SourceSelectControlled control = { control } name = "traceSourceId" / >
< / FormRow >
2026-01-22 00:25:39 +00:00
< FormRow
label = { 'Timestamp Column' }
helpText = "DateTime column or expression that is part of your table's primary key."
>
< SQLInlineEditorControlled
tableConnection = { {
databaseName ,
tableName ,
connectionId ,
} }
control = { control }
name = "timestampValueExpression"
disableKeywordAutocomplete
/ >
< / FormRow >
2025-02-11 19:59:12 +00:00
< / Stack >
< / >
) ;
}
2025-06-25 16:41:42 +00:00
interface TableModelProps {
control : Control < TSourceUnion > ;
setValue : UseFormSetValue < TSourceUnion > ;
}
2025-12-23 16:34:48 +00:00
export function MetricTableModelForm ( { control , setValue } : TableModelProps ) {
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : DEFAULT_DATABASE ,
} ) ;
const connectionId = useWatch ( { control , name : 'connection' } ) ;
const metricTables = useWatch ( { control , name : 'metricTables' } ) ;
const prevMetricTablesRef = useRef ( metricTables ) ;
2025-02-09 01:25:23 +00:00
2026-01-13 12:55:12 +00:00
const metadata = useMetadataWithSettings ( ) ;
2025-02-12 19:04:50 +00:00
useEffect ( ( ) = > {
2025-02-26 00:00:48 +00:00
for ( const [ _key , _value ] of Object . entries ( OTEL_CLICKHOUSE_EXPRESSIONS ) ) {
setValue ( _key as any , _value ) ;
}
2025-12-23 16:34:48 +00:00
} , [ setValue ] ) ;
useEffect ( ( ) = > {
( async ( ) = > {
2025-02-12 19:04:50 +00:00
try {
2025-12-23 16:34:48 +00:00
if ( metricTables && prevMetricTablesRef . current ) {
// Check which metric table changed
for ( const metricType of Object . values ( MetricsDataType ) ) {
const newValue =
metricTables [ metricType as keyof typeof metricTables ] ;
const prevValue =
prevMetricTablesRef . current [
metricType as keyof typeof prevMetricTablesRef . current
] ;
if ( newValue !== prevValue ) {
const isValid = await isValidMetricTable ( {
databaseName ,
tableName : newValue as string ,
connectionId ,
metricType : metricType as MetricsDataType ,
2026-01-13 12:55:12 +00:00
metadata ,
2025-02-12 19:04:50 +00:00
} ) ;
2025-12-23 16:34:48 +00:00
if ( ! isValid ) {
notifications . show ( {
color : 'red' ,
message : ` ${ newValue } is not a valid OTEL ${ metricType } schema. ` ,
} ) ;
}
2025-02-12 19:04:50 +00:00
}
}
}
2025-12-23 16:34:48 +00:00
prevMetricTablesRef . current = metricTables ;
2025-02-12 19:04:50 +00:00
} catch ( e ) {
console . error ( e ) ;
notifications . show ( {
color : 'red' ,
message : e.message ,
} ) ;
}
2025-12-23 16:34:48 +00:00
} ) ( ) ;
2026-01-13 12:55:12 +00:00
} , [ metricTables , databaseName , connectionId , metadata ] ) ;
2025-02-09 01:25:23 +00:00
return (
< >
< Stack gap = "sm" >
2025-05-21 17:22:04 +00:00
{ Object . values ( MetricsDataType ) . map ( metricType = > (
2025-02-12 19:04:50 +00:00
< FormRow
key = { metricType . toLowerCase ( ) }
label = { ` ${ metricType } Table ` }
2025-05-21 17:22:04 +00:00
helpText = {
metricType === MetricsDataType . ExponentialHistogram ||
metricType === MetricsDataType . Summary
? ` Table containing ${ metricType . toLowerCase ( ) } metrics data. Note: not yet fully supported by HyperDX `
: ` Table containing ${ metricType . toLowerCase ( ) } metrics data `
}
2025-02-12 19:04:50 +00:00
>
< DBTableSelectControlled
connectionId = { connectionId }
database = { databaseName }
control = { control }
name = { ` metricTables. ${ metricType . toLowerCase ( ) } ` }
/ >
< / FormRow >
) ) }
2025-03-25 18:40:16 +00:00
< FormRow
label = { 'Correlated Log Source' }
helpText = "HyperDX Source for logs associated with metrics. Optional"
>
< SourceSelectControlled control = { control } name = "logSourceId" / >
< / FormRow >
2025-02-09 01:25:23 +00:00
< / Stack >
< / >
) ;
}
function TableModelForm ( {
control ,
setValue ,
kind ,
} : {
2025-06-25 16:41:42 +00:00
control : Control < TSourceUnion > ;
setValue : UseFormSetValue < TSourceUnion > ;
2025-02-09 01:25:23 +00:00
kind : SourceKind ;
} ) {
switch ( kind ) {
case SourceKind . Log :
2025-12-23 16:34:48 +00:00
return < LogTableModelForm control = { control } setValue = { setValue } / > ;
2025-02-09 01:25:23 +00:00
case SourceKind . Trace :
2025-12-23 16:34:48 +00:00
return < TraceTableModelForm control = { control } setValue = { setValue } / > ;
2025-02-11 19:59:12 +00:00
case SourceKind . Session :
2025-12-23 16:34:48 +00:00
return < SessionTableModelForm control = { control } setValue = { setValue } / > ;
2025-02-09 01:25:23 +00:00
case SourceKind . Metric :
2025-12-23 16:34:48 +00:00
return < MetricTableModelForm control = { control } setValue = { setValue } / > ;
2025-02-09 01:25:23 +00:00
}
}
2024-11-12 12:53:15 +00:00
export function TableSourceForm ( {
sourceId ,
onSave ,
onCreate ,
isNew = false ,
defaultName ,
onCancel ,
} : {
sourceId? : string ;
onSave ? : ( ) = > void ;
onCreate ? : ( source : TSource ) = > void ;
onCancel ? : ( ) = > void ;
isNew? : boolean ;
defaultName? : string ;
} ) {
const { data : source } = useSource ( { id : sourceId } ) ;
const { data : connections } = useConnections ( ) ;
2025-12-29 21:29:05 +00:00
const { control , setValue , handleSubmit , resetField , setError , clearErrors } =
useForm < TSourceUnion > ( {
defaultValues : {
kind : SourceKind.Log ,
name : defaultName ,
connection : connections?. [ 0 ] ? . id ,
from : {
databaseName : 'default' ,
tableName : '' ,
} ,
2026-01-21 16:07:30 +00:00
querySettings : source?.querySettings ,
2024-11-12 12:53:15 +00:00
} ,
2025-12-29 21:29:05 +00:00
// TODO: HDX-1768 remove type assertion
values : source as TSourceUnion ,
resetOptions : {
keepDirtyValues : true ,
keepErrors : true ,
} ,
} ) ;
2024-11-12 12:53:15 +00:00
2025-12-29 21:29:05 +00:00
const watchedConnection = useWatch ( {
control ,
name : 'connection' ,
defaultValue : source?.connection ,
} ) ;
const watchedDatabaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
defaultValue : source?.from?.databaseName || DEFAULT_DATABASE ,
} ) ;
const watchedTableName = useWatch ( {
control ,
name : 'from.tableName' ,
defaultValue : source?.from?.tableName ,
} ) ;
const watchedKind = useWatch ( {
control ,
name : 'kind' ,
defaultValue : source?.kind || SourceKind . Log ,
} ) ;
2025-12-23 16:34:48 +00:00
const prevTableNameRef = useRef ( watchedTableName ) ;
2026-01-13 12:55:12 +00:00
const metadata = useMetadataWithSettings ( ) ;
2024-11-12 12:53:15 +00:00
useEffect ( ( ) = > {
2025-12-23 16:34:48 +00:00
( async ( ) = > {
2024-11-12 12:53:15 +00:00
try {
2025-12-23 16:34:48 +00:00
if ( watchedTableName !== prevTableNameRef . current ) {
prevTableNameRef . current = watchedTableName ;
if (
watchedConnection != null &&
watchedDatabaseName != null &&
( watchedKind === SourceKind . Metric || watchedTableName != null )
) {
const config = await inferTableSourceConfig ( {
databaseName : watchedDatabaseName ,
tableName :
watchedKind !== SourceKind . Metric ? watchedTableName : '' ,
connectionId : watchedConnection ,
2026-01-13 12:55:12 +00:00
metadata ,
2024-11-12 12:53:15 +00:00
} ) ;
2025-12-23 16:34:48 +00:00
if ( Object . keys ( config ) . length > 0 ) {
notifications . show ( {
color : 'green' ,
message :
'Automatically inferred source configuration from table schema.' ,
} ) ;
}
Object . entries ( config ) . forEach ( ( [ key , value ] ) = > {
resetField ( key as any , {
keepDirty : true ,
defaultValue : value ,
} ) ;
2024-11-12 12:53:15 +00:00
} ) ;
2025-12-23 16:34:48 +00:00
}
2024-11-12 12:53:15 +00:00
}
} catch ( e ) {
console . error ( e ) ;
}
2025-12-23 16:34:48 +00:00
} ) ( ) ;
} , [
watchedTableName ,
watchedConnection ,
watchedDatabaseName ,
watchedKind ,
resetField ,
2026-01-13 12:55:12 +00:00
metadata ,
2025-12-23 16:34:48 +00:00
] ) ;
2024-11-12 12:53:15 +00:00
// Sets the default connection field to the first connection after the
// connections have been loaded
useEffect ( ( ) = > {
resetField ( 'connection' , { defaultValue : connections?. [ 0 ] ? . id } ) ;
} , [ connections , resetField ] ) ;
2025-12-29 21:29:05 +00:00
const kind = useWatch ( {
control ,
name : 'kind' ,
defaultValue : source?.kind || SourceKind . Log ,
} ) ;
2024-11-12 12:53:15 +00:00
const createSource = useCreateSource ( ) ;
const updateSource = useUpdateSource ( ) ;
const deleteSource = useDeleteSource ( ) ;
2025-07-24 15:41:56 +00:00
// Bidirectional source linking
const { data : sources } = useSources ( ) ;
2025-12-23 16:34:48 +00:00
const currentSourceId = useWatch ( { control , name : 'id' } ) ;
// Watch all potential correlation fields
const logSourceId = useWatch ( { control , name : 'logSourceId' } ) ;
const traceSourceId = useWatch ( { control , name : 'traceSourceId' } ) ;
const metricSourceId = useWatch ( { control , name : 'metricSourceId' } ) ;
const sessionTraceSourceId = useWatch ( { control , name : 'traceSourceId' } ) ; // For sessions
const prevLogSourceIdRef = useRef ( logSourceId ) ;
const prevTraceSourceIdRef = useRef ( traceSourceId ) ;
const prevMetricSourceIdRef = useRef ( metricSourceId ) ;
const prevSessionTraceSourceIdRef = useRef ( sessionTraceSourceId ) ;
2025-07-24 15:41:56 +00:00
useEffect ( ( ) = > {
2025-12-23 16:34:48 +00:00
( async ( ) = > {
if ( ! currentSourceId || ! sources || ! kind ) return ;
2025-07-24 15:41:56 +00:00
const correlationFields = CORRELATION_FIELD_MAP [ kind ] ;
2025-12-23 16:34:48 +00:00
if ( ! correlationFields ) return ;
// Check each field for changes
const changedFields : Array < {
name : keyof TSourceUnion ;
value : string | undefined ;
} > = [ ] ;
if ( logSourceId !== prevLogSourceIdRef . current ) {
prevLogSourceIdRef . current = logSourceId ;
changedFields . push ( {
name : 'logSourceId' as keyof TSourceUnion ,
value : logSourceId ? ? undefined ,
} ) ;
}
if ( traceSourceId !== prevTraceSourceIdRef . current ) {
prevTraceSourceIdRef . current = traceSourceId ;
changedFields . push ( {
name : 'traceSourceId' as keyof TSourceUnion ,
value : traceSourceId ? ? undefined ,
} ) ;
}
if ( metricSourceId !== prevMetricSourceIdRef . current ) {
prevMetricSourceIdRef . current = metricSourceId ;
changedFields . push ( {
name : 'metricSourceId' as keyof TSourceUnion ,
value : metricSourceId ? ? undefined ,
} ) ;
}
if (
sessionTraceSourceId !== prevSessionTraceSourceIdRef . current &&
kind === SourceKind . Session
) {
prevSessionTraceSourceIdRef . current = sessionTraceSourceId ;
changedFields . push ( {
name : 'traceSourceId' as keyof TSourceUnion ,
value : sessionTraceSourceId ? ? undefined ,
} ) ;
}
2025-07-24 15:41:56 +00:00
2025-12-23 16:34:48 +00:00
for ( const {
name : fieldName ,
value : newTargetSourceId ,
} of changedFields ) {
if ( ! ( fieldName in correlationFields ) ) continue ;
const targetConfigs = correlationFields [ fieldName ] ;
for ( const { targetKind , targetField } of targetConfigs ) {
// Find the previously linked source if any
const previouslyLinkedSource = sources . find (
s = > s . kind === targetKind && s [ targetField ] === currentSourceId ,
) ;
// If there was a previously linked source and it's different from the new one, unlink it
if (
previouslyLinkedSource &&
previouslyLinkedSource . id !== newTargetSourceId
) {
await updateSource . mutateAsync ( {
source : {
. . . previouslyLinkedSource ,
[ targetField ] : undefined ,
} as TSource ,
} ) ;
}
// If a new source is selected, link it back
if ( newTargetSourceId ) {
const targetSource = sources . find ( s = > s . id === newTargetSourceId ) ;
if ( targetSource && targetSource . kind === targetKind ) {
// Only update if the target field is empty to avoid overwriting existing correlations
if ( ! targetSource [ targetField ] ) {
await updateSource . mutateAsync ( {
source : {
. . . targetSource ,
[ targetField ] : currentSourceId ,
} as TSource ,
} ) ;
}
2025-07-24 15:41:56 +00:00
}
}
}
}
2025-12-23 16:34:48 +00:00
} ) ( ) ;
} , [
logSourceId ,
traceSourceId ,
metricSourceId ,
sessionTraceSourceId ,
kind ,
currentSourceId ,
sources ,
updateSource ,
] ) ;
2025-07-24 15:41:56 +00:00
2025-06-25 16:41:42 +00:00
const sourceFormSchema = sourceSchemaWithout ( { id : true } ) ;
2025-12-02 00:12:46 +00:00
const handleError = useCallback (
( error : z.ZodError < TSourceUnion > ) = > {
const errors = error . errors ;
for ( const err of errors ) {
const errorPath : string = err . path . join ( '.' ) ;
// TODO: HDX-1768 get rid of this type assertion if possible
setError ( errorPath as any , { . . . err } ) ;
}
notifications . show ( {
color : 'red' ,
message : (
< Stack >
< Text size = "sm" >
< b > Failed to create source < / b >
2025-06-25 16:41:42 +00:00
< / Text >
2025-12-02 00:12:46 +00:00
{ errors . map ( ( err , i ) = > (
< Text key = { i } size = "sm" >
✖ { err . message }
< / Text >
) ) }
< / Stack >
) ,
} ) ;
} ,
[ setError ] ,
) ;
2025-06-25 16:41:42 +00:00
2024-11-12 12:53:15 +00:00
const _onCreate = useCallback ( ( ) = > {
2025-06-25 16:41:42 +00:00
clearErrors ( ) ;
2025-07-24 15:41:56 +00:00
handleSubmit ( async data = > {
2025-06-25 16:41:42 +00:00
const parseResult = sourceFormSchema . safeParse ( data ) ;
if ( parseResult . error ) {
handleError ( parseResult . error ) ;
return ;
}
2025-07-24 15:41:56 +00:00
2024-11-12 12:53:15 +00:00
createSource . mutate (
2025-06-25 16:41:42 +00:00
// TODO: HDX-1768 get rid of this type assertion
{ source : data as TSource } ,
2024-11-12 12:53:15 +00:00
{
2025-07-24 15:41:56 +00:00
onSuccess : async newSource = > {
// Handle bidirectional linking for new sources
const correlationFields = CORRELATION_FIELD_MAP [ newSource . kind ] ;
if ( correlationFields && sources ) {
for ( const [ fieldName , targetConfigs ] of Object . entries (
correlationFields ,
) ) {
const targetSourceId = ( newSource as any ) [ fieldName ] ;
if ( targetSourceId ) {
for ( const { targetKind , targetField } of targetConfigs ) {
const targetSource = sources . find (
s = > s . id === targetSourceId ,
) ;
if ( targetSource && targetSource . kind === targetKind ) {
// Only update if the target field is empty to avoid overwriting existing correlations
if ( ! targetSource [ targetField ] ) {
await updateSource . mutateAsync ( {
source : {
. . . targetSource ,
[ targetField ] : newSource . id ,
} as TSource ,
} ) ;
}
}
}
}
}
}
onCreate ? . ( newSource ) ;
2024-11-12 12:53:15 +00:00
notifications . show ( {
color : 'green' ,
message : 'Source created' ,
} ) ;
} ,
2025-06-25 16:41:42 +00:00
onError : error = > {
2024-11-12 12:53:15 +00:00
notifications . show ( {
color : 'red' ,
2025-06-25 16:41:42 +00:00
message : ` Failed to create source - ${ error . message } ` ,
2024-11-12 12:53:15 +00:00
} ) ;
} ,
} ,
) ;
} ) ( ) ;
2025-07-24 15:41:56 +00:00
} , [
2025-12-02 00:12:46 +00:00
clearErrors ,
handleError ,
sourceFormSchema ,
2025-07-24 15:41:56 +00:00
handleSubmit ,
createSource ,
onCreate ,
sources ,
updateSource ,
] ) ;
2024-11-12 12:53:15 +00:00
const _onSave = useCallback ( ( ) = > {
2025-06-25 16:41:42 +00:00
clearErrors ( ) ;
2024-11-12 12:53:15 +00:00
handleSubmit ( data = > {
2025-06-25 16:41:42 +00:00
const parseResult = sourceFormSchema . safeParse ( data ) ;
if ( parseResult . error ) {
handleError ( parseResult . error ) ;
return ;
}
2024-11-12 12:53:15 +00:00
updateSource . mutate (
2025-06-25 16:41:42 +00:00
// TODO: HDX-1768 get rid of this type assertion
{ source : data as TSource } ,
2024-11-12 12:53:15 +00:00
{
onSuccess : ( ) = > {
onSave ? . ( ) ;
notifications . show ( {
color : 'green' ,
message : 'Source updated' ,
} ) ;
} ,
onError : ( ) = > {
notifications . show ( {
color : 'red' ,
message : 'Failed to update source' ,
} ) ;
} ,
} ,
) ;
} ) ( ) ;
2025-12-02 00:12:46 +00:00
} , [
handleSubmit ,
updateSource ,
onSave ,
clearErrors ,
handleError ,
sourceFormSchema ,
] ) ;
2024-11-12 12:53:15 +00:00
2025-12-23 16:34:48 +00:00
const databaseName = useWatch ( {
control ,
name : 'from.databaseName' ,
2025-12-29 21:29:05 +00:00
defaultValue : source?.from?.databaseName || DEFAULT_DATABASE ,
} ) ;
const connectionId = useWatch ( {
control ,
name : 'connection' ,
defaultValue : source?.connection ,
2025-12-23 16:34:48 +00:00
} ) ;
2025-06-25 16:41:42 +00:00
2026-01-21 16:07:30 +00:00
const {
fields : querySettingFields ,
append : appendSetting ,
remove : removeSetting ,
} = useFieldArray ( { control , name : 'querySettings' } ) ;
2024-11-12 12:53:15 +00:00
return (
< div
style = {
{
// maxWidth: 700
}
}
>
< Stack gap = "md" mb = "md" >
2026-01-07 14:02:36 +00:00
< Text mb = "lg" > Source Settings < / Text >
2024-11-12 12:53:15 +00:00
< FormRow label = { 'Name' } >
< InputControlled
control = { control }
name = "name"
rules = { { required : 'Name is required' } }
/ >
< / FormRow >
< FormRow label = { 'Source Data Type' } >
< Controller
control = { control }
name = "kind"
render = { ( { field : { onChange , value } } ) = > (
< Radio.Group
value = { value }
2025-02-04 02:00:40 +00:00
onChange = { v = > onChange ( v ) }
2024-11-12 12:53:15 +00:00
withAsterisk
>
< Group >
2025-02-04 02:00:40 +00:00
< Radio value = { SourceKind . Log } label = "Log" / >
< Radio value = { SourceKind . Trace } label = "Trace" / >
2025-02-11 19:59:12 +00:00
{ IS_METRICS_ENABLED && (
2025-02-12 19:04:50 +00:00
< Radio value = { SourceKind . Metric } label = "OTEL Metrics" / >
2025-02-11 19:59:12 +00:00
) }
2025-02-04 02:00:40 +00:00
{ IS_SESSIONS_ENABLED && (
< Radio value = { SourceKind . Session } label = "Session" / >
) }
2024-11-12 12:53:15 +00:00
< / Group >
< / Radio.Group >
) }
/ >
< / FormRow >
2025-06-25 16:41:42 +00:00
< FormRow label = { 'Server Connection' } >
< ConnectionSelectControlled control = { control } name = { ` connection ` } / >
< / FormRow >
< FormRow label = { 'Database' } >
< DatabaseSelectControlled
control = { control }
name = { ` from.databaseName ` }
connectionId = { connectionId }
/ >
< / FormRow >
{ kind !== SourceKind . Metric && (
< FormRow label = { 'Table' } >
< DBTableSelectControlled
database = { databaseName }
control = { control }
name = { ` from.tableName ` }
connectionId = { connectionId }
rules = { { required : 'Table is required' } }
/ >
< / FormRow >
) }
2026-01-21 16:07:30 +00:00
< FormRow
label = {
< Anchor
href = "https://clickhouse.com/docs/operations/settings/settings"
size = "sm"
target = "_blank"
>
Query Settings
< / Anchor >
}
helpText = "Query-level Session Settings that will be added to each query for this source."
>
< Grid columns = { 11 } >
{ querySettingFields . map ( ( field , index ) = > (
< Fragment key = { field . id } >
< Grid.Col span = { 5 } pe = { 0 } >
< InputControlled
placeholder = "Setting"
control = { control }
name = { ` querySettings. ${ index } .setting ` }
/ >
< / Grid.Col >
< Grid.Col span = { 5 } pe = { 0 } >
< InputControlled
placeholder = "Value"
control = { control }
name = { ` querySettings. ${ index } .value ` }
/ >
< / Grid.Col >
< Grid.Col span = { 1 } ps = { 0 } >
< Flex align = "center" justify = "center" gap = "sm" h = "100%" >
< ActionIcon
variant = "subtle"
color = "gray"
title = "Remove setting"
onClick = { ( ) = > removeSetting ( index ) }
>
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Flex >
< / Grid.Col >
< / Fragment >
) ) }
< / Grid >
< Button
variant = "secondary"
size = "sm"
color = "gray"
mt = "md"
disabled = { querySettingFields . length >= 10 }
onClick = { ( ) = > {
if ( querySettingFields . length < 10 ) {
appendSetting ( { setting : '' , value : '' } ) ;
}
} }
>
< IconCirclePlus size = { 14 } className = "me-2" / >
Add Setting
< / Button >
< / FormRow >
2024-11-12 12:53:15 +00:00
< / Stack >
2025-12-23 16:34:48 +00:00
< TableModelForm control = { control } setValue = { setValue } kind = { kind } / >
2026-01-07 14:02:36 +00:00
< Group justify = "flex-end" mt = "lg" >
{ onCancel && (
< Button variant = "secondary" onClick = { onCancel } size = "xs" >
Cancel
< / Button >
) }
{ isNew ? (
< Button
variant = "primary"
onClick = { _onCreate }
size = "xs"
loading = { createSource . isPending }
>
Save New Source
< / Button >
) : (
< >
< ConfirmDeleteMenu
onDelete = { ( ) = > deleteSource . mutate ( { id : sourceId ? ? '' } ) }
/ >
< Button
variant = "primary"
onClick = { _onSave }
size = "xs"
loading = { createSource . isPending }
>
Save Source
< / Button >
< / >
) }
< / Group >
2024-11-12 12:53:15 +00:00
< / div >
) ;
}