mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-17 08:50:42 +01:00
Compare commits
8 Commits
feat/heatm
...
ns/resourc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd1859919 | ||
|
|
341366e3ee | ||
|
|
4eb3dcb32d | ||
|
|
bb0afe3600 | ||
|
|
e504c4081e | ||
|
|
0fe45e7114 | ||
|
|
35356fc602 | ||
|
|
e1acfc94ba |
@@ -583,7 +583,7 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.dashboardModule.DeleteUnsafe(ctx, orgID, dashID); err != nil {
|
||||
if err := module.dashboardModule.DeleteUnsafeV2(ctx, orgID, dashID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,15 @@ func (module *module) DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) DeleteUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
return module.pkgDashboardModule.DeleteUnsafeV2(ctx, orgID, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { getAIObservabilityFieldsKeys } from 'api/generated/services/ai-observability';
|
||||
import { getFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { getFieldKeySuggestions } from '../getFieldKeySuggestions';
|
||||
import { FieldKeysResponse } from '../types';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsKeys: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/fields', () => ({
|
||||
getFieldsKeys: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIKeys = getAIObservabilityFieldsKeys as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsKeys
|
||||
>;
|
||||
const mockedGenericKeys = getFieldsKeys as jest.MockedFunction<
|
||||
typeof getFieldsKeys
|
||||
>;
|
||||
|
||||
const keysResponse = (): FieldKeysResponse => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
},
|
||||
});
|
||||
|
||||
describe('getFieldKeySuggestions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
const response = keysResponse();
|
||||
mockedAIKeys.mockResolvedValue(response);
|
||||
|
||||
const fieldKeysConfig = { searchText: 'llm' };
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
getFieldKeySuggestions(fieldKeysConfig, 'builder_ai_query', abortSignal),
|
||||
).resolves.toBe(response);
|
||||
expect(mockedAIKeys).toHaveBeenCalledWith(fieldKeysConfig, abortSignal);
|
||||
expect(mockedGenericKeys).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<
|
||||
[
|
||||
'an unmarked query' | 'an explicitly generic query',
|
||||
undefined | 'builder_query',
|
||||
]
|
||||
>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const response = keysResponse();
|
||||
mockedGenericKeys.mockResolvedValue(response);
|
||||
|
||||
const fieldKeysConfig = {
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
searchText: 'svc',
|
||||
};
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
getFieldKeySuggestions(fieldKeysConfig, builderQueryType, abortSignal),
|
||||
).resolves.toBe(response);
|
||||
expect(mockedGenericKeys).toHaveBeenCalledWith(fieldKeysConfig, abortSignal);
|
||||
expect(mockedAIKeys).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { getAIObservabilityFieldsValues } from 'api/generated/services/ai-observability';
|
||||
import { getFieldsValues } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { getFieldValueSuggestions } from '../getFieldValueSuggestions';
|
||||
import { FieldValuesResponse } from '../types';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/fields', () => ({
|
||||
getFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIValues = getAIObservabilityFieldsValues as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsValues
|
||||
>;
|
||||
const mockedGenericValues = getFieldsValues as jest.MockedFunction<
|
||||
typeof getFieldsValues
|
||||
>;
|
||||
|
||||
const valuesResponse = (): FieldValuesResponse => ({
|
||||
status: 'success',
|
||||
data: { complete: true, values: { stringValues: ['gpt-4o'] } },
|
||||
});
|
||||
|
||||
describe('getFieldValueSuggestions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query, forwarding the key as name', async () => {
|
||||
const response = valuesResponse();
|
||||
mockedAIValues.mockResolvedValue(response);
|
||||
|
||||
const fieldValuesConfig = { name: 'gen_ai.request.model', searchText: 'gpt' };
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
getFieldValueSuggestions(fieldValuesConfig, 'builder_ai_query', abortSignal),
|
||||
).resolves.toBe(response);
|
||||
expect(mockedAIValues).toHaveBeenCalledWith(fieldValuesConfig, abortSignal);
|
||||
expect(mockedGenericValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<
|
||||
[
|
||||
'an unmarked query' | 'an explicitly generic query',
|
||||
undefined | 'builder_query',
|
||||
]
|
||||
>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const response = valuesResponse();
|
||||
mockedGenericValues.mockResolvedValue(response);
|
||||
|
||||
const fieldValuesConfig = {
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
name: 'service.name',
|
||||
searchText: 'front',
|
||||
};
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
getFieldValueSuggestions(fieldValuesConfig, builderQueryType, abortSignal),
|
||||
).resolves.toBe(response);
|
||||
expect(mockedGenericValues).toHaveBeenCalledWith(
|
||||
fieldValuesConfig,
|
||||
abortSignal,
|
||||
);
|
||||
expect(mockedAIValues).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
14
frontend/src/api/querySuggestions/getFieldKeySuggestions.ts
Normal file
14
frontend/src/api/querySuggestions/getFieldKeySuggestions.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getAIObservabilityFieldsKeys } from 'api/generated/services/ai-observability';
|
||||
import { getFieldsKeys } from 'api/generated/services/fields';
|
||||
import type { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
|
||||
import { FieldKeysConfig, FieldKeysResponse } from './types';
|
||||
|
||||
export const getFieldKeySuggestions = (
|
||||
fieldKeysConfig: FieldKeysConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<FieldKeysResponse> =>
|
||||
builderQueryType === 'builder_ai_query'
|
||||
? getAIObservabilityFieldsKeys(fieldKeysConfig, abortSignal)
|
||||
: getFieldsKeys(fieldKeysConfig, abortSignal);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getAIObservabilityFieldsValues } from 'api/generated/services/ai-observability';
|
||||
import { getFieldsValues } from 'api/generated/services/fields';
|
||||
import type { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
|
||||
import { FieldValuesConfig, FieldValuesResponse } from './types';
|
||||
|
||||
export const getFieldValueSuggestions = (
|
||||
fieldValuesConfig: FieldValuesConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<FieldValuesResponse> =>
|
||||
builderQueryType === 'builder_ai_query'
|
||||
? getAIObservabilityFieldsValues(fieldValuesConfig, abortSignal)
|
||||
: getFieldsValues(fieldValuesConfig, abortSignal);
|
||||
31
frontend/src/api/querySuggestions/types.ts
Normal file
31
frontend/src/api/querySuggestions/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type {
|
||||
GetAIObservabilityFieldsKeys200,
|
||||
GetAIObservabilityFieldsValues200,
|
||||
GetAIObservabilityFieldsKeysParams,
|
||||
GetAIObservabilityFieldsValuesParams,
|
||||
GetFieldsKeys200,
|
||||
GetFieldsKeysParams,
|
||||
GetFieldsValues200,
|
||||
GetFieldsValuesParams,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type FieldKeysConfig =
|
||||
| GetFieldsKeysParams
|
||||
| GetAIObservabilityFieldsKeysParams;
|
||||
|
||||
export type FieldValuesConfig =
|
||||
| GetFieldsValuesParams
|
||||
| GetAIObservabilityFieldsValuesParams;
|
||||
|
||||
export type FieldKeysConfigProp = Omit<
|
||||
FieldKeysConfig,
|
||||
'signal' | 'searchText'
|
||||
>;
|
||||
|
||||
export type FieldKeysResponse =
|
||||
| GetFieldsKeys200
|
||||
| GetAIObservabilityFieldsKeys200;
|
||||
|
||||
export type FieldValuesResponse =
|
||||
| GetFieldsValues200
|
||||
| GetAIObservabilityFieldsValues200;
|
||||
@@ -6,7 +6,8 @@ import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { FieldKeysConfigProp } from 'api/querySuggestions/types';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import AddedFields from './AddedFields';
|
||||
@@ -31,6 +32,9 @@ interface FieldsSelectorProps {
|
||||
// Lets users add a free-typed field which
|
||||
// does not show up in the suggestions
|
||||
allowCustomFields?: boolean;
|
||||
fieldKeysConfig?: FieldKeysConfigProp;
|
||||
builderQueryType?: BuilderQueryType;
|
||||
extraFields?: TelemetryFieldKey[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
@@ -50,6 +54,9 @@ function FieldsSelectorContent({
|
||||
maxFields,
|
||||
requiredFields,
|
||||
allowCustomFields,
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields,
|
||||
width = DEFAULT_PANEL_WIDTH,
|
||||
height,
|
||||
defaultPosition,
|
||||
@@ -158,6 +165,9 @@ function FieldsSelectorContent({
|
||||
onAdd={handleAdd}
|
||||
isAtLimit={isAtLimit}
|
||||
allowCustomFields={allowCustomFields}
|
||||
fieldKeysConfig={fieldKeysConfig}
|
||||
builderQueryType={builderQueryType}
|
||||
extraFields={extraFields}
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
|
||||
@@ -3,18 +3,22 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { FieldKeysConfigProp } from 'api/querySuggestions/types';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
BuilderQueryType,
|
||||
FieldContext,
|
||||
SignalType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
import { mergeExtraFields } from 'utils/extraFields';
|
||||
|
||||
import styles from './FieldsSelector.module.scss';
|
||||
|
||||
const EMPTY_EXTRA_FIELDS: TelemetryFieldKey[] = [];
|
||||
|
||||
interface OtherFieldsProps {
|
||||
signal: DataSource;
|
||||
debouncedInputValue: string;
|
||||
@@ -22,6 +26,9 @@ interface OtherFieldsProps {
|
||||
onAdd: (field: TelemetryFieldKey) => void;
|
||||
isAtLimit: boolean;
|
||||
allowCustomFields?: boolean;
|
||||
fieldKeysConfig?: FieldKeysConfigProp;
|
||||
builderQueryType?: BuilderQueryType;
|
||||
extraFields?: TelemetryFieldKey[];
|
||||
}
|
||||
|
||||
function OtherFields({
|
||||
@@ -31,26 +38,26 @@ function OtherFields({
|
||||
onAdd,
|
||||
isAtLimit,
|
||||
allowCustomFields,
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields = EMPTY_EXTRA_FIELDS,
|
||||
}: OtherFieldsProps): JSX.Element {
|
||||
const { data, isFetching } = useGetQueryKeySuggestions(
|
||||
const { data: fetchedFields, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal,
|
||||
...fieldKeysConfig,
|
||||
signal: DATA_SOURCE_TO_SIGNAL[signal],
|
||||
searchText: debouncedInputValue,
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FIELDS_SELECTOR_SUGGESTIONS,
|
||||
signal,
|
||||
debouncedInputValue,
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
const otherFields = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
const search = debouncedInputValue.trim().toLowerCase();
|
||||
// Normalize: synthesize `key` once so downstream reads can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
const suggestions: TelemetryFieldKey[] = mergeExtraFields(
|
||||
extraFields.filter((field) => field.name.toLowerCase().includes(search)),
|
||||
fetchedFields ?? [],
|
||||
).map((attr) => ({
|
||||
...attr,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
|
||||
signal: attr.signal as SignalType,
|
||||
@@ -87,7 +94,13 @@ function OtherFields({
|
||||
key: buildCompositeKey(typed, ''),
|
||||
};
|
||||
return [customField, ...available];
|
||||
}, [data, addedFields, allowCustomFields, debouncedInputValue]);
|
||||
}, [
|
||||
extraFields,
|
||||
fetchedFields,
|
||||
addedFields,
|
||||
allowCustomFields,
|
||||
debouncedInputValue,
|
||||
]);
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { act, fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import FieldsSelector from '../FieldsSelector';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
|
||||
useFieldKeysSuggestion: jest.fn(() => ({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
isFetched: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
@@ -21,22 +27,15 @@ jest.mock('periscope/components/FloatingPanel', () => ({
|
||||
}));
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
(useFieldKeysSuggestion as jest.Mock).mockReturnValue({
|
||||
data: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
isFetching: false,
|
||||
isFetched: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfigProp } from 'api/querySuggestions/types';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import OtherFields from '../OtherFields';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
|
||||
useFieldKeysSuggestion: jest.fn(() => ({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
isFetched: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
(useFieldKeysSuggestion as jest.Mock).mockReturnValue({
|
||||
data: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
isFetching: false,
|
||||
isFetched: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -82,7 +83,6 @@ describe('OtherFields — custom (free-typed) option', () => {
|
||||
mockSuggestions(['orderId']);
|
||||
renderOtherFields({ debouncedInputValue: 'orderid' });
|
||||
|
||||
// the real suggestion shows, the lowercased custom name does not
|
||||
expect(screen.getByText('orderId')).toBeInTheDocument();
|
||||
expect(screen.queryByText('orderid')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -116,10 +116,126 @@ describe('OtherFields — custom (free-typed) option', () => {
|
||||
it('shows the custom option at the field limit but hides its Add button', () => {
|
||||
renderOtherFields({ debouncedInputValue: 'unknown.a.b.c', isAtLimit: true });
|
||||
|
||||
// same as every other row at the limit: name shown, no Add button
|
||||
expect(screen.getByText('unknown.a.b.c')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /add/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OtherFields — field keys config', () => {
|
||||
const pool: TelemetryFieldKey[] = [
|
||||
{ name: 'total_tokens', fieldContext: 'trace', fieldDataType: 'float64' },
|
||||
{ name: 'llm_call_count', fieldContext: 'trace', fieldDataType: 'float64' },
|
||||
];
|
||||
|
||||
const fieldKeysConfig: FieldKeysConfigProp = {
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
};
|
||||
const builderQueryType: BuilderQueryType = 'builder_ai_query';
|
||||
|
||||
const mockPool = (fields: TelemetryFieldKey[]): void => {
|
||||
(useFieldKeysSuggestion as jest.Mock).mockReturnValue({
|
||||
data: fields,
|
||||
isFetching: false,
|
||||
isFetched: true,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockPool(pool);
|
||||
});
|
||||
|
||||
it('lists the pool it is handed', () => {
|
||||
renderOtherFields({
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.getByText('total_tokens')).toBeInTheDocument();
|
||||
expect(screen.getByText('llm_call_count')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('forwards the fetch params and search to the shared keys hook', () => {
|
||||
renderOtherFields({
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
allowCustomFields: false,
|
||||
debouncedInputValue: 'llm',
|
||||
});
|
||||
|
||||
expect(useFieldKeysSuggestion).toHaveBeenCalledWith(
|
||||
{
|
||||
...fieldKeysConfig,
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.LOGS],
|
||||
searchText: 'llm',
|
||||
},
|
||||
builderQueryType,
|
||||
);
|
||||
});
|
||||
|
||||
it('lists extra fields the keys endpoint never returns', () => {
|
||||
mockPool([{ name: 'total_tokens' } as TelemetryFieldKey]);
|
||||
|
||||
renderOtherFields({
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields: [{ name: 'last_activity_time' } as TelemetryFieldKey],
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.getByText('last_activity_time')).toBeInTheDocument();
|
||||
expect(screen.getByText('total_tokens')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters extra fields by search text', () => {
|
||||
mockPool([]);
|
||||
|
||||
renderOtherFields({
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields: [
|
||||
{ name: 'last_activity_time' } as TelemetryFieldKey,
|
||||
{ name: 'timestamp' } as TelemetryFieldKey,
|
||||
],
|
||||
debouncedInputValue: 'activity',
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.getByText('last_activity_time')).toBeInTheDocument();
|
||||
expect(screen.queryByText('timestamp')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps a fetched key whose name does not contain the search text', () => {
|
||||
mockPool([
|
||||
{ name: 'service.name', fieldContext: 'resource' } as TelemetryFieldKey,
|
||||
]);
|
||||
|
||||
renderOtherFields({
|
||||
debouncedInputValue: 'resource.service',
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.getByText('service.name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('omits pool fields that are already added', () => {
|
||||
renderOtherFields({
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
allowCustomFields: false,
|
||||
addedFields: [
|
||||
{
|
||||
name: 'total_tokens',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
key: 'trace:total_tokens:float64',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('total_tokens')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('llm_call_count')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { FieldKeysConfigProp } from 'api/querySuggestions/types';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import './ListViewOrderBy.styles.scss';
|
||||
|
||||
const DEFAULT_EXTRA_FIELDS: TelemetryFieldKey[] = [
|
||||
{ name: 'timestamp' } as TelemetryFieldKey,
|
||||
];
|
||||
|
||||
interface ListViewOrderByProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
dataSource: DataSource;
|
||||
fieldKeysConfig?: FieldKeysConfigProp;
|
||||
builderQueryType?: BuilderQueryType;
|
||||
extraFields?: TelemetryFieldKey[];
|
||||
}
|
||||
|
||||
// Loader component for the dropdown when loading or no results
|
||||
@@ -26,6 +33,9 @@ function ListViewOrderBy({
|
||||
value,
|
||||
onChange,
|
||||
dataSource,
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields = DEFAULT_EXTRA_FIELDS,
|
||||
}: ListViewOrderByProps): JSX.Element {
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [debouncedInput, setDebouncedInput] = useState('');
|
||||
@@ -34,17 +44,14 @@ function ListViewOrderBy({
|
||||
>([]);
|
||||
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Fetch key suggestions based on debounced input
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['orderByKeySuggestions', dataSource, debouncedInput],
|
||||
queryFn: async () => {
|
||||
const response = await getKeySuggestions({
|
||||
signal: dataSource,
|
||||
searchText: debouncedInput,
|
||||
});
|
||||
return response.data;
|
||||
const { data, isLoading } = useFieldKeysSuggestion(
|
||||
{
|
||||
...fieldKeysConfig,
|
||||
signal: DATA_SOURCE_TO_SIGNAL[dataSource],
|
||||
searchText: debouncedInput,
|
||||
},
|
||||
});
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
@@ -55,24 +62,24 @@ function ListViewOrderBy({
|
||||
[],
|
||||
);
|
||||
|
||||
const extraKeysSignature = extraFields.map((field) => field.name).join(',');
|
||||
|
||||
// Update options when API data changes
|
||||
useEffect(() => {
|
||||
const rawKeys: QueryKeyDataSuggestionsProps[] = data?.data?.keys
|
||||
? Object.values(data.data?.keys).flat()
|
||||
: [];
|
||||
const keyNames = (data ?? []).map((field) => field.name);
|
||||
const search = searchInput.trim().toLowerCase();
|
||||
const extraMatches = extraKeysSignature
|
||||
.split(',')
|
||||
.filter((key) => key.length > 0 && key.toLowerCase().includes(search));
|
||||
const uniqueKeys = [...new Set([...extraMatches, ...keyNames])];
|
||||
|
||||
const keyNames = rawKeys.map((key) => key.name);
|
||||
const uniqueKeys = [
|
||||
...new Set(searchInput ? keyNames : ['timestamp', ...keyNames]),
|
||||
];
|
||||
|
||||
const updatedOptions = uniqueKeys.flatMap((key) => [
|
||||
{ label: `${key} (desc)`, value: `${key}:desc` },
|
||||
{ label: `${key} (asc)`, value: `${key}:asc` },
|
||||
]);
|
||||
|
||||
setSelectOptions(updatedOptions);
|
||||
}, [data, searchInput]);
|
||||
setSelectOptions(
|
||||
uniqueKeys.flatMap((key) => [
|
||||
{ label: `${key} (desc)`, value: `${key}:desc` },
|
||||
{ label: `${key} (asc)`, value: `${key}:asc` },
|
||||
]),
|
||||
);
|
||||
}, [data, searchInput, extraKeysSignature]);
|
||||
|
||||
// Handle search input with debounce
|
||||
const handleSearch = (input: string): void => {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import {
|
||||
TRACE_VIEW_BUILDER_QUERY_TYPE,
|
||||
TRACE_VIEW_FIELD_KEYS,
|
||||
TRACE_VIEW_ORDER_BY_EXTRA_FIELDS,
|
||||
} from 'container/LLMObservability/Explorer/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import ListViewOrderBy from '../ListViewOrderBy';
|
||||
|
||||
const seenAI: URLSearchParams[] = [];
|
||||
const seenGeneric: URLSearchParams[] = [];
|
||||
|
||||
const mockAIKeys = (names: string[]): void => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${ENVIRONMENT.baseURL}/api/v1/ai_observability/fields/keys`,
|
||||
(req, res, ctx) => {
|
||||
seenAI.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: Object.fromEntries(names.map((name) => [name, [{ name }]])),
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const mockGenericKeys = (names: string[]): void => {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (req, res, ctx) => {
|
||||
seenGeneric.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: Object.fromEntries(names.map((name) => [name, [{ name }]])),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const openDropdown = (): void => {
|
||||
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||
};
|
||||
|
||||
const getOptionLabels = (): string[] =>
|
||||
Array.from(document.querySelectorAll('.ant-select-item-option-content')).map(
|
||||
(node) => node.textContent ?? '',
|
||||
);
|
||||
|
||||
describe('ListViewOrderBy', () => {
|
||||
beforeEach(() => {
|
||||
seenAI.length = 0;
|
||||
seenGeneric.length = 0;
|
||||
});
|
||||
|
||||
it('reads the ai_observability trace context for an AI query', async () => {
|
||||
mockAIKeys(['total_tokens']);
|
||||
|
||||
render(
|
||||
<ListViewOrderBy
|
||||
value="last_activity_time:desc"
|
||||
onChange={jest.fn()}
|
||||
dataSource={DataSource.TRACES}
|
||||
fieldKeysConfig={TRACE_VIEW_FIELD_KEYS}
|
||||
builderQueryType={TRACE_VIEW_BUILDER_QUERY_TYPE}
|
||||
extraFields={TRACE_VIEW_ORDER_BY_EXTRA_FIELDS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(seenAI).toHaveLength(1);
|
||||
});
|
||||
expect(seenAI[0]?.get('searchText')).toBe('');
|
||||
expect(seenAI[0]?.get('fieldContext')).toBe(
|
||||
TelemetrytypesFieldContextDTO.trace,
|
||||
);
|
||||
expect(seenGeneric).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('offers the extra keys alongside the ones the endpoint reports', async () => {
|
||||
mockAIKeys(['total_tokens']);
|
||||
|
||||
render(
|
||||
<ListViewOrderBy
|
||||
value="last_activity_time:desc"
|
||||
onChange={jest.fn()}
|
||||
dataSource={DataSource.TRACES}
|
||||
fieldKeysConfig={TRACE_VIEW_FIELD_KEYS}
|
||||
builderQueryType={TRACE_VIEW_BUILDER_QUERY_TYPE}
|
||||
extraFields={TRACE_VIEW_ORDER_BY_EXTRA_FIELDS}
|
||||
/>,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getOptionLabels()).toContain('total_tokens (desc)');
|
||||
});
|
||||
expect(getOptionLabels()).toContain('last_activity_time (asc)');
|
||||
});
|
||||
|
||||
it('keeps a matching extra key while searching', async () => {
|
||||
mockAIKeys([]);
|
||||
|
||||
render(
|
||||
<ListViewOrderBy
|
||||
value="last_activity_time:desc"
|
||||
onChange={jest.fn()}
|
||||
dataSource={DataSource.TRACES}
|
||||
fieldKeysConfig={TRACE_VIEW_FIELD_KEYS}
|
||||
builderQueryType={TRACE_VIEW_BUILDER_QUERY_TYPE}
|
||||
extraFields={TRACE_VIEW_ORDER_BY_EXTRA_FIELDS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(seenAI.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
openDropdown();
|
||||
fireEvent.change(screen.getByRole('combobox'), {
|
||||
target: { value: 'activity' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getOptionLabels()).toContain('last_activity_time (desc)');
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to timestamp and the generic endpoint', async () => {
|
||||
mockGenericKeys(['service.name']);
|
||||
|
||||
render(
|
||||
<ListViewOrderBy
|
||||
value="timestamp:desc"
|
||||
onChange={jest.fn()}
|
||||
dataSource={DataSource.TRACES}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(seenGeneric).toHaveLength(1);
|
||||
});
|
||||
expect(seenGeneric[0]?.get('signal')).toBe(DataSource.TRACES);
|
||||
expect(seenGeneric[0]?.get('searchText')).toBe('');
|
||||
|
||||
openDropdown();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getOptionLabels()).toContain('timestamp (desc)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from 'types/antlrQueryTypes';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
getCurrentValueIndexAtCursor,
|
||||
getQueryContextAtCursor,
|
||||
@@ -45,6 +45,13 @@ import { validateQuery } from 'utils/queryValidationUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
|
||||
import { getRecentQueries } from 'lib/recentQueries/getRecentQueries';
|
||||
import type {
|
||||
TelemetrytypesGettableFieldKeysDTOKeysAnyOf,
|
||||
TelemetrytypesSourceDTO,
|
||||
TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getFieldKeySuggestions } from 'api/querySuggestions/getFieldKeySuggestions';
|
||||
import { getFieldValueSuggestions } from 'api/querySuggestions/getFieldValueSuggestions';
|
||||
import type { SignalType } from 'types/api/v5/queryRange';
|
||||
|
||||
import {
|
||||
@@ -52,12 +59,6 @@ import {
|
||||
SUGGESTION_FETCH_DEBOUNCE_MS,
|
||||
SUGGESTIONS_SECTION,
|
||||
} from './constants';
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
SuggestedFieldKey,
|
||||
SuggestedFieldKeysByName,
|
||||
} from './fieldSuggestions';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
@@ -265,8 +266,10 @@ function QuerySearch({
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: SuggestedFieldKeysByName): any[] =>
|
||||
Object.values(keys).flatMap((items: SuggestedFieldKey[]) =>
|
||||
const generateOptions = (
|
||||
keys: TelemetrytypesGettableFieldKeysDTOKeysAnyOf,
|
||||
): any[] =>
|
||||
Object.values(keys).flatMap((items: TelemetrytypesTelemetryFieldKeyDTO[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
@@ -319,17 +322,19 @@ function QuerySearch({
|
||||
|
||||
lastFetchedKeyRef.current = searchText || '';
|
||||
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricNamespace,
|
||||
});
|
||||
const response = await getFieldKeySuggestions(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[dataSource],
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricNamespace,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
);
|
||||
|
||||
if (response.data.data) {
|
||||
const { keys } = response.data.data;
|
||||
if (response.data.keys) {
|
||||
const { keys } = response.data;
|
||||
const options = generateOptions(keys);
|
||||
// Deduplicate by full variant identity (name + context + data type), NOT by
|
||||
// label. deduping by label removes varient which is not expected. If we need
|
||||
@@ -497,21 +502,23 @@ function QuerySearch({
|
||||
try {
|
||||
const values = valueSuggestionsOverride
|
||||
? await valueSuggestionsOverride(key, sanitizedSearchText)
|
||||
: await fetchFieldValuesForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText: sanitizedSearchText,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
}).then((response) => {
|
||||
const responseData = response.data as any;
|
||||
const data = responseData.data || {};
|
||||
const values = data.values || {};
|
||||
: await getFieldValueSuggestions(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[dataSource],
|
||||
name: key,
|
||||
searchText: sanitizedSearchText,
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
).then((response) => {
|
||||
const responseData = response.data;
|
||||
const responseDataValues = responseData.values;
|
||||
|
||||
return {
|
||||
stringValues: values.stringValues || [],
|
||||
numberValues: values.numberValues || [],
|
||||
complete: data.complete ?? false,
|
||||
stringValues: responseDataValues.stringValues ?? [],
|
||||
numberValues: responseDataValues.numberValues ?? [],
|
||||
complete: responseData.complete ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
} from '../fieldSuggestions';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsKeys: jest.fn(),
|
||||
getAIObservabilityFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIKeys = getAIObservabilityFieldsKeys as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsKeys
|
||||
>;
|
||||
const mockedGenericKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
>;
|
||||
const mockedAIValues = getAIObservabilityFieldsValues as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsValues
|
||||
>;
|
||||
const mockedGenericValues = getValueSuggestions as jest.MockedFunction<
|
||||
typeof getValueSuggestions
|
||||
>;
|
||||
|
||||
const aiValuesResponse = (
|
||||
values: { stringValues?: string[]; numberValues?: number[] } | null,
|
||||
complete = true,
|
||||
): Awaited<ReturnType<typeof getAIObservabilityFieldsValues>> =>
|
||||
({
|
||||
status: 'success',
|
||||
data: { complete, values },
|
||||
}) as Awaited<ReturnType<typeof getAIObservabilityFieldsValues>>;
|
||||
|
||||
describe('fetchFieldKeysForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
},
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const keys = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'llm',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).toHaveBeenCalledWith({ searchText: 'llm' });
|
||||
expect(mockedGenericKeys).not.toHaveBeenCalled();
|
||||
expect(keys.data.data).toStrictEqual({
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
mockedGenericKeys.mockResolvedValue({
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as Awaited<ReturnType<typeof getKeySuggestions>>);
|
||||
|
||||
await fetchFieldKeysForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'svc',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).not.toHaveBeenCalled();
|
||||
expect(mockedGenericKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.TRACES, searchText: 'svc' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a null ai_observability keys payload to an empty map', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: false, keys: null },
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(response.data.data).toStrictEqual({ complete: false, keys: {} });
|
||||
});
|
||||
|
||||
it('passes the generic response through untouched', async () => {
|
||||
const genericResponse = {
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as unknown as Awaited<ReturnType<typeof getKeySuggestions>>;
|
||||
mockedGenericKeys.mockResolvedValue(genericResponse);
|
||||
|
||||
await expect(
|
||||
fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFieldValuesForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIValues.mockResolvedValue(
|
||||
aiValuesResponse({ stringValues: ['gpt-4o'], numberValues: [] }),
|
||||
);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'gen_ai.request.model',
|
||||
searchText: 'gpt',
|
||||
});
|
||||
|
||||
expect(mockedGenericValues).not.toHaveBeenCalled();
|
||||
expect(response).toStrictEqual({
|
||||
data: {
|
||||
data: {
|
||||
complete: true,
|
||||
values: { stringValues: ['gpt-4o'], numberValues: [] },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the key as the name the endpoint expects', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse({}));
|
||||
|
||||
await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).toHaveBeenCalledWith({
|
||||
name: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps the ai_observability payload in the envelope the call site unwraps', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse(null, false));
|
||||
|
||||
await expect(
|
||||
fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'llm_call_count',
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toStrictEqual({
|
||||
data: { data: { complete: false, values: null } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const genericResponse = {
|
||||
data: {
|
||||
data: { complete: false, values: { stringValues: ['frontend'] } },
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof getValueSuggestions>>;
|
||||
mockedGenericValues.mockResolvedValue(genericResponse);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).not.toHaveBeenCalled();
|
||||
expect(mockedGenericValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signal: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
}),
|
||||
);
|
||||
expect(response).toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface SuggestedFieldKey {
|
||||
name: string;
|
||||
fieldContext?: string;
|
||||
fieldDataType?: string;
|
||||
}
|
||||
|
||||
export type SuggestedFieldKeysByName = Record<string, SuggestedFieldKey[]>;
|
||||
|
||||
export interface SuggestedFieldKeysPayload {
|
||||
complete: boolean;
|
||||
keys: SuggestedFieldKeysByName;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldKeysResponse {
|
||||
data: { data?: SuggestedFieldKeysPayload };
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesPayload {
|
||||
complete?: boolean;
|
||||
values?: {
|
||||
stringValues?: string[] | null;
|
||||
numberValues?: number[] | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesResponse {
|
||||
data: { data?: SuggestedFieldValuesPayload };
|
||||
}
|
||||
|
||||
interface FetchFieldKeysParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
metricNamespace?: string;
|
||||
}
|
||||
|
||||
interface FetchFieldValuesParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
key: string;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
}
|
||||
|
||||
export const fetchFieldKeysForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
}: FetchFieldKeysParams): Promise<SuggestedFieldKeysResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsKeys({ searchText });
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: response.data
|
||||
? { complete: response.data.complete, keys: response.data.keys ?? {} }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchFieldValuesForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
}: FetchFieldValuesParams): Promise<SuggestedFieldValuesResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsValues({
|
||||
name: key,
|
||||
searchText,
|
||||
});
|
||||
|
||||
return { data: { data: response.data } };
|
||||
}
|
||||
|
||||
// getValueSuggestions' declared response type does not match what the endpoint returns.
|
||||
return getValueSuggestions({
|
||||
signal: dataSource,
|
||||
key,
|
||||
searchText,
|
||||
signalSource,
|
||||
metricName,
|
||||
}) as unknown as Promise<SuggestedFieldValuesResponse>;
|
||||
};
|
||||
@@ -1,10 +1,15 @@
|
||||
import { EditorView } from '@uiw/react-codemirror';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { getFieldKeySuggestions } from 'api/querySuggestions/getFieldKeySuggestions';
|
||||
import { getFieldValueSuggestions } from 'api/querySuggestions/getFieldValueSuggestions';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
@@ -30,17 +35,25 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
data: { keys: {} as Record<string, QueryKeyDataSuggestionsProps[]> },
|
||||
},
|
||||
jest.mock('api/querySuggestions/getFieldKeySuggestions', () => ({
|
||||
getFieldKeySuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: {} },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { values: { stringValues: [], numberValues: [] } } },
|
||||
jest.mock('api/querySuggestions/getFieldValueSuggestions', () => ({
|
||||
getFieldValueSuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
values: {
|
||||
stringValues: [],
|
||||
numberValues: [],
|
||||
boolValues: [],
|
||||
relatedValues: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -68,8 +81,8 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
|
||||
it('fetches key suggestions when typing a key (debounced)', async () => {
|
||||
// Use real timers for CodeMirror integration tests
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
const mockedGetKeys = getFieldKeySuggestions as jest.MockedFunction<
|
||||
typeof getFieldKeySuggestions
|
||||
>;
|
||||
mockedGetKeys.mockClear();
|
||||
|
||||
@@ -102,10 +115,22 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
|
||||
it('fetches value suggestions when editing value context', async () => {
|
||||
// Use real timers for CodeMirror integration tests
|
||||
const mockedGetValues = getValueSuggestions as jest.MockedFunction<
|
||||
typeof getValueSuggestions
|
||||
const mockedGetValues = getFieldValueSuggestions as jest.MockedFunction<
|
||||
typeof getFieldValueSuggestions
|
||||
>;
|
||||
mockedGetValues.mockClear();
|
||||
mockedGetValues.mockResolvedValueOnce({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
values: {
|
||||
stringValues: ['payment-service'],
|
||||
numberValues: [200],
|
||||
boolValues: [],
|
||||
relatedValues: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<QuerySearch
|
||||
@@ -129,12 +154,18 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
// the string and number values off the response both reach the dropdown
|
||||
await expect(
|
||||
screen.findByText('payment-service'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText('200')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fetches key suggestions on mount for LOGS', async () => {
|
||||
// Use real timers for CodeMirror integration tests
|
||||
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
const mockedGetKeysOnMount = getFieldKeySuggestions as jest.MockedFunction<
|
||||
typeof getFieldKeySuggestions
|
||||
>;
|
||||
mockedGetKeysOnMount.mockClear();
|
||||
|
||||
@@ -153,6 +184,7 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
() =>
|
||||
expect(mockedGetKeysOnMount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.LOGS, searchText: '' }),
|
||||
undefined,
|
||||
),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
@@ -357,8 +389,8 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
const mockedGetKeys = getFieldKeySuggestions as jest.MockedFunction<
|
||||
typeof getFieldKeySuggestions
|
||||
>;
|
||||
mockedGetKeys.mockClear();
|
||||
|
||||
|
||||
@@ -31,15 +31,25 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { keys: {} } },
|
||||
jest.mock('api/querySuggestions/getFieldKeySuggestions', () => ({
|
||||
getFieldKeySuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: {} },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { values: { stringValues: [], numberValues: [] } } },
|
||||
jest.mock('api/querySuggestions/getFieldValueSuggestions', () => ({
|
||||
getFieldValueSuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
values: {
|
||||
stringValues: [],
|
||||
numberValues: [],
|
||||
boolValues: [],
|
||||
relatedValues: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetFieldsValues } from 'api/generated/services/fields';
|
||||
import {
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
@@ -29,15 +26,6 @@ interface UseFieldValuesReturn {
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export const DATA_SOURCE_TO_SIGNAL: Record<
|
||||
DataSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
|
||||
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
|
||||
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
|
||||
};
|
||||
|
||||
const QUICK_FILTERS_SOURCE_TO_SOURCE: Partial<
|
||||
Record<QuickFiltersSource, TelemetrytypesSourceDTO>
|
||||
> = {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Button, Skeleton } from 'antd';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'components/QuickFilters/FilterRenderers/Checkbox/v2/useFieldValues';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,8 @@ export enum LOCALSTORAGE {
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
TRACES_VIEW_COLUMNS = 'TRACES_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMNS = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_COLUMNS = 'AI_OBSERVABILITY_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
|
||||
@@ -106,8 +106,8 @@ export const REACT_QUERY_KEY = {
|
||||
// Dashboard Grid Card Query Keys
|
||||
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
|
||||
|
||||
// Fields Selector Query Keys
|
||||
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
|
||||
// Field Keys Suggestion Query Keys
|
||||
FIELD_KEYS_SUGGESTION: 'FIELD_KEYS_SUGGESTION',
|
||||
|
||||
// AI Assistant Query Keys
|
||||
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Settings } from '@signozhq/icons';
|
||||
import { FieldKeysConfigProp } from 'api/querySuggestions/types';
|
||||
import FieldsSelector from 'components/FieldsSelector';
|
||||
import Controls, { ControlsProps } from 'container/Controls';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import useQueryPagination from 'hooks/queryPagination/useQueryPagination';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './Controls.module.scss';
|
||||
@@ -14,7 +16,10 @@ function TraceExplorerControls({
|
||||
totalCount,
|
||||
perPageOptions,
|
||||
config,
|
||||
showSizeChanger = true,
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
extraFields,
|
||||
requiredFields,
|
||||
}: TraceExplorerControlsProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
const [isFieldsSelectorOpen, setIsFieldsSelectorOpen] = useState(false);
|
||||
@@ -44,6 +49,10 @@ function TraceExplorerControls({
|
||||
onFieldsChange={config.fieldsSelector.onFieldsChange}
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.TRACES}
|
||||
fieldKeysConfig={fieldKeysConfig}
|
||||
builderQueryType={builderQueryType}
|
||||
extraFields={extraFields}
|
||||
requiredFields={requiredFields}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -57,26 +66,28 @@ function TraceExplorerControls({
|
||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
showSizeChanger={showSizeChanger}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TraceExplorerControls.defaultProps = {
|
||||
config: null,
|
||||
};
|
||||
|
||||
type TraceExplorerControlsProps = Pick<
|
||||
ControlsProps,
|
||||
'isLoading' | 'totalCount' | 'perPageOptions'
|
||||
> & {
|
||||
config?: OptionsMenuConfig | null;
|
||||
showSizeChanger?: boolean;
|
||||
fieldKeysConfig?: FieldKeysConfigProp;
|
||||
builderQueryType?: BuilderQueryType;
|
||||
extraFields?: TelemetryFieldKey[];
|
||||
requiredFields?: readonly string[];
|
||||
};
|
||||
|
||||
TraceExplorerControls.defaultProps = {
|
||||
showSizeChanger: true,
|
||||
config: null,
|
||||
fieldKeysConfig: undefined,
|
||||
builderQueryType: undefined,
|
||||
extraFields: undefined,
|
||||
requiredFields: undefined,
|
||||
};
|
||||
|
||||
export default memo(TraceExplorerControls);
|
||||
|
||||
@@ -10,6 +10,25 @@
|
||||
|
||||
.actionsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.orderByContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.orderByLabel {
|
||||
color: var(--muted-foreground);
|
||||
// Between --periscope-font-size-small (11px) and -base (13px), so literal.
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
@@ -3,35 +3,45 @@ import {
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getTraceLink } from 'container/TracesExplorer/ListView/utils';
|
||||
import { TracesTableRow } from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import TracesTable from 'container/TracesExplorer/TracesTable/TracesTable';
|
||||
import { getTraceLink } from '../ListView/utils';
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import TracesTable from '../TracesTable/TracesTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import {
|
||||
TRACE_VIEW_BUILDER_QUERY_TYPE,
|
||||
TRACE_VIEW_COLUMN_EXTRA_FIELDS,
|
||||
TRACE_VIEW_DEFAULT_ORDER_BY,
|
||||
TRACE_VIEW_FIELD_KEYS,
|
||||
TRACE_VIEW_ORDER_BY_EXTRA_FIELDS,
|
||||
} from '../constants';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { PER_PAGE_OPTIONS } from './configs';
|
||||
import { useTraceViewColumns } from './useTraceViewColumns';
|
||||
import styles from './TracesView.module.scss';
|
||||
|
||||
interface TracesViewProps {
|
||||
@@ -49,6 +59,16 @@ function TracesView({
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>(TRACE_VIEW_DEFAULT_ORDER_BY);
|
||||
|
||||
const {
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
requiredFields,
|
||||
isLoading: isColumnsLoading,
|
||||
} = useTraceViewColumns();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
@@ -60,8 +80,8 @@ function TracesView({
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType),
|
||||
[stagedQuery],
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
@@ -73,6 +93,7 @@ function TracesView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
@@ -81,6 +102,7 @@ function TracesView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -142,27 +164,43 @@ function TracesView({
|
||||
}
|
||||
}, [isLoading, isFetching, isError, rows.length]);
|
||||
|
||||
const handleOrderChange = useCallback((value: string): void => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
|
||||
const fieldsSelectorConfig = useMemo(
|
||||
() => ({ fieldsSelector: { value: selectedFields, onFieldsChange } }),
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.actionsContainer}>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
|
||||
<div className="trace-explorer-controls">
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
panelType={PANEL_TYPES.TRACE}
|
||||
/>
|
||||
<div className={styles.orderByContainer}>
|
||||
<div className={styles.orderByLabel}>
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={handleOrderChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
fieldKeysConfig={TRACE_VIEW_FIELD_KEYS}
|
||||
builderQueryType={TRACE_VIEW_BUILDER_QUERY_TYPE}
|
||||
extraFields={TRACE_VIEW_ORDER_BY_EXTRA_FIELDS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={rows.length}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
config={fieldsSelectorConfig}
|
||||
fieldKeysConfig={TRACE_VIEW_FIELD_KEYS}
|
||||
builderQueryType={TRACE_VIEW_BUILDER_QUERY_TYPE}
|
||||
extraFields={TRACE_VIEW_COLUMN_EXTRA_FIELDS}
|
||||
requiredFields={requiredFields}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,10 +208,11 @@ function TracesView({
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS}
|
||||
respectColumnOrder
|
||||
panelType="TRACE"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoading || isColumnsLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
error={error}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useColumnStore } from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { useTraceViewColumns } from '../useTraceViewColumns';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
const AGGREGATE_KEYS = [
|
||||
'llm_call_count',
|
||||
'tool_call_count',
|
||||
'distinct_tool_count',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
'max_llm_duration_nano',
|
||||
];
|
||||
|
||||
const fieldNames = (fields: TelemetryFieldKey[]): string[] =>
|
||||
fields.map((field) => field.name);
|
||||
|
||||
const columnNames = (columns: { header?: unknown }[]): string[] =>
|
||||
columns.map((column) => column.header as string);
|
||||
|
||||
function wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const seenAI: URLSearchParams[] = [];
|
||||
|
||||
const mockAggregateKeys = (names: string[]): void => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${ENVIRONMENT.baseURL}/api/v1/ai_observability/fields/keys`,
|
||||
(req, res, ctx) => {
|
||||
seenAI.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: Object.fromEntries(
|
||||
names.map((name) => [
|
||||
name,
|
||||
[
|
||||
{
|
||||
name,
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
fieldDataType: TelemetrytypesFieldDataTypeDTO.float64,
|
||||
},
|
||||
],
|
||||
]),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const renderColumns = async (): Promise<
|
||||
ReturnType<typeof renderHook<ReturnType<typeof useTraceViewColumns>, unknown>>
|
||||
> => {
|
||||
const rendered = renderHook(() => useTraceViewColumns(), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(rendered.result.current.isLoading).toBe(false);
|
||||
});
|
||||
return rendered;
|
||||
};
|
||||
|
||||
describe('useTraceViewColumns', () => {
|
||||
beforeEach(() => {
|
||||
seenAI.length = 0;
|
||||
useColumnStore.getState().tables = {};
|
||||
localStorage.clear();
|
||||
mockAggregateKeys(AGGREGATE_KEYS);
|
||||
});
|
||||
|
||||
it('reads the aggregates from the trace context of the keys endpoint', async () => {
|
||||
await renderColumns();
|
||||
|
||||
expect(seenAI).toHaveLength(1);
|
||||
expect(seenAI[0]?.get('searchText')).toBe('');
|
||||
expect(seenAI[0]?.get('fieldContext')).toBe(
|
||||
TelemetrytypesFieldContextDTO.trace,
|
||||
);
|
||||
});
|
||||
|
||||
it('pools the hardcoded display-only columns with the endpoint aggregates', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(columnNames(result.current.columns)).toStrictEqual([
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'trace_id',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
...AGGREGATE_KEYS,
|
||||
]);
|
||||
});
|
||||
|
||||
it('selects only the default-visible columns on first render', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'trace_id',
|
||||
'llm_call_count',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a newly reported aggregate hidden until it is picked', async () => {
|
||||
mockAggregateKeys(['brand_new_aggregate']);
|
||||
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(columnNames(result.current.columns)).toContain('brand_new_aggregate');
|
||||
expect(fieldNames(result.current.selectedFields)).not.toContain(
|
||||
'brand_new_aggregate',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides the columns dropped from the selection', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'total_tokens', fieldContext: 'trace', fieldDataType: 'float64' },
|
||||
]);
|
||||
});
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'trace_id',
|
||||
'total_tokens',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows a column added back from the pool', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([{ name: 'trace_id' }]);
|
||||
});
|
||||
act(() => {
|
||||
result.current.onFieldsChange([{ name: 'trace_id' }, { name: 'input' }]);
|
||||
});
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'trace_id',
|
||||
'input',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the trace id column even when the selection drops it', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([{ name: 'span_count' }]);
|
||||
});
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toContain('trace_id');
|
||||
expect(result.current.requiredFields).toStrictEqual(['trace_id']);
|
||||
});
|
||||
|
||||
it('persists the selection order', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([
|
||||
{ name: 'total_tokens', fieldContext: 'trace', fieldDataType: 'float64' },
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
]);
|
||||
});
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'total_tokens',
|
||||
'trace_id',
|
||||
'service.name',
|
||||
]);
|
||||
expect(
|
||||
useColumnStore.getState().tables[STORAGE_KEY].columnOrder,
|
||||
).toStrictEqual([
|
||||
'trace:total_tokens:float64',
|
||||
'trace_id',
|
||||
'resource:service.name',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -5,18 +5,29 @@ import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
|
||||
const TRACE_FIELDS = [
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'name' },
|
||||
{ name: 'duration_nano' },
|
||||
{ name: 'span_count' },
|
||||
{ name: 'trace_id' },
|
||||
] as TelemetryFieldKey[];
|
||||
/** Always visible: it is the row's link to the trace. */
|
||||
export const TRACE_ID_COLUMN_ID = 'trace_id';
|
||||
|
||||
export const columns: TableColumnDef<TracesTableRow>[] = TRACE_FIELDS.map(
|
||||
(field) => ({
|
||||
/** Everything else starts hidden, including any aggregate the endpoint adds later. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set([
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'llm_call_count',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
TRACE_ID_COLUMN_ID,
|
||||
]);
|
||||
|
||||
export const buildTraceViewColumns = (
|
||||
fields: TelemetryFieldKey[],
|
||||
): TableColumnDef<TracesTableRow>[] =>
|
||||
fields.map((field) => ({
|
||||
...getFieldColumn(field),
|
||||
enableRemove: false,
|
||||
canBeHidden: false,
|
||||
}),
|
||||
);
|
||||
defaultVisibility: DEFAULT_VISIBLE_FIELDS.has(field.name),
|
||||
// The shared column builder pins anything in TIMESTAMP_FIELD_NAMES; these stay movable.
|
||||
enableMove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
enableRemove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
canBeHidden: field.name !== TRACE_ID_COLUMN_ID,
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { mergeExtraFields } from 'utils/extraFields';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import {
|
||||
hideColumn,
|
||||
initializeFromDefaults,
|
||||
setColumnOrder,
|
||||
showColumn,
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
TRACE_VIEW_BUILDER_QUERY_TYPE,
|
||||
TRACE_VIEW_COLUMN_EXTRA_FIELDS,
|
||||
TRACE_VIEW_FIELD_KEYS,
|
||||
} from '../constants';
|
||||
import { buildTraceViewColumns, TRACE_ID_COLUMN_ID } from './configs';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
/** Matches the id getFieldColumn derives, so fields and columns address alike. */
|
||||
const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
interface UseTraceViewColumns {
|
||||
columns: TableColumnDef<TracesTableRow>[];
|
||||
selectedFields: TelemetryFieldKey[];
|
||||
onFieldsChange: (next: TelemetryFieldKey[]) => void;
|
||||
requiredFields: readonly string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// TODO(ai-explorer): browser-local only, unlike the list views' `?options=` columns.
|
||||
export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
const { data: fetchedFields = [], isFetched } = useFieldKeysSuggestion(
|
||||
{
|
||||
...TRACE_VIEW_FIELD_KEYS,
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.TRACES],
|
||||
searchText: '',
|
||||
},
|
||||
TRACE_VIEW_BUILDER_QUERY_TYPE,
|
||||
);
|
||||
|
||||
const availableFields = useMemo(
|
||||
() => mergeExtraFields(TRACE_VIEW_COLUMN_EXTRA_FIELDS, fetchedFields),
|
||||
[fetchedFields],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(availableFields),
|
||||
[availableFields],
|
||||
);
|
||||
|
||||
// Defaults from a partial column set would persist as the user's own choice.
|
||||
useEffect(() => {
|
||||
if (isFetched) {
|
||||
initializeFromDefaults(STORAGE_KEY, columns);
|
||||
}
|
||||
}, [isFetched, columns]);
|
||||
|
||||
const hiddenColumnIds = useHiddenColumnIds(STORAGE_KEY);
|
||||
const columnOrder = useColumnOrder(STORAGE_KEY);
|
||||
|
||||
const selectedFields = useMemo(() => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
|
||||
return availableFields
|
||||
.filter((field) => !hidden.has(columnIdOf(field)))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
);
|
||||
}, [availableFields, hiddenColumnIds, columnOrder]);
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(next: TelemetryFieldKey[]): void => {
|
||||
const keptIds = new Set(next.map(columnIdOf));
|
||||
|
||||
columns.forEach((column) => {
|
||||
if (keptIds.has(column.id) || column.id === TRACE_ID_COLUMN_ID) {
|
||||
showColumn(STORAGE_KEY, column.id);
|
||||
} else {
|
||||
hideColumn(STORAGE_KEY, column.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Columns missing from the order sort last, so the visible ones suffice.
|
||||
setColumnOrder(STORAGE_KEY, next.map(columnIdOf));
|
||||
},
|
||||
[columns],
|
||||
);
|
||||
|
||||
return {
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
requiredFields: [TRACE_ID_COLUMN_ID],
|
||||
isLoading: !isFetched,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
@@ -34,3 +37,29 @@ export const TOOLBAR_VIEWS = {
|
||||
key: 'clickhouse',
|
||||
},
|
||||
};
|
||||
|
||||
export const TRACE_VIEW_DEFAULT_ORDER_BY = 'last_activity_time:desc';
|
||||
|
||||
/** Display-only: ordering or filtering on one is an error, so the keys endpoint omits them. */
|
||||
export const TRACE_VIEW_COLUMN_EXTRA_FIELDS: TelemetryFieldKey[] = [
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'root_span_name' },
|
||||
{ name: 'trace_duration_nano' },
|
||||
{ name: 'span_count' },
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'start_time' },
|
||||
{ name: 'end_time' },
|
||||
{ name: 'error_count' },
|
||||
{ name: 'input' },
|
||||
{ name: 'output' },
|
||||
] as TelemetryFieldKey[];
|
||||
|
||||
export const TRACE_VIEW_FIELD_KEYS = {
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
} as const;
|
||||
|
||||
export const TRACE_VIEW_BUILDER_QUERY_TYPE = 'builder_ai_query' as const;
|
||||
|
||||
export const TRACE_VIEW_ORDER_BY_EXTRA_FIELDS: TelemetryFieldKey[] = [
|
||||
{ name: 'last_activity_time' } as TelemetryFieldKey,
|
||||
];
|
||||
|
||||
@@ -92,17 +92,25 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
data: { keys: {} },
|
||||
},
|
||||
jest.mock('api/querySuggestions/getFieldKeySuggestions', () => ({
|
||||
getFieldKeySuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: {} },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { values: { stringValues: [], numberValues: [] } } },
|
||||
jest.mock('api/querySuggestions/getFieldValueSuggestions', () => ({
|
||||
getFieldValueSuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
values: {
|
||||
stringValues: [],
|
||||
numberValues: [],
|
||||
boolValues: [],
|
||||
relatedValues: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -179,7 +179,10 @@ const setupServer = (capturedPayloads: QueryRangePayloadV5[]): void => {
|
||||
),
|
||||
// Add handler for the fields endpoint that's causing warnings
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, async (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json([])),
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { complete: true, keys: {} } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { QueryClient } from 'react-query';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig, FieldKeysResponse } from 'api/querySuggestions/types';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getFieldKeysQueryOptions,
|
||||
toFieldKeys,
|
||||
} from '../useFieldKeysSuggestion';
|
||||
|
||||
/** Drives the options object the way react-query does, without a client. */
|
||||
const fetchKeys = async (
|
||||
fieldKeysConfig: FieldKeysConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
): Promise<TelemetryFieldKey[]> => {
|
||||
const { queryFn, select } = getFieldKeysQueryOptions(
|
||||
fieldKeysConfig,
|
||||
builderQueryType,
|
||||
);
|
||||
const response = await (
|
||||
queryFn as (context: { signal: AbortSignal }) => Promise<FieldKeysResponse>
|
||||
)({ signal: new AbortController().signal });
|
||||
|
||||
return select?.(response) ?? [];
|
||||
};
|
||||
|
||||
const mockKeys = (
|
||||
path: '/api/v1/ai_observability/fields/keys' | '/api/v1/fields/keys',
|
||||
names: string[],
|
||||
onRequest?: (params: URLSearchParams) => void,
|
||||
): void => {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}${path}`, (req, res, ctx) => {
|
||||
onRequest?.(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: Object.fromEntries(names.map((name) => [name, [{ name }]])),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
describe('useFieldKeysSuggestion', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
const seen: URLSearchParams[] = [];
|
||||
mockKeys(
|
||||
'/api/v1/ai_observability/fields/keys',
|
||||
['total_tokens'],
|
||||
(params) => {
|
||||
seen.push(params);
|
||||
},
|
||||
);
|
||||
|
||||
const keys = await fetchKeys(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.TRACES],
|
||||
searchText: 'llm',
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
},
|
||||
'builder_ai_query',
|
||||
);
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0]?.get('searchText')).toBe('llm');
|
||||
expect(seen[0]?.get('fieldContext')).toBe(
|
||||
TelemetrytypesFieldContextDTO.trace,
|
||||
);
|
||||
expect(keys.map((key) => key.name)).toStrictEqual(['total_tokens']);
|
||||
});
|
||||
|
||||
it('reads the generic endpoint for an unmarked query', async () => {
|
||||
const seen: URLSearchParams[] = [];
|
||||
mockKeys('/api/v1/fields/keys', ['service.name'], (params) => {
|
||||
seen.push(params);
|
||||
});
|
||||
|
||||
const keys = await fetchKeys({
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.TRACES],
|
||||
searchText: 'svc',
|
||||
});
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0]?.get('signal')).toBe(DataSource.TRACES);
|
||||
expect(seen[0]?.get('searchText')).toBe('svc');
|
||||
expect(keys.map((key) => key.name)).toStrictEqual(['service.name']);
|
||||
});
|
||||
|
||||
it('reads the trace context of the ai_observability endpoint', async () => {
|
||||
const seen: URLSearchParams[] = [];
|
||||
mockKeys(
|
||||
'/api/v1/ai_observability/fields/keys',
|
||||
['total_tokens'],
|
||||
(params) => {
|
||||
seen.push(params);
|
||||
},
|
||||
);
|
||||
|
||||
const keys = await fetchKeys(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.TRACES],
|
||||
searchText: '',
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
},
|
||||
'builder_ai_query',
|
||||
);
|
||||
|
||||
expect(seen[0]?.get('searchText')).toBe('');
|
||||
expect(keys.map((key) => key.name)).toStrictEqual(['total_tokens']);
|
||||
});
|
||||
|
||||
it('reuses the cached keys response for a second empty search', async () => {
|
||||
const seen: URLSearchParams[] = [];
|
||||
mockKeys(
|
||||
'/api/v1/ai_observability/fields/keys',
|
||||
['total_tokens'],
|
||||
(params) => {
|
||||
seen.push(params);
|
||||
},
|
||||
);
|
||||
|
||||
const fieldKeysConfig = {
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.TRACES],
|
||||
searchText: '',
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
};
|
||||
|
||||
// Built twice: equal keys must resolve to one cache entry, not two requests.
|
||||
await queryClient.fetchQuery(
|
||||
getFieldKeysQueryOptions(fieldKeysConfig, 'builder_ai_query'),
|
||||
);
|
||||
await queryClient.fetchQuery(
|
||||
getFieldKeysQueryOptions(fieldKeysConfig, 'builder_ai_query'),
|
||||
);
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('hands the query signal to the fetcher so a superseded search aborts', async () => {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_req, res, ctx) =>
|
||||
res(ctx.delay(500), ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
const { queryFn } = getFieldKeysQueryOptions({
|
||||
signal: DATA_SOURCE_TO_SIGNAL[DataSource.LOGS],
|
||||
searchText: 'svc',
|
||||
});
|
||||
const pending = (
|
||||
queryFn as (context: { signal: AbortSignal }) => Promise<unknown>
|
||||
)({ signal: controller.signal });
|
||||
|
||||
controller.abort();
|
||||
|
||||
await expect(pending).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it('treats a null keys map as empty', () => {
|
||||
expect(
|
||||
toFieldKeys({
|
||||
status: 'success',
|
||||
data: { complete: false, keys: null },
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
QueryKey,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { getFieldKeySuggestions } from 'api/querySuggestions/getFieldKeySuggestions';
|
||||
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { FieldKeysConfig, FieldKeysResponse } from 'api/querySuggestions/types';
|
||||
import { BuilderQueryType, TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
export type FieldKeysQueryOptions = UseQueryOptions<
|
||||
FieldKeysResponse,
|
||||
ErrorType<RenderErrorResponseDTO>,
|
||||
TelemetryFieldKey[]
|
||||
> & { queryKey: QueryKey };
|
||||
|
||||
export const toFieldKeys = (
|
||||
res: FieldKeysResponse | undefined,
|
||||
): TelemetryFieldKey[] =>
|
||||
Object.values(res?.data?.keys ?? {})
|
||||
.flat()
|
||||
.map((key) => ({ ...key }) as TelemetryFieldKey);
|
||||
|
||||
export const getFieldKeysQueryOptions = (
|
||||
fieldKeysConfig: FieldKeysConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
): FieldKeysQueryOptions => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.FIELD_KEYS_SUGGESTION,
|
||||
builderQueryType,
|
||||
fieldKeysConfig,
|
||||
],
|
||||
queryFn: ({ signal }): Promise<FieldKeysResponse> =>
|
||||
getFieldKeySuggestions(fieldKeysConfig, builderQueryType, signal),
|
||||
select: toFieldKeys,
|
||||
staleTime: FIELD_API_CACHE_TIME,
|
||||
cacheTime: FIELD_API_CACHE_TIME,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
export const useFieldKeysSuggestion = (
|
||||
fieldKeysConfig: FieldKeysConfig,
|
||||
builderQueryType?: BuilderQueryType,
|
||||
): UseQueryResult<TelemetryFieldKey[], ErrorType<RenderErrorResponseDTO>> =>
|
||||
useQuery(getFieldKeysQueryOptions(fieldKeysConfig, builderQueryType));
|
||||
@@ -52,14 +52,24 @@ jest.mock('hooks/useSafeNavigate', () =>
|
||||
.createBrowserHistorySafeNavigateMock(),
|
||||
);
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { data: { keys: {} } } }),
|
||||
jest.mock('api/querySuggestions/getFieldKeySuggestions', () => ({
|
||||
getFieldKeySuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: {} },
|
||||
}),
|
||||
}));
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { values: { stringValues: [], numberValues: [] } } },
|
||||
jest.mock('api/querySuggestions/getFieldValueSuggestions', () => ({
|
||||
getFieldValueSuggestions: jest.fn().mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
values: {
|
||||
stringValues: [],
|
||||
numberValues: [],
|
||||
boolValues: [],
|
||||
relatedValues: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Format } from 'constants/formats/types';
|
||||
@@ -22,6 +23,15 @@ export enum DataSource {
|
||||
LOGS = 'logs',
|
||||
}
|
||||
|
||||
export const DATA_SOURCE_TO_SIGNAL: Record<
|
||||
DataSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
|
||||
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
|
||||
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
|
||||
};
|
||||
|
||||
export enum StringOperators {
|
||||
NOOP = 'noop',
|
||||
COUNT = 'count',
|
||||
|
||||
44
frontend/src/utils/__tests__/extraFields.test.ts
Normal file
44
frontend/src/utils/__tests__/extraFields.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { mergeExtraFields } from '../extraFields';
|
||||
|
||||
describe('mergeExtraFields', () => {
|
||||
it('drops fetched keys that share a composite key with an extra field', () => {
|
||||
expect(
|
||||
mergeExtraFields(
|
||||
[{ name: 'trace_id' } as TelemetryFieldKey],
|
||||
[
|
||||
{ name: 'trace_id' } as TelemetryFieldKey,
|
||||
{ name: 'total_tokens' } as TelemetryFieldKey,
|
||||
],
|
||||
).map((key) => key.name),
|
||||
).toStrictEqual(['trace_id', 'total_tokens']);
|
||||
});
|
||||
|
||||
it('keeps extra and fetched keys that share a name but differ in context', () => {
|
||||
expect(
|
||||
mergeExtraFields(
|
||||
[{ name: 'service.name', fieldContext: 'resource' } as TelemetryFieldKey],
|
||||
[
|
||||
{
|
||||
name: 'service.name',
|
||||
fieldContext: 'attribute',
|
||||
} as TelemetryFieldKey,
|
||||
{ name: 'total_tokens' } as TelemetryFieldKey,
|
||||
],
|
||||
),
|
||||
).toStrictEqual([
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'service.name', fieldContext: 'attribute' },
|
||||
{ name: 'total_tokens' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the fetched keys when there are no extra fields', () => {
|
||||
expect(
|
||||
mergeExtraFields(undefined, [
|
||||
{ name: 'total_tokens' } as TelemetryFieldKey,
|
||||
]).map((key) => key.name),
|
||||
).toStrictEqual(['total_tokens']);
|
||||
});
|
||||
});
|
||||
23
frontend/src/utils/extraFields.ts
Normal file
23
frontend/src/utils/extraFields.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
export const mergeExtraFields = (
|
||||
extra: TelemetryFieldKey[] = [],
|
||||
fetched: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] => {
|
||||
const extraKeys = new Set(
|
||||
extra.map((field) =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType),
|
||||
),
|
||||
);
|
||||
|
||||
return [
|
||||
...extra,
|
||||
...fetched.filter(
|
||||
(field) =>
|
||||
!extraKeys.has(
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType),
|
||||
),
|
||||
),
|
||||
];
|
||||
};
|
||||
@@ -82,6 +82,10 @@ type Module interface {
|
||||
|
||||
DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
// DeleteUnsafeV2 deletes a v2 dashboard and its related state without applying deletion guards.
|
||||
// Intended for internal system callers.
|
||||
DeleteUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error
|
||||
|
||||
// get the v2 dashboard data by public dashboard id
|
||||
|
||||
@@ -294,6 +294,15 @@ func (module *module) DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer
|
||||
return err
|
||||
}
|
||||
|
||||
return module.deleteV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
// DeleteUnsafeV2 deletes a v2 dashboard bypassing the guards. Intended for internal system callers.
|
||||
func (module *module) DeleteUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.deleteV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) deleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
// Syncing to an empty tag set drops every tag link for the dashboard.
|
||||
if _, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, nil); err != nil {
|
||||
|
||||
@@ -566,17 +566,39 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// flattenJSONPaths flattens a decoded JSON document into dotted keys, overwriting existing keys in out.
|
||||
func flattenJSONPaths(prefix string, m map[string]any, out map[string]any) {
|
||||
for k, v := range m {
|
||||
key := k
|
||||
if prefix != "" {
|
||||
key = prefix + "." + k
|
||||
}
|
||||
switch child := v.(type) {
|
||||
case map[string]any:
|
||||
flattenJSONPaths(key, child, out)
|
||||
case telemetrystoretypes.JSONValue:
|
||||
flattenJSONPaths(key, child, out)
|
||||
default:
|
||||
out[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeSpanAttributeColumns merges (attributes_string, attributes_number, attributes_bool, resources_string) into
|
||||
// unified "attributes" and "resource" keys, and parses the stringified `events`
|
||||
// and `links` columns into structured slices. Raw DB columns are removed.
|
||||
//
|
||||
// The `attributes` JSON column is flattened in first and the legacy maps merged over it, so maps win on collision.
|
||||
func mergeSpanAttributeColumns(data map[string]any) {
|
||||
attrStr, hasStr := data["attributes_string"]
|
||||
attrNum, hasNum := data["attributes_number"]
|
||||
attrBool, hasBool := data["attributes_bool"]
|
||||
attrJSON, _ := data["attributes"].(telemetrystoretypes.JSONValue)
|
||||
// todo(nitya): move to resource json
|
||||
resStr, hasRes := data["resources_string"]
|
||||
if hasStr || hasNum || hasBool || hasRes {
|
||||
if hasStr || hasNum || hasBool || attrJSON != nil || hasRes {
|
||||
attributes := make(map[string]any)
|
||||
flattenJSONPaths("", attrJSON, attributes)
|
||||
if m, ok := attrStr.(map[string]string); ok {
|
||||
for k, v := range m {
|
||||
attributes[k] = v
|
||||
|
||||
@@ -195,3 +195,115 @@ func TestMergeSpanAttributeColumns_EmptyEventsAndLinks(t *testing.T) {
|
||||
t.Fatalf("expected empty []spantypes.Link, got %#v", data["links"])
|
||||
}
|
||||
}
|
||||
|
||||
// Arrays stay native leaves: the collector stringifies top-level arrays and explodes nested ones
|
||||
// into indexed keys in the legacy maps; the JSON home keeps them whole and we do not mimic either.
|
||||
func TestMergeSpanAttributeColumns_JSONColumn(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data map[string]any
|
||||
want map[string]any
|
||||
}{
|
||||
{
|
||||
name: "JSONOnly_FlattensNestedPaths_PreservesTypes",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{
|
||||
"http": map[string]any{"route": "/api/pay", "retry": map[string]any{"count": float64(3)}},
|
||||
"cache.hit": true,
|
||||
},
|
||||
},
|
||||
want: map[string]any{"http.route": "/api/pay", "http.retry.count": float64(3), "cache.hit": true},
|
||||
},
|
||||
{
|
||||
name: "Straddle_MapsWinOnCollision_JSONFillsGaps",
|
||||
data: map[string]any{
|
||||
"attributes_string": map[string]string{"http.route": "/old", "only.map": "m"},
|
||||
"attributes_number": map[string]float64{"http.status": 500},
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": map[string]any{"route": "/new"}, "only.json": "j"},
|
||||
},
|
||||
want: map[string]any{"http.route": "/old", "only.map": "m", "http.status": float64(500), "only.json": "j"},
|
||||
},
|
||||
{
|
||||
name: "MapOnly_EmptyJSONDoc_KeepsMapValues",
|
||||
data: map[string]any{
|
||||
"attributes_string": map[string]string{"http.route": "/map"},
|
||||
"attributes_number": map[string]float64{"http.status": 200},
|
||||
"attributes_bool": map[string]bool{"cache.hit": true},
|
||||
"attributes": telemetrystoretypes.JSONValue{},
|
||||
},
|
||||
want: map[string]any{"http.route": "/map", "http.status": float64(200), "cache.hit": true},
|
||||
},
|
||||
{
|
||||
name: "MapOnly_NilJSON_BehavesAsAbsent",
|
||||
data: map[string]any{
|
||||
"attributes_string": map[string]string{"http.route": "/map"},
|
||||
"attributes": telemetrystoretypes.JSONValue(nil),
|
||||
},
|
||||
want: map[string]any{"http.route": "/map"},
|
||||
},
|
||||
{
|
||||
name: "Arrays_StayLeafValues",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": map[string]any{"tags": []any{"a", "b"}, "codes": []any{float64(1), float64(2)}}},
|
||||
},
|
||||
want: map[string]any{"http.tags": []any{"a", "b"}, "http.codes": []any{float64(1), float64(2)}},
|
||||
},
|
||||
{
|
||||
name: "TopLevelArrayOfMaps_StaysNativeLeaf",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"key": []any{map[string]any{"a": float64(1)}, map[string]any{"b": float64(2)}}},
|
||||
},
|
||||
want: map[string]any{"key": []any{map[string]any{"a": float64(1)}, map[string]any{"b": float64(2)}}},
|
||||
},
|
||||
{
|
||||
name: "NestedArrayOfMaps_StaysNativeLeaf_NoIndexPaths",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": map[string]any{"items": []any{map[string]any{"a": float64(1)}}}},
|
||||
},
|
||||
want: map[string]any{"http.items": []any{map[string]any{"a": float64(1)}}},
|
||||
},
|
||||
{
|
||||
name: "DualWritten_NestedArray_IndexKeysAndJSONArrayCoexist",
|
||||
data: map[string]any{
|
||||
"attributes_number": map[string]float64{"http.items.0.a": 1},
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": map[string]any{"items": []any{map[string]any{"a": float64(1)}}}},
|
||||
},
|
||||
want: map[string]any{"http.items.0.a": float64(1), "http.items": []any{map[string]any{"a": float64(1)}}},
|
||||
},
|
||||
{
|
||||
name: "JSONNull_KeptAsNil",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"k": nil},
|
||||
},
|
||||
want: map[string]any{"k": nil},
|
||||
},
|
||||
{
|
||||
name: "KeyIsLeafValue_NotFlattened",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": "plaintext"},
|
||||
},
|
||||
want: map[string]any{"http": "plaintext"},
|
||||
},
|
||||
{
|
||||
name: "KeyIsParent_FlattensToDottedPath",
|
||||
data: map[string]any{
|
||||
"attributes": telemetrystoretypes.JSONValue{"http": map[string]any{"route": "/a"}},
|
||||
},
|
||||
want: map[string]any{"http.route": "/a"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
mergeSpanAttributeColumns(testCase.data)
|
||||
|
||||
attrs, ok := testCase.data["attributes"].(map[string]any)
|
||||
require.True(t, ok, "attributes should be map[string]any, got %T", testCase.data["attributes"])
|
||||
assert.Equal(t, testCase.want, attrs)
|
||||
for _, removed := range []string{"attributes_string", "attributes_number", "attributes_bool"} {
|
||||
_, present := testCase.data[removed]
|
||||
assert.False(t, present, "%s should be removed", removed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ func (m *Manager) deprovisionDashboards(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.dashboardModule.DeleteUnsafe(ctx, orgID, dashID); err != nil {
|
||||
if err := m.dashboardModule.DeleteUnsafeV2(ctx, orgID, dashID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ const (
|
||||
// ResolveLogicalFields picks which logical fields a filter term builds conditions
|
||||
// for. With 0 or 1 field it returns the input unchanged and no warning. When a
|
||||
// name is ambiguous (several logical fields — a family is one field and never
|
||||
// ambiguous with itself) it returns a warning; a resource+attribute mix defaults
|
||||
// to the resource fields (the common intent), noted in the warning.
|
||||
// ambiguous with itself) it returns a warning; a resource + other-context mix
|
||||
// (attribute, body, scope, …) defaults to the resource fields (the common
|
||||
// intent), noted in the warning.
|
||||
func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields []*telemetrytypes.LogicalField) ([]*telemetrytypes.LogicalField, string) {
|
||||
if len(logicalFields) <= 1 {
|
||||
return logicalFields, ""
|
||||
@@ -39,18 +40,17 @@ func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields
|
||||
logicalFields,
|
||||
)
|
||||
|
||||
hasResource, hasAttribute := false, false
|
||||
hasResource, hasOther := false, false
|
||||
for _, item := range logicalFields {
|
||||
switch item.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
if item.FieldContext == telemetrytypes.FieldContextResource {
|
||||
hasResource = true
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
hasAttribute = true
|
||||
} else {
|
||||
hasOther = true
|
||||
}
|
||||
}
|
||||
|
||||
// when there is both resource and attribute context, default to resource only
|
||||
if hasResource && hasAttribute {
|
||||
// with resource and any other context, default to resource only
|
||||
if hasResource && hasOther {
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
|
||||
for _, item := range logicalFields {
|
||||
if item.FieldContext == telemetrytypes.FieldContextResource {
|
||||
@@ -58,8 +58,8 @@ func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields
|
||||
}
|
||||
}
|
||||
logicalFields = filtered
|
||||
warning += " " + "Using `resource` context by default. To query attributes explicitly, " +
|
||||
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", field.Name)
|
||||
warning += " " + "Using `resource` context by default. To query another context explicitly, " +
|
||||
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s' or 'body.%s')", field.Name, field.Name)
|
||||
}
|
||||
|
||||
return logicalFields, warning
|
||||
|
||||
@@ -175,6 +175,42 @@ func TestResolveLogicalFieldsKeepsFamilyThroughAmbiguity(t *testing.T) {
|
||||
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, memberNames(resolved[0]))
|
||||
}
|
||||
|
||||
// Resource wins over every other context, not just attribute: a bare key that
|
||||
// also lives in body or scope must collapse to resource alone, so the surviving
|
||||
// candidate does not AND against the resource fingerprint CTE.
|
||||
func TestResolveLogicalFieldsResourceWinsOverOtherContexts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
other telemetrytypes.FieldContext
|
||||
}{
|
||||
{name: "ResourceOverBody", other: telemetrytypes.FieldContextBody},
|
||||
{name: "ResourceOverScope", other: telemetrytypes.FieldContextScope},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
requested := &telemetrytypes.TelemetryFieldKey{Name: "service.name"}
|
||||
fields := []*telemetrytypes.LogicalField{
|
||||
telemetrytypes.SingleLogicalField("service.name", &telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}),
|
||||
telemetrytypes.SingleLogicalField("service.name", &telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
FieldContext: testCase.other,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}),
|
||||
}
|
||||
|
||||
resolved, warning := ResolveLogicalFields(requested, fields)
|
||||
assert.NotEmpty(t, warning)
|
||||
require.Len(t, resolved, 1)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, resolved[0].FieldContext)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Members of a family with different data types never merge: the identity
|
||||
// (signal, context, data type) separates them into distinct logical fields.
|
||||
func TestMatchingLogicalFieldsNeverMergesAcrossDataTypes(t *testing.T) {
|
||||
|
||||
@@ -45,7 +45,7 @@ SELECT timestamp AS __SELECT_KEY_0_timestamp, trace_id AS __SELECT_KEY_1_trace_i
|
||||
http_url AS __SELECT_KEY_17_http_url, external_http_method AS __SELECT_KEY_18_external_http_method,
|
||||
http_method AS __SELECT_KEY_19_http_method, http_host AS __SELECT_KEY_20_http_host, db_name AS __SELECT_KEY_21_db_name,
|
||||
db_operation AS __SELECT_KEY_22_db_operation, has_error AS __SELECT_KEY_23_has_error, is_remote AS __SELECT_KEY_24_is_remote,
|
||||
attributes_string, attributes_number, attributes_bool, resources_string
|
||||
attributes_string, attributes_number, attributes_bool, resources_string, attributes
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE trace_id GLOBAL IN (SELECT trace_id FROM __trace_scope)
|
||||
AND (((mapContains(attributes_string, 'gen_ai.request.model')
|
||||
|
||||
@@ -676,7 +676,7 @@ SELECT timestamp AS __SELECT_KEY_0_timestamp, trace_id AS __SELECT_KEY_1_trace_i
|
||||
http_url AS __SELECT_KEY_17_http_url, external_http_method AS __SELECT_KEY_18_external_http_method,
|
||||
http_method AS __SELECT_KEY_19_http_method, http_host AS __SELECT_KEY_20_http_host, db_name AS __SELECT_KEY_21_db_name,
|
||||
db_operation AS __SELECT_KEY_22_db_operation, has_error AS __SELECT_KEY_23_has_error, is_remote AS __SELECT_KEY_24_is_remote,
|
||||
attributes_string, attributes_number, attributes_bool, resources_string
|
||||
attributes_string, attributes_number, attributes_bool, resources_string, attributes
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE (((mapContains(attributes_string, 'gen_ai.request.model')
|
||||
OR mapContains(attributes_string, 'gen_ai.tool.name')
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package logsstatementbuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/statementbuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// A key present in both resource and body contexts must filter on resource only.
|
||||
// The resource condition builds the fingerprint CTE, so a surviving body condition
|
||||
// would AND against it and match almost nothing (engineering-pod#6086).
|
||||
func TestStatementBuilderResourceBodyConflict(t *testing.T) {
|
||||
store := telemetrytypestest.NewMockMetadataStore()
|
||||
store.SetStaticFields(logstelemetryschema.IntrinsicFields)
|
||||
store.SetKey(&telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
})
|
||||
bodyKey := &telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
FieldContext: telemetrytypes.FieldContextBody,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}
|
||||
require.NoError(t, bodyKey.SetJSONAccessPlan(telemetrytypes.JSONColumnMetadata{
|
||||
BaseColumn: logstelemetryschema.LogsV2BodyV2Column,
|
||||
PromotedColumn: logstelemetryschema.LogsV2BodyPromotedColumn,
|
||||
}, map[string][]telemetrytypes.FieldDataType{"service.name": {telemetrytypes.FieldDataTypeString}}))
|
||||
store.SetKey(bodyKey)
|
||||
|
||||
fl := flaggertest.WithUseJSONBody(t, true)
|
||||
storage := logstelemetryschema.NewStorage()
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalLogs)
|
||||
statementBuilder := NewLogQueryStatementBuilder(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
store,
|
||||
storage,
|
||||
aggExprRewriter,
|
||||
logstelemetryschema.DefaultFullTextColumn,
|
||||
fl,
|
||||
nil,
|
||||
statementbuilder.Config{SkipResourceFingerprint: statementbuilder.SkipResourceFingerprint{Enabled: false, Threshold: 100000}},
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||
expected qbtypes.Statement
|
||||
}{
|
||||
{
|
||||
name: "AmbiguousKeyFiltersResourceOnly",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'webapp'"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"webapp", "%service.name%", "%service.name\":\"webapp%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of field context / data type: [name=service.name,context=resource,datatype=string name=service.name,context=body,datatype=string]. Using `resource` context by default. To query another context explicitly, use the fully qualified name (e.g., 'attribute.service.name' or 'body.service.name')",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
q, err := statementBuilder.Build(context.Background(), valuer.UUID{}, 1747947419000, 1747983448000, testCase.requestType, testCase.query, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.expected.Query, q.Query)
|
||||
require.Equal(t, testCase.expected.Args, q.Args)
|
||||
require.Equal(t, testCase.expected.Warnings, q.Warnings)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package tracesstatementbuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/tracestelemetryschema"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// jsonAttrColRe matches the bare `attributes` JSON column in the SELECT list, not the legacy maps.
|
||||
var jsonAttrColRe = regexp.MustCompile(`,\s*attributes\s*(,| FROM )`)
|
||||
|
||||
func newBulkTestBuilder(t *testing.T, releaseTime time.Time) *traceQueryStatementBuilder {
|
||||
t.Helper()
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
store := telemetrytypestest.NewMockMetadataStore()
|
||||
store.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
store.KeysMap["http.route"] = []*telemetrytypes.TelemetryFieldKey{{
|
||||
Name: "http.route",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
}}
|
||||
store.ColumnEvolutionMetadataMap["traces:attribute:__all__"] = tracestelemetryschema.MockAttributeEvolutionData(releaseTime)
|
||||
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
return NewTraceQueryStatementBuilder(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
store, storage, aggExprRewriter, nil, fl, false, 100000,
|
||||
)
|
||||
}
|
||||
|
||||
// TestListQuerySelectsAllAttributeHomes: every bag home is scanned in any window, with no evolution lookup.
|
||||
func TestListQuerySelectsAllAttributeHomes(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
rel := releaseTime.UnixMilli()
|
||||
day := int64(24 * time.Hour / time.Millisecond)
|
||||
|
||||
b := newBulkTestBuilder(t, releaseTime)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
startMs uint64
|
||||
endMs uint64
|
||||
}{
|
||||
{"BeforeRollout", uint64(rel - 2*day), uint64(rel - day)},
|
||||
{"AfterRollout", uint64(rel + day), uint64(rel + 2*day)},
|
||||
{"StraddlingRollout", uint64(rel - day), uint64(rel + day)},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stmt, err := b.Build(
|
||||
context.Background(), valuer.UUID{}, testCase.startMs, testCase.endMs,
|
||||
qbtypes.RequestTypeRaw,
|
||||
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{Signal: telemetrytypes.SignalTraces},
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
selectList := stmt.Query[:strings.Index(stmt.Query, " FROM ")]
|
||||
|
||||
assert.Regexp(t, jsonAttrColRe, stmt.Query, "json `attributes` column; select=%s", selectList)
|
||||
for _, col := range []string{"attributes_string", "attributes_number", "attributes_bool", "resources_string"} {
|
||||
assert.Contains(t, stmt.Query, col, "select=%s", selectList)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupByAttributeHomeAcrossRollout: the group-by home follows the window — legacy map
|
||||
// before the rollout, JSON with legacy fallback while straddling, JSON only after it.
|
||||
func TestGroupByAttributeHomeAcrossRollout(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
rel := releaseTime.UnixMilli()
|
||||
day := int64(24 * time.Hour / time.Millisecond)
|
||||
|
||||
b := newBulkTestBuilder(t, releaseTime)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
startMs uint64
|
||||
endMs uint64
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "BeforeRollout_ReadsLegacyMap",
|
||||
startMs: uint64(rel - 2*day),
|
||||
endMs: uint64(rel - day),
|
||||
wantContains: []string{"mapContains(attributes_string, 'http.route')", "attributes_string['http.route']"},
|
||||
wantNotContains: []string{"attributes.`http.route`"},
|
||||
},
|
||||
{
|
||||
name: "StraddlingRollout_JSONThenLegacyFallback",
|
||||
startMs: uint64(rel - day),
|
||||
endMs: uint64(rel + day),
|
||||
wantContains: []string{"attributes.`http.route` IS NOT NULL", "attributes.`http.route`::String", "attributes_string['http.route']"},
|
||||
wantNotContains: nil,
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_ReadsJSONOnly",
|
||||
startMs: uint64(rel + day),
|
||||
endMs: uint64(rel + 2*day),
|
||||
wantContains: []string{"attributes.`http.route`::String"},
|
||||
wantNotContains: []string{"attributes_string"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stmt, err := b.Build(
|
||||
context.Background(), valuer.UUID{}, testCase.startMs, testCase.endMs,
|
||||
qbtypes.RequestTypeTimeSeries,
|
||||
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.TraceAggregation{{Expression: "count()"}},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "http.route",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}}},
|
||||
Limit: 10,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
for _, want := range testCase.wantContains {
|
||||
assert.Contains(t, stmt.Query, want)
|
||||
}
|
||||
for _, unwanted := range testCase.wantNotContains {
|
||||
assert.NotContains(t, stmt.Query, unwanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -581,7 +581,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY multiIf(mapContains(attributes_string, 'user.id'), attributes_string['user.id'], NULL) desc LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY multiIf(mapContains(attributes_string, 'user.id'), attributes_string['user.id'], NULL) desc LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -837,7 +837,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -879,7 +879,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp asc LIMIT ?",
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp asc LIMIT ?",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -906,7 +906,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String <> '') AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String <> '') AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"1.0.0", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -161,7 +161,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -198,7 +198,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -235,7 +235,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -400,7 +400,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -434,7 +434,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -476,7 +476,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
|
||||
@@ -447,14 +447,14 @@ var (
|
||||
{Name: SpanIsRemoteColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
}
|
||||
|
||||
// ContextualSpanColumns lists the typed attribute and resource columns
|
||||
// selected raw (rather than via ColumnExpressionFor) so that consume.go
|
||||
// can merge them into unified "attributes" and "resource" maps.
|
||||
// ContextualSpanColumns lists the bag columns selected raw so consume.go can merge
|
||||
// them into unified "attributes" and "resource" maps, legacy maps winning on collision.
|
||||
ContextualSpanColumns = []string{
|
||||
SpanAttributesStringColumn,
|
||||
SpanAttributesNumberColumn,
|
||||
SpanAttributesBoolColumn,
|
||||
SpanResourcesStringColumn,
|
||||
SpanAttributesColumn,
|
||||
}
|
||||
|
||||
DefaultFields = map[string]telemetrytypes.TelemetryFieldKey{
|
||||
|
||||
7
tests/fixtures/traces.py
vendored
7
tests/fixtures/traces.py
vendored
@@ -331,7 +331,7 @@ class Traces(ABC):
|
||||
flags: np.uint32 = 0,
|
||||
scope: dict[str, Any] = {},
|
||||
resource_write_mode: Literal["legacy_only", "dual_write"] = "dual_write",
|
||||
attribute_write_mode: Literal["legacy_only", "dual_write"] = "dual_write",
|
||||
attribute_write_mode: Literal["legacy_only", "dual_write", "json_only"] = "dual_write",
|
||||
) -> None:
|
||||
if timestamp is None:
|
||||
timestamp = datetime.datetime.now()
|
||||
@@ -514,8 +514,11 @@ class Traces(ABC):
|
||||
|
||||
# Spans before the attribute JSON-evolution time populate only the legacy
|
||||
# attributes_{string,number,bool} maps; spans at or after it dual-write the
|
||||
# native-typed `attributes` JSON column too.
|
||||
# native-typed `attributes` JSON column too, and spans past the map-write
|
||||
# cutoff populate only the JSON column (metadata rows are still written).
|
||||
self.attributes_json = {} if attribute_write_mode == "legacy_only" else dict(attributes)
|
||||
if attribute_write_mode == "json_only":
|
||||
self.attribute_string, self.attributes_number, self.attributes_bool = {}, {}, {}
|
||||
|
||||
# Process events and derive error events. self.events holds the parsed
|
||||
# response shape; np_arr() encodes back to the DB format on insert.
|
||||
|
||||
@@ -64,8 +64,8 @@ def test_resource_default_warning(
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
"by default. To query another context explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name' or 'body.service.name')"
|
||||
)
|
||||
assert warning["warnings"] == [
|
||||
{"message": expected_service_name_warning},
|
||||
@@ -237,8 +237,8 @@ def test_deduped_warnings_for_single_query(
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
"by default. To query another context explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name' or 'body.service.name')"
|
||||
)
|
||||
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
|
||||
assert warning["warnings"] == [
|
||||
@@ -328,8 +328,8 @@ def test_deduped_warnings_for_multiple_queries(
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
"by default. To query another context explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name' or 'body.service.name')"
|
||||
)
|
||||
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
|
||||
assert warning["warnings"] == [
|
||||
|
||||
@@ -5,11 +5,15 @@ from http import HTTPStatus
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
BuilderQuery,
|
||||
OrderBy,
|
||||
RequestType,
|
||||
TelemetryFieldKey,
|
||||
assert_grouped_series,
|
||||
build_aggregation,
|
||||
build_group_by_field,
|
||||
build_traces_scalar_query,
|
||||
get_rows,
|
||||
index_series_by_label,
|
||||
make_query_request,
|
||||
)
|
||||
@@ -329,3 +333,80 @@ def test_traces_attributes_json_collision_and_map_parity(
|
||||
aggregations = (response.json()["data"]["data"]["results"][0].get("aggregations")) or []
|
||||
series = index_series_by_label(aggregations[0]["series"], "http.route") if aggregations else {}
|
||||
assert set(series.keys()) == expected, label
|
||||
|
||||
|
||||
def test_traces_attributes_json_list_view(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
seed_attribute_evolution: Callable[[str, datetime], None],
|
||||
) -> None:
|
||||
"""One straddling list query surfaces each row's attributes bag from its own home
|
||||
(legacy maps / dual-written / JSON-only), flattened to dotted keys with native types."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
evolution_time = datetime.now(tz=UTC).replace(second=0, microsecond=0) - timedelta(minutes=30)
|
||||
seed_attribute_evolution("traces", evolution_time)
|
||||
|
||||
service = "list-homes-service"
|
||||
spans = [
|
||||
Traces(
|
||||
timestamp=evolution_time - timedelta(minutes=10),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name="map only",
|
||||
resources={"service.name": service},
|
||||
attributes={"http.route": "/map", "http.retry.count": 1, "http.cache.hit": True},
|
||||
attribute_write_mode="legacy_only",
|
||||
),
|
||||
Traces(
|
||||
timestamp=evolution_time + timedelta(minutes=5),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name="dual written",
|
||||
resources={"service.name": service},
|
||||
attributes={"http.route": "/dual", "http.retry.count": 2, "http.cache.hit": False},
|
||||
attribute_write_mode="dual_write",
|
||||
),
|
||||
Traces(
|
||||
timestamp=evolution_time + timedelta(minutes=10),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name="json only",
|
||||
resources={"service.name": service},
|
||||
attributes={"http.route": "/json", "http.retry.count": 3, "http.cache.hit": True},
|
||||
attribute_write_mode="json_only",
|
||||
),
|
||||
]
|
||||
insert_traces(spans)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((evolution_time - timedelta(minutes=15)).timestamp() * 1000),
|
||||
end_ms=int((evolution_time + timedelta(minutes=15)).timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="traces",
|
||||
name="A",
|
||||
limit=10,
|
||||
filter_expression=f"resource.service.name = '{service}'",
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
).to_dict()
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
rows = get_rows(response)
|
||||
assert len(rows) == 3
|
||||
expected = [
|
||||
("map only", {"http.route": "/map", "http.retry.count": 1, "http.cache.hit": True}),
|
||||
("dual written", {"http.route": "/dual", "http.retry.count": 2, "http.cache.hit": False}),
|
||||
("json only", {"http.route": "/json", "http.retry.count": 3, "http.cache.hit": True}),
|
||||
]
|
||||
for row, (name, attributes) in zip(rows, expected, strict=True):
|
||||
assert row["data"]["name"] == name
|
||||
# int/float compare equal in Python, so map-sourced float64 and JSON-sourced numbers both match.
|
||||
assert row["data"]["attributes"] == attributes, name
|
||||
|
||||
Reference in New Issue
Block a user