diff --git a/src/components/configEditor/TracesConfig.test.tsx b/src/components/configEditor/TracesConfig.test.tsx index b3d44e20..8afb5224 100644 --- a/src/components/configEditor/TracesConfig.test.tsx +++ b/src/components/configEditor/TracesConfig.test.tsx @@ -1,31 +1,43 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { TracesConfig } from './TracesConfig'; +import { TracesConfig, TraceConfigProps } from './TracesConfig'; import allLabels from 'labels'; import { columnLabelToPlaceholder } from 'data/utils'; import { defaultTraceTable } from 'otel'; +function defaultTraceConfigProps(): TraceConfigProps { + return { + tracesConfig: {}, + onDefaultDatabaseChange: () => {}, + onDefaultTableChange: () => {}, + onOtelEnabledChange: () => {}, + onOtelVersionChange: () => {}, + onTraceIdColumnChange: () => {}, + onSpanIdColumnChange: () => {}, + onOperationNameColumnChange: () => {}, + onParentSpanIdColumnChange: () => {}, + onServiceNameColumnChange: () => {}, + onDurationColumnChange: () => {}, + onDurationUnitChange: () => {}, + onStartTimeColumnChange: () => {}, + onTagsColumnChange: () => {}, + onServiceTagsColumnChange: () => {}, + onKindColumnChange: () => {}, + onStatusCodeColumnChange: () => {}, + onStatusMessageColumnChange: () => {}, + onStateColumnChange: () => {}, + onInstrumentationLibraryNameColumnChange: () => {}, + onInstrumentationLibraryVersionColumnChange: () => {}, + onFlattenNestedChange: () => {}, + onEventsColumnPrefixChange: () => {}, + onLinksColumnPrefixChange: () => {} + }; +} + describe('TracesConfig', () => { it('should render', () => { const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); }); @@ -33,24 +45,7 @@ describe('TracesConfig', () => { it('should call onDefaultDatabase when changed', () => { const onDefaultDatabaseChange = jest.fn(); const result = render( - {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -65,24 +60,7 @@ describe('TracesConfig', () => { it('should call onDefaultTable when changed', () => { const onDefaultTableChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={onDefaultTableChange} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -94,31 +72,14 @@ describe('TracesConfig', () => { expect(onDefaultTableChange).toHaveBeenCalledWith('changed'); }); - it('should call onOtelEnabled when changed', () => { + it('should call onOtelEnabled when changed', async () => { const onOtelEnabledChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={onOtelEnabledChange} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); - const input = result.getByRole('checkbox'); + const input = (await result.findAllByRole('checkbox'))[0]; expect(input).toBeInTheDocument(); fireEvent.click(input); expect(onOtelEnabledChange).toHaveBeenCalledTimes(1); @@ -129,22 +90,9 @@ describe('TracesConfig', () => { const onOtelVersionChange = jest.fn(); const result = render( {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} onOtelVersionChange={onOtelVersionChange} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} /> ); expect(result.container.firstChild).not.toBeNull(); @@ -160,24 +108,7 @@ describe('TracesConfig', () => { it('should call onTraceIdColumnChange when changed', () => { const onTraceIdColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={onTraceIdColumnChange} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -192,24 +123,7 @@ describe('TracesConfig', () => { it('should call onSpanIdColumnChange when changed', () => { const onSpanIdColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={onSpanIdColumnChange} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -224,24 +138,7 @@ describe('TracesConfig', () => { it('should call onOperationNameColumnChange when changed', () => { const onOperationNameColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={onOperationNameColumnChange} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -256,24 +153,7 @@ describe('TracesConfig', () => { it('should call onParentSpanIdColumnChange when changed', () => { const onParentSpanIdColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={onParentSpanIdColumnChange} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -288,24 +168,7 @@ describe('TracesConfig', () => { it('should call onServiceNameColumnChange when changed', () => { const onServiceNameColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={onServiceNameColumnChange} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -320,24 +183,7 @@ describe('TracesConfig', () => { it('should call onDurationColumnChange when changed', () => { const onDurationColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={onDurationColumnChange} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -352,24 +198,7 @@ describe('TracesConfig', () => { it('should call onDurationUnitChange when changed', () => { const onDurationUnitChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={onDurationUnitChange} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -384,24 +213,7 @@ describe('TracesConfig', () => { it('should call onStartTimeColumnChange when changed', () => { const onStartTimeColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={onStartTimeColumnChange} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -416,24 +228,7 @@ describe('TracesConfig', () => { it('should call onTagsColumnChange when changed', () => { const onTagsColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={onTagsColumnChange} - onServiceTagsColumnChange={() => {}} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -448,24 +243,7 @@ describe('TracesConfig', () => { it('should call onServiceTagsColumnChange when changed', () => { const onServiceTagsColumnChange = jest.fn(); const result = render( - {}} - onDefaultTableChange={() => {}} - onOtelEnabledChange={() => {}} - onOtelVersionChange={() => {}} - onTraceIdColumnChange={() => {}} - onSpanIdColumnChange={() => {}} - onOperationNameColumnChange={() => {}} - onParentSpanIdColumnChange={() => {}} - onServiceNameColumnChange={() => {}} - onDurationColumnChange={() => {}} - onDurationUnitChange={() => {}} - onStartTimeColumnChange={() => {}} - onTagsColumnChange={() => {}} - onServiceTagsColumnChange={onServiceTagsColumnChange} - onEventsColumnPrefixChange={() => {}} - /> + ); expect(result.container.firstChild).not.toBeNull(); @@ -476,4 +254,138 @@ describe('TracesConfig', () => { expect(onServiceTagsColumnChange).toHaveBeenCalledTimes(1); expect(onServiceTagsColumnChange).toHaveBeenCalledWith('changed'); }); + + it('should call onKindColumnChange when changed', () => { + const onKindColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.kind.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onKindColumnChange).toHaveBeenCalledTimes(1); + expect(onKindColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onStatusCodeColumnChange when changed', () => { + const onStatusCodeColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.statusCode.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onStatusCodeColumnChange).toHaveBeenCalledTimes(1); + expect(onStatusCodeColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onStatusMessageColumnChange when changed', () => { + const onStatusMessageColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.statusMessage.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onStatusMessageColumnChange).toHaveBeenCalledTimes(1); + expect(onStatusMessageColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onStateColumnChange when changed', () => { + const onStateColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.state.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onStateColumnChange).toHaveBeenCalledTimes(1); + expect(onStateColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onInstrumentationLibraryNameColumnChange when changed', () => { + const onInstrumentationLibraryNameColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.instrumentationLibraryName.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onInstrumentationLibraryNameColumnChange).toHaveBeenCalledTimes(1); + expect(onInstrumentationLibraryNameColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onInstrumentationLibraryVersionColumnChange when changed', () => { + const onInstrumentationLibraryVersionColumnChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.instrumentationLibraryVersion.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onInstrumentationLibraryVersionColumnChange).toHaveBeenCalledTimes(1); + expect(onInstrumentationLibraryVersionColumnChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onFlattenNestedChange when changed', async () => { + const onFlattenNestedChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = (await result.findAllByRole('checkbox'))[1]; + expect(input).toBeInTheDocument(); + fireEvent.click(input); + expect(onFlattenNestedChange).toHaveBeenCalledTimes(1); + expect(onFlattenNestedChange).toHaveBeenCalledWith(true); + }); + + it('should call onEventsColumnPrefixChange when changed', () => { + const onEventsColumnPrefixChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.eventsPrefix.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onEventsColumnPrefixChange).toHaveBeenCalledTimes(1); + expect(onEventsColumnPrefixChange).toHaveBeenCalledWith('changed'); + }); + + it('should call onLinksColumnPrefixChange when changed', () => { + const onLinksColumnPrefixChange = jest.fn(); + const result = render( + + ); + expect(result.container.firstChild).not.toBeNull(); + + const input = result.getByPlaceholderText(columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.linksPrefix.label)); + expect(input).toBeInTheDocument(); + fireEvent.change(input, { target: { value: 'changed' } }); + fireEvent.blur(input); + expect(onLinksColumnPrefixChange).toHaveBeenCalledTimes(1); + expect(onLinksColumnPrefixChange).toHaveBeenCalledWith('changed'); + }); }); diff --git a/src/components/configEditor/TracesConfig.tsx b/src/components/configEditor/TracesConfig.tsx index 5849c1e3..f4d05b03 100644 --- a/src/components/configEditor/TracesConfig.tsx +++ b/src/components/configEditor/TracesConfig.tsx @@ -10,8 +10,9 @@ import { DurationUnitSelect } from 'components/queryBuilder/DurationUnitSelect'; import { CHTracesConfig } from 'types/config'; import allLabels from 'labels'; import { columnLabelToPlaceholder } from 'data/utils'; +import { Switch } from 'components/queryBuilder/Switch'; -interface TraceConfigProps { +export interface TraceConfigProps { tracesConfig?: CHTracesConfig; onDefaultDatabaseChange: (v: string) => void; onDefaultTableChange: (v: string) => void; @@ -27,7 +28,15 @@ interface TraceConfigProps { onStartTimeColumnChange: (v: string) => void; onTagsColumnChange: (v: string) => void; onServiceTagsColumnChange: (v: string) => void; + onKindColumnChange: (v: string) => void; + onStatusCodeColumnChange: (v: string) => void; + onStatusMessageColumnChange: (v: string) => void; + onStateColumnChange: (v: string) => void; + onInstrumentationLibraryNameColumnChange: (v: string) => void; + onInstrumentationLibraryVersionColumnChange: (v: string) => void; + onFlattenNestedChange: (v: boolean) => void; onEventsColumnPrefixChange: (v: string) => void; + onLinksColumnPrefixChange: (v: string) => void; } export const TracesConfig = (props: TraceConfigProps) => { @@ -36,13 +45,20 @@ export const TracesConfig = (props: TraceConfigProps) => { onOtelEnabledChange, onOtelVersionChange, onTraceIdColumnChange, onSpanIdColumnChange, onOperationNameColumnChange, onParentSpanIdColumnChange, onServiceNameColumnChange, onDurationColumnChange, onDurationUnitChange, onStartTimeColumnChange, - onTagsColumnChange, onServiceTagsColumnChange, onEventsColumnPrefixChange + onTagsColumnChange, onServiceTagsColumnChange, + onKindColumnChange, onStatusCodeColumnChange, onStatusMessageColumnChange, + onStateColumnChange, + onInstrumentationLibraryNameColumnChange, onInstrumentationLibraryVersionColumnChange, + onFlattenNestedChange, onEventsColumnPrefixChange, onLinksColumnPrefixChange, } = props; let { defaultDatabase, defaultTable, otelEnabled, otelVersion, traceIdColumn, spanIdColumn, operationNameColumn, parentSpanIdColumn, serviceNameColumn, - durationColumn, durationUnit, startTimeColumn, tagsColumn, serviceTagsColumn, eventsColumnPrefix + durationColumn, durationUnit, startTimeColumn, tagsColumn, serviceTagsColumn, + kindColumn, statusCodeColumn, statusMessageColumn, stateColumn, + instrumentationLibraryNameColumn, instrumentationLibraryVersionColumn, + flattenNested, traceEventsColumnPrefix, traceLinksColumnPrefix, } = (props.tracesConfig || {}) as CHTracesConfig; const labels = allLabels.components.Config.TracesConfig; @@ -57,8 +73,16 @@ export const TracesConfig = (props: TraceConfigProps) => { durationColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceDurationTime); tagsColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceTags); serviceTagsColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceServiceTags); - eventsColumnPrefix = otelConfig.traceColumnMap.get(ColumnHint.TraceEventsPrefix); + kindColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceKind); + statusCodeColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceStatusCode); + statusMessageColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceStatusMessage); + stateColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceState); + instrumentationLibraryNameColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceInstrumentationLibraryName); + instrumentationLibraryVersionColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceInstrumentationLibraryVersion); durationUnit = otelConfig.traceDurationUnit.toString(); + flattenNested = otelConfig.flattenNested; + traceEventsColumnPrefix = otelConfig.traceEventsColumnPrefix; + traceLinksColumnPrefix = otelConfig.traceLinksColumnPrefix; } return ( @@ -183,14 +207,78 @@ export const TracesConfig = (props: TraceConfigProps) => { value={serviceTagsColumn || ''} onChange={onServiceTagsColumnChange} /> + + + + + + + + ); diff --git a/src/components/queryBuilder/Switch.tsx b/src/components/queryBuilder/Switch.tsx index 076b2454..4fca682e 100644 --- a/src/components/queryBuilder/Switch.tsx +++ b/src/components/queryBuilder/Switch.tsx @@ -7,12 +7,13 @@ interface SwitchProps { onChange: (value: boolean) => void; label: string; tooltip: string; + disabled?: boolean; inline?: boolean; wide?: boolean; } export const Switch = (props: SwitchProps) => { - const { value, onChange, label, tooltip, inline, wide } = props; + const { value, onChange, label, tooltip, disabled, inline, wide } = props; const theme = useTheme(); const switchContainerStyle: React.CSSProperties = { @@ -31,6 +32,7 @@ export const Switch = (props: SwitchProps) => {
onChange(e.currentTarget.checked)} diff --git a/src/components/queryBuilder/views/TraceQueryBuilder.tsx b/src/components/queryBuilder/views/TraceQueryBuilder.tsx index 6593ea73..3513fede 100644 --- a/src/components/queryBuilder/views/TraceQueryBuilder.tsx +++ b/src/components/queryBuilder/views/TraceQueryBuilder.tsx @@ -5,7 +5,7 @@ import { FiltersEditor } from '../FilterEditor'; import allLabels from 'labels'; import { ModeSwitch } from '../ModeSwitch'; import { getColumnByHint } from 'data/sqlGenerator'; -import { Alert, Collapse, VerticalGroup } from '@grafana/ui'; +import { Alert, Collapse, Stack } from '@grafana/ui'; import { DurationUnitSelect } from 'components/queryBuilder/DurationUnitSelect'; import { Datasource } from 'data/CHDatasource'; import { useBuilderOptionChanges } from 'hooks/useBuilderOptionChanges'; @@ -18,6 +18,7 @@ import TraceIdInput from '../TraceIdInput'; import { OrderByEditor, getOrderByOptions } from '../OrderByEditor'; import { LimitEditor } from '../LimitEditor'; import { LabeledInput } from 'components/configEditor/LabeledInput'; +import { Switch } from '../Switch'; interface TraceQueryBuilderProps { datasource: Datasource; @@ -39,7 +40,15 @@ interface TraceQueryBuilderState { durationUnit: TimeUnit; tagsColumn?: SelectedColumn; serviceTagsColumn?: SelectedColumn; - eventsColumnPrefix?: SelectedColumn; + kindColumn?: SelectedColumn; + statusCodeColumn?: SelectedColumn; + statusMessageColumn?: SelectedColumn; + stateColumn?: SelectedColumn; + instrumentationLibraryNameColumn?: SelectedColumn; + instrumentationLibraryVersionColumn?: SelectedColumn; + flattenNested?: boolean; + traceEventsColumnPrefix?: string; + traceLinksColumnPrefix?: string; traceId: string; orderBy: OrderBy[]; limit: number; @@ -54,7 +63,7 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { const [isColumnsOpen, setColumnsOpen] = useState(showConfigWarning); // Toggle Columns collapse section const [isFiltersOpen, setFiltersOpen] = useState(!(builderOptions.meta?.isTraceIdMode && builderOptions.meta.traceId)); // Toggle Filters collapse section const labels = allLabels.components.TraceQueryBuilder; - const builderState: TraceQueryBuilderState = useMemo(() => ({ + const builderState = useMemo(() => ({ isTraceIdMode: builderOptions.meta?.isTraceIdMode || false, otelEnabled: builderOptions.meta?.otelEnabled || false, otelVersion: builderOptions.meta?.otelVersion || '', @@ -68,7 +77,15 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { durationUnit: builderOptions.meta?.traceDurationUnit || TimeUnit.Nanoseconds, tagsColumn: getColumnByHint(builderOptions, ColumnHint.TraceTags), serviceTagsColumn: getColumnByHint(builderOptions, ColumnHint.TraceServiceTags), - eventsColumnPrefix: getColumnByHint(builderOptions, ColumnHint.TraceEventsPrefix), + kindColumn: getColumnByHint(builderOptions, ColumnHint.TraceKind), + statusCodeColumn: getColumnByHint(builderOptions, ColumnHint.TraceStatusCode), + statusMessageColumn: getColumnByHint(builderOptions, ColumnHint.TraceStatusMessage), + stateColumn: getColumnByHint(builderOptions, ColumnHint.TraceState), + instrumentationLibraryNameColumn: getColumnByHint(builderOptions, ColumnHint.TraceInstrumentationLibraryName), + instrumentationLibraryVersionColumn: getColumnByHint(builderOptions, ColumnHint.TraceInstrumentationLibraryVersion), + flattenNested: Boolean(builderOptions.meta?.flattenNested), + traceEventsColumnPrefix: builderOptions.meta?.traceEventsColumnPrefix || '', + traceLinksColumnPrefix: builderOptions.meta?.traceLinksColumnPrefix || '', traceId: builderOptions.meta?.traceId || '', orderBy: builderOptions.orderBy || [], limit: builderOptions.limit || 0, @@ -86,7 +103,13 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { next.durationTimeColumn, next.tagsColumn, next.serviceTagsColumn, - next.eventsColumnPrefix + next.serviceTagsColumn, + next.kindColumn, + next.statusCodeColumn, + next.statusMessageColumn, + next.stateColumn, + next.instrumentationLibraryNameColumn, + next.instrumentationLibraryVersionColumn, ].filter(c => c !== undefined) as SelectedColumn[]; builderOptionsDispatch(setOptions({ @@ -98,6 +121,9 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { isTraceIdMode: next.isTraceIdMode, traceDurationUnit: next.durationUnit, traceId: next.traceId, + flattenNested: next.flattenNested, + traceEventsColumnPrefix: next.traceEventsColumnPrefix, + traceLinksColumnPrefix: next.traceLinksColumnPrefix, } })); }, builderState); @@ -108,12 +134,12 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { const configWarning = showConfigWarning && ( setConfigWarningOpen(false)}> - +
{'To speed up your query building, enter your default trace configuration in your '} ClickHouse Data Source settings
-
+
); @@ -261,12 +287,106 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { />
- + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
diff --git a/src/components/queryBuilder/views/traceQueryBuilderHooks.test.ts b/src/components/queryBuilder/views/traceQueryBuilderHooks.test.ts index 63b7efaf..6b87b57f 100644 --- a/src/components/queryBuilder/views/traceQueryBuilderHooks.test.ts +++ b/src/components/queryBuilder/views/traceQueryBuilderHooks.test.ts @@ -20,7 +20,10 @@ describe('useTraceDefaultsOnMount', () => { meta: { otelEnabled: expect.anything(), otelVersion: undefined, - traceDurationUnit: expect.anything() + traceDurationUnit: expect.anything(), + flattenNested: expect.anything(), + traceEventsColumnPrefix: expect.anything(), + traceLinksColumnPrefix: expect.anything(), } }; @@ -77,7 +80,10 @@ describe('useOtelColumns', () => { const expectedOptions = { columns, meta: { - traceDurationUnit: expect.anything() + traceDurationUnit: expect.anything(), + flattenNested: expect.anything(), + traceEventsColumnPrefix: expect.anything(), + traceLinksColumnPrefix: expect.anything(), } }; diff --git a/src/components/queryBuilder/views/traceQueryBuilderHooks.ts b/src/components/queryBuilder/views/traceQueryBuilderHooks.ts index f8747839..fecd3f14 100644 --- a/src/components/queryBuilder/views/traceQueryBuilderHooks.ts +++ b/src/components/queryBuilder/views/traceQueryBuilderHooks.ts @@ -19,6 +19,9 @@ export const useTraceDefaultsOnMount = (datasource: Datasource, isNewQuery: bool const defaultDurationUnit = datasource.getDefaultTraceDurationUnit(); const otelVersion = datasource.getTraceOtelVersion(); const defaultColumns = datasource.getDefaultTraceColumns(); + const defaultFlattenNested = datasource.getDefaultTraceFlattenNested(); + const defaultEventsColumnPrefix = datasource.getDefaultTraceEventsColumnPrefix(); + const defaultLinksColumnPrefix = datasource.getDefaultTraceLinksColumnPrefix(); const nextColumns: SelectedColumn[] = []; for (let [hint, colName] of defaultColumns) { @@ -32,7 +35,10 @@ export const useTraceDefaultsOnMount = (datasource: Datasource, isNewQuery: bool meta: { otelEnabled: Boolean(otelVersion), otelVersion, - traceDurationUnit: defaultDurationUnit + traceDurationUnit: defaultDurationUnit, + flattenNested: defaultFlattenNested, + traceEventsColumnPrefix: defaultEventsColumnPrefix, + traceLinksColumnPrefix: defaultLinksColumnPrefix, } })); didSetDefaults.current = true; @@ -68,7 +74,10 @@ export const useOtelColumns = (otelEnabled: boolean, otelVersion: string, builde builderOptionsDispatch(setOptions({ columns, meta: { - traceDurationUnit: otelConfig.traceDurationUnit + traceDurationUnit: otelConfig.traceDurationUnit, + flattenNested: otelConfig.flattenNested, + traceEventsColumnPrefix: otelConfig.traceEventsColumnPrefix, + traceLinksColumnPrefix: otelConfig.traceLinksColumnPrefix, } })); didSetColumns.current = true; diff --git a/src/data/CHDatasource.ts b/src/data/CHDatasource.ts index 0ae9f71d..33f58054 100644 --- a/src/data/CHDatasource.ts +++ b/src/data/CHDatasource.ts @@ -493,7 +493,12 @@ export class Datasource traceConfig.startTimeColumn && result.set(ColumnHint.Time, traceConfig.startTimeColumn); traceConfig.tagsColumn && result.set(ColumnHint.TraceTags, traceConfig.tagsColumn); traceConfig.serviceTagsColumn && result.set(ColumnHint.TraceServiceTags, traceConfig.serviceTagsColumn); - traceConfig.eventsColumnPrefix && result.set(ColumnHint.TraceEventsPrefix, traceConfig.eventsColumnPrefix); + traceConfig.kindColumn && result.set(ColumnHint.TraceKind, traceConfig.kindColumn); + traceConfig.statusCodeColumn && result.set(ColumnHint.TraceStatusCode, traceConfig.statusCodeColumn); + traceConfig.statusMessageColumn && result.set(ColumnHint.TraceStatusMessage, traceConfig.statusMessageColumn); + traceConfig.instrumentationLibraryNameColumn && result.set(ColumnHint.TraceInstrumentationLibraryName, traceConfig.instrumentationLibraryNameColumn); + traceConfig.instrumentationLibraryVersionColumn && result.set(ColumnHint.TraceInstrumentationLibraryVersion, traceConfig.instrumentationLibraryVersionColumn); + traceConfig.stateColumn && result.set(ColumnHint.TraceState, traceConfig.stateColumn); return result; } @@ -510,6 +515,18 @@ export class Datasource return this.settings.jsonData.traces?.durationUnit as TimeUnit || TimeUnit.Nanoseconds; } + getDefaultTraceFlattenNested(): boolean { + return this.settings.jsonData.traces?.flattenNested || false; + } + + getDefaultTraceEventsColumnPrefix(): string { + return this.settings.jsonData.traces?.traceEventsColumnPrefix || 'Events'; + } + + getDefaultTraceLinksColumnPrefix(): string { + return this.settings.jsonData.traces?.traceLinksColumnPrefix || 'Links'; + } + async fetchDatabases(): Promise { return this.fetchData('SHOW DATABASES'); } @@ -584,7 +601,7 @@ export class Datasource return columns; } - + /** * Fetches column suggestions from the table schema. */ diff --git a/src/data/sqlGenerator.test.ts b/src/data/sqlGenerator.test.ts index fe27fcb3..d686808c 100644 --- a/src/data/sqlGenerator.test.ts +++ b/src/data/sqlGenerator.test.ts @@ -8,9 +8,9 @@ describe('SQL Generator', () => { table: 'sample', queryType: QueryType.Table, columns: [ - { name: 'a', type: 'UInt64' }, - { name: 'b', type: 'String' }, - { name: 'c', type: 'String' }, + { name: 'a', type: 'UInt64' }, + { name: 'b', type: 'String' }, + { name: 'c', type: 'String' }, ], limit: 1000, filters: [ @@ -41,9 +41,9 @@ describe('SQL Generator', () => { queryType: QueryType.Table, mode: BuilderMode.Aggregate, columns: [ - { name: 'a', type: 'DateTime' }, - { name: 'b', type: 'String' }, - { name: 'c', type: 'String' }, + { name: 'a', type: 'DateTime' }, + { name: 'b', type: 'String' }, + { name: 'c', type: 'String' }, ], aggregates: [ { aggregateType: AggregateType.Count, column: '*', alias: 'd' } @@ -77,9 +77,9 @@ describe('SQL Generator', () => { table: 'logs', queryType: QueryType.Logs, columns: [ - { name: 'log_ts', type: 'DateTime', hint: ColumnHint.Time }, - { name: 'log_level', type: 'String', hint: ColumnHint.LogLevel }, - { name: 'log_body', type: 'String', hint: ColumnHint.LogMessage }, + { name: 'log_ts', type: 'DateTime', hint: ColumnHint.Time }, + { name: 'log_level', type: 'String', hint: ColumnHint.LogLevel }, + { name: 'log_body', type: 'String', hint: ColumnHint.LogMessage }, ], limit: 1000, filters: [ @@ -122,8 +122,8 @@ describe('SQL Generator', () => { table: 'time_data', queryType: QueryType.TimeSeries, columns: [ - { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time }, - { name: 'number_field', type: 'UInt64' }, + { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time }, + { name: 'number_field', type: 'UInt64' }, ], limit: 100, filters: [ @@ -154,8 +154,8 @@ describe('SQL Generator', () => { table: 'time_data', queryType: QueryType.TimeSeries, columns: [ - { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time }, - { name: 'number_field', type: 'UInt64' }, + { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time }, + { name: 'number_field', type: 'UInt64' }, ], limit: 100, aggregates: [{ aggregateType: AggregateType.Sum, column: 'number_field', alias: 'total' }], @@ -226,6 +226,132 @@ describe('SQL Generator', () => { expect(sql).toEqual(expectedSqlParts.join(' ')); }); + it('generates trace ID query with additional fields, flatten nested disabled', () => { + const opts: QueryBuilderOptions = { + database: 'default', + table: 'otel_traces', + queryType: QueryType.Traces, + columns: [ + { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId }, + { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId }, + { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId }, + { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName }, + { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName }, + { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time }, + { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime }, + { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags }, + { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags }, + { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode }, + { name: 'Kind', type: 'String', hint: ColumnHint.TraceKind }, + { name: 'StatusMessage', type: 'String', hint: ColumnHint.TraceStatusMessage }, + { name: 'InstrumentationLibraryName', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryName }, + { name: 'InstrumentationLibraryVersion', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryVersion }, + { name: 'TraceState', type: 'String', hint: ColumnHint.TraceState }, + ], + filters: [], + meta: { + minimized: true, + otelEnabled: true, + otelVersion: 'latest', + traceDurationUnit: TimeUnit.Nanoseconds, + isTraceIdMode: true, + traceId: 'abcdefg', + flattenNested: false, + traceEventsColumnPrefix: 'Events', + traceLinksColumnPrefix: 'Links', + }, + limit: 1000, + orderBy: [] + }; + + const expectedSqlParts = [ + `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM "default"."otel_traces_trace_id_ts" WHERE TraceId = trace_id) as trace_start,`, + `(SELECT max(End) + 1 FROM "default"."otel_traces_trace_id_ts" WHERE TraceId = trace_id) as trace_end`, + 'SELECT "TraceId" as traceID, "SpanId" as spanID, "ParentSpanId" as parentSpanID,', + '"ServiceName" as serviceName, "SpanName" as operationName, multiply(toUnixTimestamp64Nano("Timestamp"), 0.000001) as startTime,', + 'multiply("Duration", 0.000001) as duration,', + `arrayMap(key -> map('key', key, 'value',"SpanAttributes"[key]),`, + `mapKeys("SpanAttributes")) as tags,`, + `arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags,`, + `if("StatusCode" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode,`, + `arrayMap((name, timestamp, attributes) -> tuple(name, toString(toUnixTimestamp64Milli(timestamp)), arrayMap( key -> map('key', key, 'value', attributes[key]), mapKeys(attributes)))::Tuple(name String, timestamp String, fields Array(Map(String, String))), "Events".Name, "Events".Timestamp, "Events".Attributes) AS logs,`, + `arrayMap((traceID, spanID, attributes) -> tuple(traceID, spanID, arrayMap(key -> map('key', key, 'value', attributes[key]), mapKeys(attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))), "Links".TraceId, "Links".SpanId, "Links".Attributes) AS references,`, + '"Kind" as kind,', + '"StatusMessage" as statusMessage,', + '"InstrumentationLibraryName" as instrumentationLibraryName,', + '"InstrumentationLibraryVersion" as instrumentationLibraryVersion,', + '"TraceState" as traceState', + `FROM "default"."otel_traces" WHERE traceID = trace_id AND "Timestamp" >= trace_start AND "Timestamp" <= trace_end`, + 'LIMIT 1000' + ]; + + const sql = generateSql(opts); + expect(sql).toEqual(expectedSqlParts.join(' ')); + }); + + it('generates trace ID query with additional fields, flatten nested enabled', () => { + const opts: QueryBuilderOptions = { + database: 'default', + table: 'otel_traces', + queryType: QueryType.Traces, + columns: [ + { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId }, + { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId }, + { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId }, + { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName }, + { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName }, + { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time }, + { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime }, + { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags }, + { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags }, + { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode }, + { name: 'Kind', type: 'String', hint: ColumnHint.TraceKind }, + { name: 'StatusMessage', type: 'String', hint: ColumnHint.TraceStatusMessage }, + { name: 'InstrumentationLibraryName', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryName }, + { name: 'InstrumentationLibraryVersion', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryVersion }, + { name: 'TraceState', type: 'String', hint: ColumnHint.TraceState }, + ], + filters: [], + meta: { + minimized: true, + otelEnabled: true, + otelVersion: 'latest', + traceDurationUnit: TimeUnit.Nanoseconds, + isTraceIdMode: true, + traceId: 'abcdefg', + flattenNested: true, + traceEventsColumnPrefix: 'Events', + traceLinksColumnPrefix: 'Links', + }, + limit: 1000, + orderBy: [] + }; + + const expectedSqlParts = [ + `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM "default"."otel_traces_trace_id_ts" WHERE TraceId = trace_id) as trace_start,`, + `(SELECT max(End) + 1 FROM "default"."otel_traces_trace_id_ts" WHERE TraceId = trace_id) as trace_end`, + 'SELECT "TraceId" as traceID, "SpanId" as spanID, "ParentSpanId" as parentSpanID,', + '"ServiceName" as serviceName, "SpanName" as operationName, multiply(toUnixTimestamp64Nano("Timestamp"), 0.000001) as startTime,', + 'multiply("Duration", 0.000001) as duration,', + `arrayMap(key -> map('key', key, 'value',"SpanAttributes"[key]),`, + `mapKeys("SpanAttributes")) as tags,`, + `arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags,`, + `if("StatusCode" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode,`, + `arrayMap(event -> tuple(multiply(toFloat64(event.Timestamp), 1000), arrayConcat(arrayMap(key -> map('key', key, 'value', event.Attributes[key]), mapKeys(event.Attributes)), [map('key', 'message', 'value', event.Name)]))::Tuple(timestamp Float64, fields Array(Map(String, String))), "Events") as logs,`, + `arrayMap(link -> tuple(link.TraceId, link.SpanId, arrayMap(key -> map('key', key, 'value', link.Attributes[key]), mapKeys(link.Attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))), "Links") AS references,`, + '"Kind" as kind,', + '"StatusMessage" as statusMessage,', + '"InstrumentationLibraryName" as instrumentationLibraryName,', + '"InstrumentationLibraryVersion" as instrumentationLibraryVersion,', + '"TraceState" as traceState', + `FROM "default"."otel_traces" WHERE traceID = trace_id AND "Timestamp" >= trace_start AND "Timestamp" <= trace_end`, + 'LIMIT 1000' + ]; + + const sql = generateSql(opts); + expect(sql).toEqual(expectedSqlParts.join(' ')); + }); + it('generates trace ID query with OTel enabled', () => { const opts: QueryBuilderOptions = { database: 'default', diff --git a/src/data/sqlGenerator.ts b/src/data/sqlGenerator.ts index 19ba0324..6817419d 100644 --- a/src/data/sqlGenerator.ts +++ b/src/data/sqlGenerator.ts @@ -149,9 +149,70 @@ const generateTraceIdQuery = (options: QueryBuilderOptions): string => { if (traceStatusCode !== undefined) { selectParts.push(`if(${escapeIdentifier(traceStatusCode.name)} IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`); } - const traceEventsPrefix = getColumnByHint(options, ColumnHint.TraceEventsPrefix); - if (traceEventsPrefix !== undefined) { - selectParts.push(`arrayMap((name, timestamp, attributes) -> tuple(name, toString(toUnixTimestamp64Milli(timestamp)), arrayMap( key -> map('key', key, 'value', attributes[key]), mapKeys(attributes)))::Tuple(name String, timestamp String, fields Array(Map(String, String))),${escapeIdentifier(traceEventsPrefix.name)}.Name, ${escapeIdentifier(traceEventsPrefix.name)}.Timestamp, ${escapeIdentifier(traceEventsPrefix.name)}.Attributes) AS logs`); + + const flattenNested = Boolean(options.meta?.flattenNested); + + const traceEventsPrefix = options.meta?.traceEventsColumnPrefix || ''; + if (traceEventsPrefix !== '') { + if (flattenNested) { + selectParts.push([ + `arrayMap(event -> tuple(multiply(toFloat64(event.Timestamp), 1000),`, + `arrayConcat(arrayMap(key -> map('key', key, 'value', event.Attributes[key]),`, + `mapKeys(event.Attributes)), [map('key', 'message', 'value', event.Name)]))::Tuple(timestamp Float64, fields Array(Map(String, String))),`, + `${escapeIdentifier(traceEventsPrefix)}) as logs` + ].join(' ')); + } else { + selectParts.push([ + `arrayMap((name, timestamp, attributes) -> tuple(name, toString(toUnixTimestamp64Milli(timestamp)),`, + `arrayMap( key -> map('key', key, 'value', attributes[key]),`, + `mapKeys(attributes)))::Tuple(name String, timestamp String, fields Array(Map(String, String))),`, + `${escapeIdentifier(traceEventsPrefix)}.Name, ${escapeIdentifier(traceEventsPrefix)}.Timestamp,`, + `${escapeIdentifier(traceEventsPrefix)}.Attributes) AS logs` + ].join(' ')); + } + } + + const traceLinksPrefix = options.meta?.traceLinksColumnPrefix || ''; + if (traceLinksPrefix !== '') { + if (flattenNested) { + selectParts.push([ + `arrayMap(link -> tuple(link.TraceId, link.SpanId, arrayMap(key -> map('key', key, 'value', link.Attributes[key]),`, + `mapKeys(link.Attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))),`, + `${escapeIdentifier(traceLinksPrefix)}) AS references` + ].join(' ')); + } else { + selectParts.push([ + `arrayMap((traceID, spanID, attributes) -> tuple(traceID, spanID, arrayMap(key -> map('key', key, 'value', attributes[key]),`, + `mapKeys(attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))),`, + `${escapeIdentifier(traceLinksPrefix)}.TraceId, ${escapeIdentifier(traceLinksPrefix)}.SpanId,`, + `${escapeIdentifier(traceLinksPrefix)}.Attributes) AS references` + ].join(' ')); + } + } + + const traceKind = getColumnByHint(options, ColumnHint.TraceKind); + if (traceKind !== undefined) { + selectParts.push(`${escapeIdentifier(traceKind.name)} as kind`); + } + + const traceStatusMessage = getColumnByHint(options, ColumnHint.TraceStatusMessage); + if (traceStatusMessage !== undefined) { + selectParts.push(`${escapeIdentifier(traceStatusMessage.name)} as statusMessage`); + } + + const traceInstrumentationLibraryName = getColumnByHint(options, ColumnHint.TraceInstrumentationLibraryName); + if (traceInstrumentationLibraryName !== undefined) { + selectParts.push(`${escapeIdentifier(traceInstrumentationLibraryName.name)} as instrumentationLibraryName`); + } + + const traceInstrumentationLibraryVersion = getColumnByHint(options, ColumnHint.TraceInstrumentationLibraryVersion); + if (traceInstrumentationLibraryVersion !== undefined) { + selectParts.push(`${escapeIdentifier(traceInstrumentationLibraryVersion.name)} as instrumentationLibraryVersion`); + } + + const traceState = getColumnByHint(options, ColumnHint.TraceState); + if (traceState !== undefined) { + selectParts.push(`${escapeIdentifier(traceState.name)} as traceState`); } const selectPartsSql = selectParts.join(', '); @@ -694,7 +755,7 @@ const getFilters = (options: QueryBuilderOptions): string => { operator = ''; negate = true; } else if (filter.operator === FilterOperator.WithInGrafanaTimeRange) { - operator = ''; + operator = ''; } if (operator) { @@ -763,11 +824,11 @@ const getFilters = (options: QueryBuilderOptions): string => { }; const stripTypeModifiers = (type: string): string => { - return type.toLowerCase(). - replace(/\(/g, ''). - replace(/\)/g, ''). - replace(/nullable/g, ''). - replace(/lowcardinality/g, ''); + return type.toLowerCase(). + replace(/\(/g, ''). + replace(/\)/g, ''). + replace(/nullable/g, ''). + replace(/lowcardinality/g, ''); } const isBooleanType = (type: string): boolean => (type?.toLowerCase().startsWith('boolean')); @@ -778,7 +839,7 @@ const isDateType = (type: string): boolean => type?.toLowerCase().startsWith('da const isStringType = (type: string): boolean => { type = stripTypeModifiers(type.toLowerCase()); return (type === 'string' || type.startsWith('fixedstring')) - && !(isBooleanType(type) || isNumberType(type) || isDateType(type)); + && !(isBooleanType(type) || isNumberType(type) || isDateType(type)); } const isNullFilter = (operator: FilterOperator): boolean => operator === FilterOperator.IsNull || operator === FilterOperator.IsNotNull; const isBooleanFilter = (type: string): boolean => isBooleanType(type); diff --git a/src/labels.ts b/src/labels.ts index 65f71279..c557556c 100644 --- a/src/labels.ts +++ b/src/labels.ts @@ -204,9 +204,41 @@ export default { label: 'Service Tags column', tooltip: 'Column for the service tags' }, + flattenNested: { + label: 'Use Flatten Nested', + tooltip: 'Enable if your traces table was created with flatten_nested=1', + }, eventsPrefix: { - label: 'Events column', - tooltip: 'Prefix for the events column' + label: 'Events prefix', + tooltip: 'Prefix for the events column (Events.Timestamp, Events.Name, etc.)' + }, + linksPrefix: { + label: 'Links prefix', + tooltip: 'Prefix for the trace references column (Links.TraceId, Links.TraceState, etc.)' + }, + kind: { + label: 'Kind column', + tooltip: 'Column for the trace kind' + }, + statusCode: { + label: 'Status Code column', + tooltip: 'Column for the trace status code' + }, + statusMessage: { + label: 'Status Message column', + tooltip: 'Column for the trace status message' + }, + instrumentationLibraryName: { + label: 'Library Name column', + tooltip: 'Column for the instrumentation library name' + }, + instrumentationLibraryVersion: { + label: 'Library Version column', + tooltip: 'Column for the instrumentation library version' + }, + state: { + label: 'State column', + tooltip: 'Column for the trace state' } } }, @@ -425,14 +457,46 @@ export default { label: 'Service Tags Column', tooltip: 'Column that contains the service tags' }, - traceIdFilter: { - label: 'Trace ID', - tooltip: 'filter by a specific trace ID' + flattenNested: { + label: 'Use Flatten Nested', + tooltip: 'Enable if your traces table was created with flatten_nested=1', }, eventsPrefix: { label: 'Events Prefix', tooltip: 'Prefix for the events column' - } + }, + linksPrefix: { + label: 'Links Prefix', + tooltip: 'Prefix for the trace references column' + }, + kind: { + label: 'Kind Column', + tooltip: 'Column that contains the trace kind' + }, + statusCode: { + label: 'Status Code Column', + tooltip: 'Column that contains the trace status code' + }, + statusMessage: { + label: 'Status Message Column', + tooltip: 'Column that contains the trace status message' + }, + instrumentationLibraryName: { + label: 'Library Name Column', + tooltip: 'Column that contains the instrumentation library name (Optional)' + }, + instrumentationLibraryVersion: { + label: 'Library Version Column', + tooltip: 'Column that contains the instrumentation library version (Optional)' + }, + state: { + label: 'State Column', + tooltip: 'Column that contains the trace state' + }, + traceIdFilter: { + label: 'Trace ID', + tooltip: 'filter by a specific trace ID' + }, }, } }, @@ -463,7 +527,11 @@ export default { [ColumnHint.TraceTags]: 'Tags', [ColumnHint.TraceServiceTags]: 'Service Tags', [ColumnHint.TraceStatusCode]: 'Status Code', - [ColumnHint.TraceEventsPrefix]: 'Events Prefix', + [ColumnHint.TraceKind]: 'Kind', + [ColumnHint.TraceStatusMessage]: 'Status Message', + [ColumnHint.TraceInstrumentationLibraryName]: 'Instrumentation Library Name', + [ColumnHint.TraceInstrumentationLibraryVersion]: 'Instrumentation Library Version', + [ColumnHint.TraceState]: 'State', } } } diff --git a/src/otel.ts b/src/otel.ts index d757124c..dc0aa8ae 100644 --- a/src/otel.ts +++ b/src/otel.ts @@ -15,6 +15,9 @@ export interface OtelVersion { traceTable: string; traceColumnMap: Map; traceDurationUnit: TimeUnit.Nanoseconds; + flattenNested: boolean; + traceEventsColumnPrefix: string; + traceLinksColumnPrefix: string; } const otel129: OtelVersion = { @@ -49,9 +52,14 @@ const otel129: OtelVersion = { [ColumnHint.TraceTags, 'SpanAttributes'], [ColumnHint.TraceServiceTags, 'ResourceAttributes'], [ColumnHint.TraceStatusCode, 'StatusCode'], - [ColumnHint.TraceEventsPrefix, 'Events'], + [ColumnHint.TraceKind, 'SpanKind'], + [ColumnHint.TraceStatusMessage, 'StatusMessage'], + [ColumnHint.TraceState, 'TraceState'], ]), + flattenNested: false, traceDurationUnit: TimeUnit.Nanoseconds, + traceEventsColumnPrefix: 'Events', + traceLinksColumnPrefix: 'Links', }; export const versions: readonly OtelVersion[] = [ diff --git a/src/types/config.ts b/src/types/config.ts index 180d72b7..a8e19972 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -92,7 +92,16 @@ export interface CHTracesConfig { startTimeColumn?: string; tagsColumn?: string; serviceTagsColumn?: string; - eventsColumnPrefix?: string; + kindColumn?: string; + statusCodeColumn?: string; + statusMessageColumn?: string; + stateColumn?: string; + instrumentationLibraryNameColumn?: string; + instrumentationLibraryVersionColumn?: string; + + flattenNested?: boolean; + traceEventsColumnPrefix?: string; + traceLinksColumnPrefix?: string; } export interface AliasTableEntry { diff --git a/src/types/queryBuilder.ts b/src/types/queryBuilder.ts index 2d43a72e..2e6a94b1 100644 --- a/src/types/queryBuilder.ts +++ b/src/types/queryBuilder.ts @@ -56,6 +56,14 @@ export interface QueryBuilderOptions { isTraceIdMode?: boolean; traceId?: string; + /** + * True if "Nested" column types should be treated as if they + * were created with flatten_nested=1. Applies to trace Events and Links columns. + */ + flattenNested?: boolean; + traceEventsColumnPrefix?: string; + traceLinksColumnPrefix?: string; + // Logs & Traces otelEnabled?: boolean; otelVersion?: string; @@ -133,7 +141,11 @@ export enum ColumnHint { TraceTags = 'trace_tags', TraceServiceTags = 'trace_service_tags', TraceStatusCode = 'trace_status_code', - TraceEventsPrefix = 'trace_events_prefix', + TraceKind = 'trace_kind', + TraceStatusMessage = 'trace_status_message', + TraceInstrumentationLibraryName = 'instrumentation_library_name', + TraceInstrumentationLibraryVersion = 'instrumentation_library_version', + TraceState = 'trace_state', } /** diff --git a/src/views/CHConfigEditor.tsx b/src/views/CHConfigEditor.tsx index d1e77985..babc82df 100644 --- a/src/views/CHConfigEditor.tsx +++ b/src/views/CHConfigEditor.tsx @@ -4,7 +4,7 @@ import { onUpdateDatasourceJsonDataOption, onUpdateDatasourceSecureJsonDataOption, } from '@grafana/data'; -import { RadioButtonGroup, Switch, Input, SecretInput, Button, Field, HorizontalGroup, Alert, VerticalGroup } from '@grafana/ui'; +import { RadioButtonGroup, Switch, Input, SecretInput, Button, Field, Alert, Stack } from '@grafana/ui'; import { CertificationKey } from '../components/ui/CertificationKey'; import { CHConfig, @@ -191,7 +191,7 @@ export const ConfigEditor: React.FC = (props) => { const uidWarning = (!options.uid) && ( - +
{'This datasource is missing the'} uid @@ -204,7 +204,7 @@ export const ConfigEditor: React.FC = (props) => { >provisioned via YAML {', please verify the UID is set. This is required to enable data linking between logs and traces.'}
-
+
); @@ -450,7 +450,15 @@ export const ConfigEditor: React.FC = (props) => { onStartTimeColumnChange={c => onTracesConfigChange('startTimeColumn', c)} onTagsColumnChange={c => onTracesConfigChange('tagsColumn', c)} onServiceTagsColumnChange={c => onTracesConfigChange('serviceTagsColumn', c)} - onEventsColumnPrefixChange={c => onTracesConfigChange('eventsColumnPrefix', c)} + onKindColumnChange={c => onTracesConfigChange('kindColumn', c)} + onStatusCodeColumnChange={c => onTracesConfigChange('statusCodeColumn', c)} + onStatusMessageColumnChange={c => onTracesConfigChange('statusMessageColumn', c)} + onStateColumnChange={c => onTracesConfigChange('stateColumn', c)} + onInstrumentationLibraryNameColumnChange={c => onTracesConfigChange('instrumentationLibraryNameColumn', c)} + onInstrumentationLibraryVersionColumnChange={c => onTracesConfigChange('instrumentationLibraryVersionColumn', c)} + onFlattenNestedChange={c => onTracesConfigChange('flattenNested', c)} + onEventsColumnPrefixChange={c => onTracesConfigChange('traceEventsColumnPrefix', c)} + onLinksColumnPrefixChange={c => onTracesConfigChange('traceLinksColumnPrefix', c)} /> @@ -471,7 +479,7 @@ export const ConfigEditor: React.FC = (props) => { {customSettings.map(({ setting, value }, i) => { return ( - + = (props) => { }} > - + ); })}