Compare commits

..

5 Commits

Author SHA1 Message Date
nityanandagohain
6d1355071e test(llm-observability): keep only the rule-walk and decode cases for pricing patterns
Pattern semantics are covered once in the collector's pkg/llmpricing.

Assisted-by: Claude Fable 5.1
2026-11-02 10:34:04 +05:30
nityanandagohain
dc1b3608f5 refactor(llm-observability): use the collector's llmpricing matcher
Assisted-by: Claude Fable 5.1
2026-11-02 10:29:10 +05:30
nityanandagohain
03184576bf chore(deps): pin signoz-otel-collector to f531882
Pseudo-version until pkg/llmpricing ships in a tag.

Assisted-by: Claude Fable 5.1
2026-11-02 10:29:09 +05:30
Nityananda Gohain
bb1fd30b10 Merge branch 'main' into issue_nerve_517 2026-11-02 09:28:14 +05:30
nityanandagohain
a6627445d0 fix(llm-observability): mirror the collector's bounded prefix match for pricing patterns 2026-10-30 20:16:02 +05:30
97 changed files with 711 additions and 2058 deletions

View File

@@ -12131,8 +12131,6 @@ components:
additionalProperties:
type: string
type: object
scope:
$ref: '#/components/schemas/SpantypesScope'
spanId:
type: string
timestamp:
@@ -12149,7 +12147,6 @@ components:
- event
- attributes
- resource
- scope
type: object
SpantypesGettableFlamegraphTrace:
properties:
@@ -12390,16 +12387,6 @@ components:
nullable: true
type: array
type: object
SpantypesScope:
properties:
attributes:
additionalProperties: {}
type: object
name:
type: string
version:
type: string
type: object
SpantypesSpanAggregation:
properties:
aggregation:
@@ -12769,8 +12756,6 @@ components:
type: object
response_status_code:
type: string
scope:
$ref: '#/components/schemas/SpantypesScope'
span_id:
type: string
status_code:

View File

@@ -13264,23 +13264,6 @@ export type SpantypesFlamegraphSpanDTOAttributes = { [key: string]: unknown };
export type SpantypesFlamegraphSpanDTOResource = { [key: string]: string };
export type SpantypesScopeDTOAttributes = { [key: string]: unknown };
export interface SpantypesScopeDTO {
/**
* @type object
*/
attributes?: SpantypesScopeDTOAttributes;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
version?: string;
}
export interface SpantypesFlamegraphSpanDTO {
/**
* @type object
@@ -13316,7 +13299,6 @@ export interface SpantypesFlamegraphSpanDTO {
* @type object
*/
resource: SpantypesFlamegraphSpanDTOResource;
scope: SpantypesScopeDTO;
/**
* @type string
*/
@@ -13862,7 +13844,6 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
response_status_code?: string;
scope?: SpantypesScopeDTO;
/**
* @type string
*/

View File

@@ -4,7 +4,6 @@ import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -148,7 +147,7 @@ function LogDetailInner({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -1,169 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { withCanvas } from '@/storybook/decorators/withCanvas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
import {
addOnToggles,
appendToAggregation,
DEFAULT_ARGS,
expectFirstQuery,
PANEL_EDITOR_ARGS,
PANEL_EDITOR_PATH,
payloadIn,
routeWith,
selectMetric,
} from './QueryBuilderV2.stories.mocks';
import QueryProbe from './QueryProbe';
const METRICS_EXPLORER_ARGS = {
config: { initialDataSource: DataSource.METRICS, queryVariant: 'static' },
showFunctions: false,
} as const;
const metricsExplorerRoute = routeWith(
ROUTES.METRICS_EXPLORER_EXPLORER,
initialQueriesMap.metrics,
);
/**
* Builder rules that hold without a backend: which add-ons a panel kind
* offers, the aggregations a metric type defaults to, and input limits. The
* probe below the builder prints the payload each play asserts on.
*/
const meta = {
title: 'Components/Query Builder/Behaviour',
component: QueryBuilderV2,
tags: ['play'],
decorators: [withCanvas({ maxWidth: 1100 })],
args: DEFAULT_ARGS,
render: (args): JSX.Element => (
<>
<QueryBuilderV2 {...args} />
<QueryProbe />
</>
),
} satisfies Meta<typeof QueryBuilderV2>;
export default meta;
type Story = StoryObj<typeof meta>;
/** A Value panel offers only the legend add-on. */
export const LogsOnValuePanel: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.VALUE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual(['legend_format']),
);
},
};
/** Metrics on a Table: reduce-to is offered; group by lives in the aggregation row. */
export const MetricsOnTablePanel: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.TABLE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.metrics) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual([
'having',
'order_by',
'limit',
'legend_format',
'reduce_to',
]),
);
},
};
/** Traces on a time series: every add-on, with group by as a toggle. */
export const TracesOnTimeSeriesPanel: Story = {
args: PANEL_EDITOR_ARGS,
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.traces) },
},
play: async ({ canvasElement }) => {
await waitFor(() =>
expect(addOnToggles(canvasElement)).toStrictEqual([
'group_by',
'having',
'order_by',
'limit',
'legend_format',
]),
);
},
};
/** Picking a gauge defaults to avg over time and avg across series. */
export const GaugeMetricDefaults: Story = {
args: METRICS_EXPLORER_ARGS,
parameters: { signoz: { route: metricsExplorerRoute } },
play: async ({ canvasElement }) => {
await selectMetric(canvasElement, 'container_cpu_utilization');
await expectFirstQuery(canvasElement, {
aggregations: [
expect.objectContaining({
metricName: 'container_cpu_utilization',
timeAggregation: 'avg',
spaceAggregation: 'avg',
}),
],
});
},
};
/** Picking a histogram defaults to the p90 across series. */
export const HistogramMetricDefaults: Story = {
args: METRICS_EXPLORER_ARGS,
parameters: { signoz: { route: metricsExplorerRoute } },
play: async ({ canvasElement }) => {
await selectMetric(canvasElement, 'signoz_latency_bucket');
await expectFirstQuery(canvasElement, {
aggregations: [
expect.objectContaining({
metricName: 'signoz_latency_bucket',
spaceAggregation: 'p90',
}),
],
});
},
};
/** A Value panel takes a single aggregation: a second one is not accepted. */
export const ValuePanelTakesOneAggregation: Story = {
args: { ...PANEL_EDITOR_ARGS, panelType: PANEL_TYPES.VALUE },
parameters: {
signoz: { route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, {
aggregations: [{ expression: 'count()' }],
});
const { before, after } = appendToAggregation(
canvasElement,
', count_distinct(service.name)',
);
await expect(after).toBe(before);
await expectFirstQuery(canvasElement, {
aggregations: [{ expression: 'count()' }],
});
await expect(
payloadIn(canvasElement).queries[0].spec.aggregations,
).toHaveLength(1);
},
};

View File

@@ -1,134 +0,0 @@
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
import { EditorView } from '@codemirror/view';
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import type { QueryBuilderV2 } from '../QueryBuilderV2';
type QueryBuilderProps = Parameters<typeof QueryBuilderV2>[0];
/** The builder's provider reads its starting query from the URL, as every host seeds it. */
export const routeWith = (path: string, query: Query): string =>
`${path}?compositeQuery=${encodeURIComponent(JSON.stringify(query))}`;
export const meterQuery: Query = {
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [
{ ...initialQueriesMap.metrics.builder.queryData[0], source: 'meter' },
],
},
};
/** `PanelEditorQueryBuilder`'s props, less the per-kind field config. */
export const PANEL_EDITOR_ARGS: Partial<QueryBuilderProps> = {
allowedDataSources: [
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
TelemetrytypesSignalDTO.metrics,
],
showTraceOperator: true,
signalSourceChangeEnabled: true,
savePreviousQuery: true,
};
export const PANEL_EDITOR_PATH = '/dashboard/story/panel/story';
interface ProbedQuery {
type: string;
spec: {
aggregations?: Record<string, unknown>[];
[key: string]: unknown;
};
}
/** The `compositeQuery` the story's `QueryProbe` prints for the builder's state. */
export const payloadIn = (
canvasElement: HTMLElement,
): { queries: ProbedQuery[] } =>
JSON.parse(
within(canvasElement).getByTestId('qb-payload').textContent ?? '{}',
) as { queries: ProbedQuery[] };
export const expectFirstQuery = async (
canvasElement: HTMLElement,
spec: Record<string, unknown>,
): Promise<void> => {
await waitFor(() =>
expect(payloadIn(canvasElement).queries[0]).toMatchObject({
type: 'builder_query',
spec: expect.objectContaining(spec),
}),
);
};
/** The add-on toggles query `A` offers, in order (`group_by`, `having`, …). */
export const addOnToggles = (canvasElement: HTMLElement): string[] =>
within(within(canvasElement).getByTestId('query-A'))
.queryAllByTestId(/^query-add-on-/)
.map((toggle) =>
(toggle.getAttribute('data-testid') ?? '').replace('query-add-on-', ''),
);
/** Picks `metricName` in query `A`'s metric selector; its list portals out. */
export const selectMetric = async (
canvasElement: HTMLElement,
metricName: string,
): Promise<void> => {
const field = within(canvasElement).getByTestId('metric-name-selector-0');
const input = within(field).getByRole('combobox');
await userEvent.click(input);
await userEvent.type(input, metricName);
const option = await waitFor(async () => {
const match = Array.from(
document.querySelectorAll<HTMLElement>(
'.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option',
),
).find((candidate) => candidate.textContent?.includes(metricName));
await expect(match).toBeDefined();
return match as HTMLElement;
});
await userEvent.click(option);
await waitFor(() => expect(screen.queryByRole('listbox')).toBeNull());
};
/**
* Appends `text` to query `A`'s aggregation editor and returns its document
* before and after. Dispatched as a CodeMirror transaction: `userEvent.type`
* strands the caret when the line redraws, and a transaction runs the editor's
* own filters, which is what an input limit is.
*/
export const appendToAggregation = (
canvasElement: HTMLElement,
text: string,
): { before: string; after: string } => {
const content = within(canvasElement)
.getByTestId('query-aggregation-editor')
.querySelector<HTMLElement>('.cm-content');
const view = content ? EditorView.findFromDOM(content) : null;
if (!view) {
throw new Error('aggregation editor not mounted');
}
const before = view.state.doc.toString();
view.dispatch({
changes: { from: before.length, insert: text },
selection: { anchor: before.length + text.length },
userEvent: 'input.type',
});
// Read synchronously: the document is the editor's own verdict, before
// React state catches up.
return { before, after: view.state.doc.toString() };
};
export const DEFAULT_ARGS: Partial<QueryBuilderProps> = {
panelType: PANEL_TYPES.TIME_SERIES,
version: 'v3',
};

View File

@@ -1,145 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { withCanvas } from '@/storybook/decorators/withCanvas';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
import {
DEFAULT_ARGS,
expectFirstQuery,
meterQuery,
PANEL_EDITOR_ARGS,
PANEL_EDITOR_PATH,
routeWith,
} from './QueryBuilderV2.stories.mocks';
import QueryProbe from './QueryProbe';
/**
* The query builder as each host mounts it: the props come from that host's
* call site, the starting query from its route. The probe below the builder
* prints the `query_range` payload its state would send.
*/
const meta = {
title: 'Components/Query Builder',
component: QueryBuilderV2,
tags: ['play'],
decorators: [withCanvas({ maxWidth: 1100 })],
args: DEFAULT_ARGS,
render: (args): JSX.Element => (
<>
<QueryBuilderV2 {...args} />
<QueryProbe />
</>
),
} satisfies Meta<typeof QueryBuilderV2>;
export default meta;
type Story = StoryObj<typeof meta>;
/** Logs explorer, graph view: a fixed logs signal with `count()`. */
export const LogsExplorer: Story = {
args: {
config: { initialDataSource: DataSource.LOGS, queryVariant: 'static' },
},
parameters: {
signoz: { route: routeWith(ROUTES.LOGS_EXPLORER, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, {
signal: 'logs',
aggregations: [{ expression: 'count()' }],
});
},
};
/** Traces explorer: a fixed traces signal, with trace matching offered. */
export const TracesExplorer: Story = {
args: {
config: { initialDataSource: DataSource.TRACES, queryVariant: 'static' },
showTraceOperator: true,
},
parameters: {
signoz: {
route: routeWith(ROUTES.TRACES_EXPLORER, initialQueriesMap.traces),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'traces' });
},
};
/** Metrics explorer: a fixed metrics signal, functions hidden. */
export const MetricsExplorer: Story = {
args: {
config: { initialDataSource: DataSource.METRICS, queryVariant: 'static' },
showFunctions: false,
},
parameters: {
signoz: {
route: routeWith(
ROUTES.METRICS_EXPLORER_EXPLORER,
initialQueriesMap.metrics,
),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'metrics' });
},
};
/** Meter explorer: metrics read from the meter source. */
export const MeterExplorer: Story = {
args: {
config: {
initialDataSource: DataSource.METRICS,
queryVariant: 'static',
signalSource: 'meter',
},
showFunctions: false,
},
parameters: {
signoz: { route: routeWith(ROUTES.METER_EXPLORER, meterQuery) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'metrics', source: 'meter' });
},
};
/**
* V2 panel editor: a data-source dropdown per query, several queries,
* formulas and trace matching.
*/
export const PanelEditor: Story = {
args: PANEL_EDITOR_ARGS,
parameters: {
signoz: {
route: routeWith(PANEL_EDITOR_PATH, initialQueriesMap.logs),
},
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'logs' });
await expect(
within(canvasElement).getByTestId('query-data-source-selector-0'),
).toBeVisible();
},
};
/** Logs alert: a fixed logs signal with functions, as the alert form mounts it. */
export const LogsAlert: Story = {
args: {
config: { initialDataSource: DataSource.LOGS, queryVariant: 'static' },
showFunctions: true,
signalSourceChangeEnabled: true,
},
parameters: {
signoz: { route: routeWith(ROUTES.ALERTS_NEW, initialQueriesMap.logs) },
},
play: async ({ canvasElement }) => {
await expectFirstQuery(canvasElement, { signal: 'logs' });
},
};

View File

@@ -1,11 +0,0 @@
.probe {
margin-top: 16px;
padding: 12px;
max-height: 320px;
overflow: auto;
border: 1px solid var(--l1-border);
border-radius: 4px;
font-size: 12px;
color: var(--l2-foreground);
background: var(--l1-background);
}

View File

@@ -1,38 +0,0 @@
import { useMemo } from 'react';
import { prepareQueryRangePayloadV5 } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import styles from './QueryProbe.module.scss';
/** Fixed, so the printed payload is the same on every render and capture. */
const WINDOW = { start: 1_700_000_000_000, end: 1_700_003_600_000 };
/**
* Prints the `compositeQuery` the builder's live state would send, so a play
* function asserts on the query rather than on the DOM that edits it.
*/
function QueryProbe(): JSX.Element {
const { currentQuery, panelType } = useQueryBuilder();
const payload = useMemo(() => {
try {
return prepareQueryRangePayloadV5({
query: currentQuery,
graphType: panelType ?? PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
...WINDOW,
}).queryPayload.compositeQuery;
} catch (error) {
return { error: String(error) };
}
}, [currentQuery, panelType]);
return (
<pre className={styles.probe} data-testid="qb-payload">
{JSON.stringify(payload, null, 2)}
</pre>
);
}
export default QueryProbe;

View File

@@ -13,7 +13,7 @@ interface PatternEditorProps {
onChange: (patterns: string[]) => void;
}
// Model-name prefix patterns as removable chips + an add input.
// Model-name patterns as removable chips + an add input.
function PatternEditor({
patterns,
isReadOnly,
@@ -37,12 +37,7 @@ function PatternEditor({
return (
<div className={styles.drawerSection}>
<span className={styles.fieldLabel}>
Model name patterns{' '}
<Typography.Text as="span" color="muted">
(prefix match)
</Typography.Text>
</span>
<span className={styles.fieldLabel}>Model name patterns</span>
<div className={styles.patternBox}>
{patterns.length > 0 && (
<div className={styles.patternChips}>
@@ -53,7 +48,7 @@ function PatternEditor({
color="secondary"
variant="outlined"
>
{pattern}*
{pattern}
{!isReadOnly && (
<button
type="button"
@@ -95,8 +90,9 @@ function PatternEditor({
)}
</div>
<Typography.Text as="p" size="small" color="muted">
Each pattern uses <strong>prefix matching</strong> against{' '}
<code>gen_ai.request.model</code>.
A pattern matches <code>gen_ai.request.model</code> exactly, plus its dated
or platform variants such as <code>-20260921</code>, <code>-latest</code> or{' '}
<code>@20260921</code>. End it with <code>*</code> to match any suffix.
</Typography.Text>
</div>
);

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
@@ -55,7 +54,7 @@ function LiveLogsContainer({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,7 +5,6 @@ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { toast } from '@signozhq/ui/sonner';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
@@ -31,7 +30,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEventSource } from 'providers/EventSource';
// interfaces
import { ILog } from 'types/api/logs/log';
import { StringOperators } from 'types/common/queryBuilder';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
import { getAbsoluteUrl } from 'utils/basePath';
@@ -69,7 +68,7 @@ function LiveLogsList({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: StringOperators.NOOP,
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { Button, Skeleton } from 'antd';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -17,7 +16,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { StringOperators } from 'types/common/queryBuilder';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { withBasePath } from 'utils/basePath';
import { useContextLogData } from './useContextLogData';
@@ -46,7 +45,7 @@ function ContextLogRenderer({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,7 +5,6 @@ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { toast } from '@signozhq/ui/sonner';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
@@ -78,7 +77,7 @@ function LogsExplorerList({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator:
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -1,7 +1,6 @@
import { ReactNode, useState } from 'react';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
@@ -29,7 +28,7 @@ function LogsActionsContainer({
}): JSX.Element {
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -1,20 +1,11 @@
import { createElement, ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { useQueries } from 'react-query';
import { renderHook } from '@testing-library/react';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import {
FormattingOptions,
PreferenceContextValue,
PreferenceSlice,
} from 'providers/preferences/types';
import { renderHook } from 'tests/test-utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { FontSize } from '../types';
import useOptionsMenu from '../useOptionsMenu';
// Mock all dependencies
@@ -22,118 +13,104 @@ jest.mock('hooks/useNotifications');
jest.mock('providers/preferences/context/PreferenceContextProvider');
jest.mock('hooks/useUrlQueryData');
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
const mockUseNotifications = jest.mocked(useNotifications);
const mockUsePreferenceContext = jest.mocked(usePreferenceContext);
const mockUseUrlQueryData = jest.mocked(useUrlQueryData);
const mockUseGetQueryKeySuggestions = jest.mocked(useGetQueryKeySuggestions);
type KeySuggestionsResult = ReturnType<typeof useGetQueryKeySuggestions>;
// The hook reads only `data.data.data.keys` and `isFetching`.
const keySuggestions = (
attributeKeys: Record<string, string>[],
): KeySuggestionsResult =>
({
data: { data: { data: { complete: true, keys: { attributeKeys } } } },
isFetching: false,
}) as unknown as KeySuggestionsResult;
const wrapper = ({ children }: { children: ReactNode }): JSX.Element =>
createElement(QueryClientProvider, { client: new QueryClient() }, children);
const toIds = (columns: TelemetryFieldKey[]): string[] =>
columns.map((column) => `${column.fieldContext}:${column.name}`);
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueries: jest.fn(),
}));
describe('useOptionsMenu', () => {
const mockNotifications = {
error: jest.fn() as jest.MockedFunction<NotificationInstance['error']>,
success: jest.fn() as jest.MockedFunction<NotificationInstance['success']>,
} as Partial<NotificationInstance> as NotificationInstance;
const mockUpdateColumns = jest.fn() as jest.MockedFunction<
PreferenceSlice['updateColumns']
>;
const mockUpdateFormatting = jest.fn() as jest.MockedFunction<
PreferenceSlice['updateFormatting']
>;
const mockRedirectWithQuery = jest.fn() as jest.MockedFunction<
(newQueryData: unknown) => void
>;
const slice = (
columns: TelemetryFieldKey[],
formatting: FormattingOptions,
): PreferenceSlice => ({
preferences: { columns, formatting },
loading: false,
error: null,
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
});
const rawFormatting: FormattingOptions = {
format: 'raw',
maxLines: 1,
fontSize: FontSize.SMALL,
};
const mockNotifications = { error: jest.fn(), success: jest.fn() };
const mockUpdateColumns = jest.fn();
const mockUpdateFormatting = jest.fn();
const mockRedirectWithQuery = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUseNotifications.mockReturnValue({
(useNotifications as jest.Mock).mockReturnValue({
notifications: mockNotifications,
});
mockUsePreferenceContext.mockReturnValue({
[SavedviewtypesSourceDTO.traces]: slice([], rawFormatting),
[SavedviewtypesSourceDTO.logs]: slice([], rawFormatting),
} satisfies PreferenceContextValue);
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 1,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 1,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
mockUseUrlQueryData.mockReturnValue({
(useUrlQueryData as jest.Mock).mockReturnValue({
query: null,
queryData: undefined,
redirectWithQuery: mockRedirectWithQuery,
});
(useQueries as jest.Mock).mockReturnValue([]);
});
it('does not show isRoot or isEntryPoint in column options when sourcePage is traces', () => {
it('does not show isRoot or isEntryPoint in column options when dataSource is TRACES', () => {
// Mock the query key suggestions to return data including isRoot and isEntryPoint
mockUseGetQueryKeySuggestions.mockReturnValue(
keySuggestions([
{
name: 'isRoot',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'isRoot',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'isEntryPoint',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'duration',
signal: 'traces',
fieldDataType: 'float64',
fieldContext: '',
},
{
name: 'serviceName',
signal: 'traces',
fieldDataType: 'string',
fieldContext: '',
},
],
},
},
},
{
name: 'isEntryPoint',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'duration',
signal: 'traces',
fieldDataType: 'float64',
fieldContext: '',
},
{
name: 'serviceName',
signal: 'traces',
fieldDataType: 'string',
fieldContext: '',
},
]),
);
},
isFetching: false,
});
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.traces,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
@@ -150,38 +127,98 @@ describe('useOptionsMenu', () => {
expect(optionNames).toContain('serviceName');
});
it('shows body in column options when sourcePage is logs', () => {
it('does not show body in column options when dataSource is METRICS', () => {
// Mock the query key suggestions to return data including body
mockUseGetQueryKeySuggestions.mockReturnValue(
keySuggestions([
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'status',
signal: 'metrics',
fieldDataType: 'int64',
fieldContext: '',
},
{
name: 'value',
signal: 'metrics',
fieldDataType: 'float64',
fieldContext: '',
},
],
},
},
},
{
name: 'level',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'timestamp',
signal: 'logs',
fieldDataType: 'int64',
fieldContext: '',
},
]),
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.METRICS,
aggregateOperator: 'count',
}),
);
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: 'count',
}),
{ wrapper },
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should NOT be in the options
expect(optionNames).not.toContain('body');
// Other attributes should be present
expect(optionNames).toContain('status');
expect(optionNames).toContain('value');
});
it('does not show body in column options when dataSource is LOGS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'level',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'timestamp',
signal: 'logs',
fieldDataType: 'int64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
@@ -197,15 +234,15 @@ describe('useOptionsMenu', () => {
});
describe('reorderSelectColumns / handleRemoveSelectedColumn — composite ID lookup', () => {
const baseFormatting: FormattingOptions = {
const baseFormatting = {
format: 'table',
maxLines: 1,
fontSize: FontSize.SMALL,
fontSize: 'small',
};
// Two entries share the same `name` but differ in `fieldContext` — the
// exact case composite IDs are meant to disambiguate.
const seedColumns: TelemetryFieldKey[] = [
const seedColumns = [
{
name: 'body',
fieldContext: 'log',
@@ -227,26 +264,36 @@ describe('useOptionsMenu', () => {
{
name: 'timestamp',
fieldContext: 'log',
fieldDataType: '',
signal: 'logs',
},
];
beforeEach(() => {
mockUseGetQueryKeySuggestions.mockReturnValue(keySuggestions([]));
mockUsePreferenceContext.mockReturnValue({
[SavedviewtypesSourceDTO.traces]: slice([], baseFormatting),
[SavedviewtypesSourceDTO.logs]: slice(seedColumns, baseFormatting),
} satisfies PreferenceContextValue);
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: { data: { data: { keys: {} } } },
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: { columns: [], formatting: baseFormatting },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: { columns: seedColumns, formatting: baseFormatting },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
});
it('reorders by composite IDs — preserves both same-name variants distinctly', () => {
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
// New order: [attribute:service.name, log:body, resource:service.name, log:timestamp]
@@ -259,7 +306,12 @@ describe('useOptionsMenu', () => {
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
const reordered = mockUpdateColumns.mock.calls[0][0];
expect(toIds(reordered)).toStrictEqual([
expect(
reordered.map(
(c: { name: string; fieldContext: string }) =>
`${c.fieldContext}:${c.name}`,
),
).toStrictEqual([
'attribute:service.name',
'log:body',
'resource:service.name',
@@ -268,13 +320,11 @@ describe('useOptionsMenu', () => {
});
it('reorder ignores ids that are not columns (e.g. state-indicator) and skips unknown ids', () => {
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
result.current.config.addColumn?.onReorder([
@@ -288,7 +338,12 @@ describe('useOptionsMenu', () => {
const reordered = mockUpdateColumns.mock.calls[0][0];
// state-indicator + unknown.composite filtered; rest preserved in given order.
expect(toIds(reordered)).toStrictEqual([
expect(
reordered.map(
(c: { name: string; fieldContext: string }) =>
`${c.fieldContext}:${c.name}`,
),
).toStrictEqual([
'log:timestamp',
'log:body',
'resource:service.name',
@@ -297,13 +352,11 @@ describe('useOptionsMenu', () => {
});
it('removes only the variant whose composite ID matches', () => {
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
// Removing 'resource:service.name' should drop ONLY the resource variant.
@@ -311,21 +364,20 @@ describe('useOptionsMenu', () => {
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
const remaining = mockUpdateColumns.mock.calls[0][0];
expect(toIds(remaining)).toStrictEqual([
'log:body',
'attribute:service.name',
'log:timestamp',
]);
expect(
remaining.map(
(c: { name: string; fieldContext: string }) =>
`${c.fieldContext}:${c.name}`,
),
).toStrictEqual(['log:body', 'attribute:service.name', 'log:timestamp']);
});
it('removing by a non-matching composite ID is a no-op (filter returns the full list)', () => {
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
result.current.config.addColumn?.onRemove('unknown.composite');
@@ -338,34 +390,35 @@ describe('useOptionsMenu', () => {
describe('fieldsSelector.value drops legacy columns without a name', () => {
it('excludes entries missing name while keeping valid columns', () => {
mockUseGetQueryKeySuggestions.mockReturnValue(keySuggestions([]));
mockUsePreferenceContext.mockReturnValue({
[SavedviewtypesSourceDTO.traces]: slice(
[
{ name: 'body', fieldContext: 'log' },
// A legacy stored column: `key` only, no `name`.
{
key: 'legacy-key-no-name',
fieldContext: 'log',
} as unknown as TelemetryFieldKey,
{ name: 'timestamp', fieldContext: 'log' },
],
{
format: 'table',
maxLines: 1,
fontSize: FontSize.SMALL,
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: { data: { data: { keys: {} } } },
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [
{ name: 'body', fieldContext: 'log' },
{ key: 'legacy-key-no-name', fieldContext: 'log' },
{ name: 'timestamp', fieldContext: 'log' },
],
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
},
),
[SavedviewtypesSourceDTO.logs]: slice([], {}),
} satisfies PreferenceContextValue);
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: { columns: [], formatting: {} },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
const { result } = renderHook(
() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.traces,
aggregateOperator: 'count',
}),
{ wrapper },
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
const fields = result.current.config.fieldsSelector?.value ?? [];

View File

@@ -1,6 +1,4 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PREFERENCE_SOURCE_PAGES } from 'providers/preferences/constants';
import { DataSource } from 'types/common/queryBuilder';
import { FontSize, OptionsQuery } from './types';
@@ -15,14 +13,6 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL,
};
export const SOURCE_PAGE_SIGNAL: Record<
(typeof PREFERENCE_SOURCE_PAGES)[number],
DataSource
> = {
[SavedviewtypesSourceDTO.logs]: DataSource.LOGS,
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
};
export const EXCLUDED_COLUMNS: Record<DataSource, string[]> = {
[DataSource.TRACES]: ['body', 'isRoot', 'isEntryPoint'],
[DataSource.METRICS]: ['body'],

View File

@@ -1,7 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { TelemetryFieldKey } from 'api/v5/v5';
import { AxiosResponse } from 'axios';
@@ -12,7 +11,6 @@ import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { PreferenceSourcePage } from 'providers/preferences/types';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
@@ -22,13 +20,13 @@ import {
FieldDataType,
SignalType,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
defaultTraceSelectedColumns,
EXCLUDED_COLUMNS,
SOURCE_PAGE_SIGNAL,
URL_OPTIONS,
} from './constants';
import {
@@ -42,7 +40,7 @@ import { buildCompositeKey, getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps {
storageKey?: string;
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
aggregateOperator?: string;
initialOptions?: InitialOptions;
}
@@ -54,14 +52,15 @@ interface UseOptionsMenu {
}
const useOptionsMenu = ({
sourcePage,
dataSource,
// aggregateOperator,
initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => {
const signal = SOURCE_PAGE_SIGNAL[sourcePage];
const { notifications } = useNotifications();
const prefCtx = usePreferenceContext();
// TODO: send null to updateColumns and updateFormatting if dataSource is not logs or traces
const { preferences, updateColumns, updateFormatting } =
usePreferenceContext()[sourcePage];
const slice = dataSource === DataSource.TRACES ? prefCtx.traces : prefCtx.logs;
const { preferences, updateColumns, updateFormatting } = slice;
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
@@ -69,10 +68,10 @@ const useOptionsMenu = ({
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({
signal,
signal: dataSource,
searchText: '',
}),
[signal],
[dataSource],
);
const { query: optionsQuery, redirectWithQuery: redirectWithOptionsData } =
@@ -128,7 +127,7 @@ const useOptionsMenu = ({
?.map((column) => attributesData.find(({ name }) => name === column))
.filter((e) => !!e) || []) as TelemetryFieldKey[];
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
initialSelected = initialSelected
?.map((col) => {
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.name)) {
@@ -158,7 +157,7 @@ const useOptionsMenu = ({
isFetchedInitialAttributes,
initialOptions?.selectColumns,
initialAttributesResult,
sourcePage,
dataSource,
]);
const {
@@ -196,7 +195,7 @@ const useOptionsMenu = ({
fieldDataType: e.fieldDataType,
}));
}
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
return defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
@@ -204,16 +203,16 @@ const useOptionsMenu = ({
}
return [];
}, [sourcePage, searchedAttributesDataV5?.data.data.keys]);
}, [dataSource, searchedAttributesDataV5?.data.data.keys]);
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
} else if (sourcePage === SavedviewtypesSourceDTO.logs) {
} else if (dataSource === DataSource.LOGS) {
// eslint-disable-next-line sonarjs/no-identical-functions
defaultColumns = defaultLogsSelectedColumns.map((e) => ({
...e,
@@ -233,7 +232,7 @@ const useOptionsMenu = ({
...initialOptions,
selectColumns: finalSelectColumns,
};
}, [sourcePage, initialOptions, initialSelectedColumns]);
}, [dataSource, initialOptions, initialSelectedColumns]);
const selectedColumnKeys = useMemo(
() => preferences?.columns?.map(({ name }) => name) || [],
@@ -242,7 +241,7 @@ const useOptionsMenu = ({
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
const exclusions = EXCLUDED_COLUMNS[signal];
const exclusions = EXCLUDED_COLUMNS[dataSource];
if (exclusions) {
return !exclusions.includes(item.name);
}
@@ -250,7 +249,7 @@ const useOptionsMenu = ({
});
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
}, [signal, searchedAttributeKeys, selectedColumnKeys]);
}, [dataSource, searchedAttributeKeys, selectedColumnKeys]);
const handleRedirectWithOptionsData = useCallback(
(newQueryData: OptionsQuery) => {
@@ -286,10 +285,7 @@ const useOptionsMenu = ({
buildCompositeKey(f.name, f.fieldContext, f.fieldDataType) !== columnKey,
);
if (
!newSelectedColumns?.length &&
sourcePage !== SavedviewtypesSourceDTO.logs
) {
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
notifications.error({
message: 'There must be at least one selected column',
});
@@ -298,7 +294,7 @@ const useOptionsMenu = ({
updateColumns(newSelectedColumns || []);
},
[sourcePage, notifications, preferences, updateColumns],
[dataSource, notifications, preferences, updateColumns],
);
const handleFormatChange = useCallback(

View File

@@ -1,9 +1,10 @@
import { useState } from 'react';
import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '@signozhq/ui/button';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { act, screen, userEvent, waitFor } from 'tests/test-utils';
import SavedViewsHeader from '../SavedViewsHeader';
import { mockSavedViewsApi } from './savedViewsApiMock';
@@ -27,7 +28,7 @@ const errors = {
};
function Harness(): JSX.Element {
const traces = usePreferenceContext()[SavedviewtypesSourceDTO.traces];
const { traces } = usePreferenceContext();
const [headerKey, setHeaderKey] = useState(0);
const columns = traces.preferences?.columns ?? [];
@@ -36,7 +37,7 @@ function Harness(): JSX.Element {
<SavedViewsHeader
key={headerKey}
source={SavedviewtypesSourceDTO.traces}
onOpenViews={jest.fn() as jest.MockedFunction<() => void>}
onOpenViews={jest.fn()}
/>
<span data-testid="columns">
{columns.map((column) => column.name).join(',')}
@@ -83,14 +84,13 @@ describe('SavedViewsHeader columns', () => {
});
it('keeps edited columns when the view is fetched again', async () => {
const user = userEvent.setup();
mockSavedViewsApi([errors]);
await openView();
await user.click(screen.getByTestId('add-column'));
await userEvent.click(screen.getByTestId('add-column'));
await waitFor(() => expect(isDirty()).toBe(true));
await user.click(screen.getByTestId('remount-header'));
await userEvent.click(screen.getByTestId('remount-header'));
await act(() => new Promise((resolve) => setTimeout(resolve, 200)));
expect(columnsText()).toBe('service.name,name,extra-2');
@@ -98,37 +98,35 @@ describe('SavedViewsHeader columns', () => {
});
it('discard puts the saved columns back', async () => {
const user = userEvent.setup();
mockSavedViewsApi([errors]);
await openView();
await user.click(screen.getByTestId('add-column'));
await userEvent.click(screen.getByTestId('add-column'));
await waitFor(() => expect(isDirty()).toBe(true));
await user.click(screen.getByTestId('saved-views-discard'));
await userEvent.click(screen.getByTestId('saved-views-discard'));
await waitFor(() => expect(columnsText()).toBe('service.name,name'));
expect(isDirty()).toBe(false);
});
it('discard after an update goes back to the updated columns', async () => {
const user = userEvent.setup();
mockSavedViewsApi([errors]);
await openView();
await user.click(screen.getByTestId('add-column'));
await userEvent.click(screen.getByTestId('add-column'));
await waitFor(() => expect(isDirty()).toBe(true));
await user.click(screen.getByTestId('saved-views-save-changes'));
await userEvent.click(screen.getByTestId('saved-views-save-changes'));
const update = await screen.findByTestId(
'saved-views-save-changes-item-update',
);
await waitFor(() => expectRowUsable(update), MENU_CHECK_TIMEOUT);
await user.click(update);
await userEvent.click(update);
await waitFor(() => expect(isDirty()).toBe(false));
await user.click(screen.getByTestId('add-column'));
await userEvent.click(screen.getByTestId('add-column'));
await waitFor(() => expect(isDirty()).toBe(true));
await user.click(screen.getByTestId('saved-views-discard'));
await userEvent.click(screen.getByTestId('saved-views-discard'));
await waitFor(() => expect(columnsText()).toBe('service.name,name,extra-2'));
expect(isDirty()).toBe(false);

View File

@@ -1,16 +1,28 @@
import { Copy, Link, PenLine, Trash2 } from '@signozhq/icons';
import { SavedviewtypesSchemaVersionDTO } from 'api/generated/services/sigNoz.schemas';
import {
SavedviewtypesSchemaVersionDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import {
buildSavedViewDeletePermission,
buildSavedViewUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
import { DataSource } from 'types/common/queryBuilder';
import { SavedViewRowActionConfig, SaveViewModalMode } from './types';
export const SAVED_VIEW_SCHEMA_VERSION = SavedviewtypesSchemaVersionDTO.v2;
// Only the logs and traces explorers have columns and formatting to save.
export const SAVED_VIEW_OPTIONS_DATA_SOURCE: Partial<
Record<SavedviewtypesSourceDTO, DataSource>
> = {
[SavedviewtypesSourceDTO.logs]: DataSource.LOGS,
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
};
// What selecting a view writes to the url, so clearing it removes exactly these.
export const SAVED_VIEW_URL_PARAMS = [
QueryParams.viewKey,

View File

@@ -8,9 +8,10 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { buildSavedViewReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { isPreferenceSourcePage } from 'providers/preferences/utils';
import { DataSource } from 'types/common/queryBuilder';
import { toAPIError } from 'utils/errorUtils';
import { SAVED_VIEW_OPTIONS_DATA_SOURCE } from '../constants';
import { UseActiveSavedViewResult } from '../types';
import { hasUnsavedViewChanges } from '../utils/hasUnsavedViewChanges';
@@ -20,9 +21,9 @@ export function useActiveSavedView(
const { viewKey } = useGetSavedViewParams();
const { stagedQuery, panelType } = useQueryBuilder();
const hasOptions = isPreferenceSourcePage(source);
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
const { options } = useOptionsMenu({
sourcePage: hasOptions ? source : SavedviewtypesSourceDTO.logs,
dataSource: optionsDataSource ?? DataSource.LOGS,
});
const { isLoading: isCheckingRead, deniedPermissions } = useAuthZ(
@@ -62,7 +63,7 @@ export function useActiveSavedView(
view,
stagedQuery,
panelType: panelType ?? PANEL_TYPES.LIST,
options: hasOptions ? options : undefined,
options: optionsDataSource ? options : undefined,
});
}, [
view,
@@ -70,7 +71,7 @@ export function useActiveSavedView(
isStagedQueryCurrent,
stagedQuery,
panelType,
hasOptions,
optionsDataSource,
options,
]);

View File

@@ -20,12 +20,13 @@ import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useAppLocation } from 'lib/router/useAppLocation';
import { usePreferenceSlice } from 'providers/preferences/context/usePreferenceSlice';
import { isPreferenceSourcePage } from 'providers/preferences/utils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { DataSource } from 'types/common/queryBuilder';
import { toAPIError } from 'utils/errorUtils';
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
import {
SAVED_VIEW_OPTIONS_DATA_SOURCE,
SAVED_VIEW_SCHEMA_VERSION,
SAVED_VIEW_TOAST_POSITION,
SAVED_VIEW_URL_PARAMS,
@@ -59,11 +60,15 @@ export function useSavedViewActions(
const { viewKey } = useGetSavedViewParams();
const logSavedViewEvent = useSavedViewLogEvent(source);
const hasOptions = isPreferenceSourcePage(source);
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
const { options } = useOptionsMenu({
sourcePage: hasOptions ? source : SavedviewtypesSourceDTO.logs,
dataSource: optionsDataSource ?? DataSource.LOGS,
});
const preferenceSlice = usePreferenceSlice(source);
const preferences = usePreferenceContext();
const preferencesSlice =
optionsDataSource === DataSource.TRACES
? preferences.traces
: preferences.logs;
const { mutateAsync: createSavedView, isLoading: isCreating } =
useCreateSavedView();
@@ -81,10 +86,10 @@ export function useSavedViewActions(
query: stagedQuery ?? currentQuery,
panelType: panelType ?? PANEL_TYPES.LIST,
displayName,
options: hasOptions ? options : undefined,
options: optionsDataSource ? options : undefined,
color,
}),
[stagedQuery, currentQuery, panelType, hasOptions, options],
[stagedQuery, currentQuery, panelType, optionsDataSource, options],
);
const selectView = useCallback(
@@ -100,14 +105,14 @@ export function useSavedViewActions(
const revertView = useCallback(
(view: SavedviewtypesSavedViewDTO): void => {
if (preferenceSlice) {
if (optionsDataSource) {
const { columns, formatting } = getViewColumnsAndFormatting(
view.spec,
source,
optionsDataSource,
);
preferenceSlice.updateColumns(columns);
preferencesSlice.updateColumns(columns);
if (formatting) {
preferenceSlice.updateFormatting(formatting);
preferencesSlice.updateFormatting(formatting);
}
}
selectView(view);
@@ -115,7 +120,7 @@ export function useSavedViewActions(
[SavedViewEventKeys.ViewId]: view.id,
});
},
[source, preferenceSlice, selectView, logSavedViewEvent],
[optionsDataSource, preferencesSlice, selectView, logSavedViewEvent],
);
// The query builder redirects only ever add params, so the url is rebuilt

View File

@@ -1,7 +1,4 @@
import {
SavedviewtypesSavedViewSpecDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SavedviewtypesSavedViewSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultLogsSelectedColumns,
@@ -10,6 +7,7 @@ import {
} from 'container/OptionsMenu/constants';
import { FontSize, LogViewMode } from 'container/OptionsMenu/types';
import { Preferences } from 'providers/preferences/types';
import { DataSource } from 'types/common/queryBuilder';
function withColumnNames(
columns: TelemetryFieldKey[] | undefined,
@@ -27,11 +25,11 @@ function withColumnNames(
// defaults filled in where the view has none.
export function getViewColumnsAndFormatting(
spec: SavedviewtypesSavedViewSpecDTO | undefined,
sourcePage: SavedviewtypesSourceDTO,
dataSource: DataSource,
): Preferences {
const selectedFields = spec?.selectedFields as TelemetryFieldKey[] | undefined;
if (sourcePage === SavedviewtypesSourceDTO.logs) {
if (dataSource === DataSource.LOGS) {
return {
columns: ensureLogsRequiredColumns(
withColumnNames(selectedFields) || defaultLogsSelectedColumns,
@@ -44,7 +42,7 @@ export function getViewColumnsAndFormatting(
},
};
}
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
return { columns: selectedFields || defaultTraceSelectedColumns };
}
return { columns: [] };

View File

@@ -7,7 +7,6 @@ import { HasUnsavedViewChangesArgs } from '../types';
import { getSavedViewQuery } from './getSavedViewQuery';
import { toSavedViewPanelType } from './toSavedViewSpec';
import { getViewColumnsAndFormatting } from './getViewColumnsAndFormatting';
import { toSavedViewSource } from './toSavedViewSource';
// Legacy context names the server stores under their v5 name.
const FIELD_CONTEXT_ALIASES: Record<string, TelemetrytypesFieldContextDTO> = {
@@ -80,7 +79,7 @@ export function hasUnsavedViewChanges({
const dataSource = stagedQuery.builder.queryData[0]?.dataSource;
const { columns, formatting } = getViewColumnsAndFormatting(
view.spec,
toSavedViewSource(dataSource),
dataSource,
);
const toNames = (fields: { name?: string; key?: string }[]): unknown[] =>
fields.map((field) => field.name ?? field.key);

View File

@@ -3,9 +3,11 @@ import { DataSource } from 'types/common/queryBuilder';
import { SavedViewSourcePage } from '../types';
// The views API files each view under a source page, but callers only know the
// signal they query: a builder query's dataSource, or the AI assistant's hint.
// TODO: remove in a follow-up once callers pass the source page directly.
// Explorers and the preferences module are keyed by DataSource (the signal),
// the api keys views by source page. Same values today, so this is the one
// place they meet. AI observability views will come with their own source and
// DataSource cannot tell them apart from traces, so preferences should move to
// source page at that point and this map goes with it.
const SAVED_VIEW_SOURCE: Record<SavedViewSourcePage, SavedviewtypesSourceDTO> =
{
[DataSource.LOGS]: SavedviewtypesSourceDTO.logs,

View File

@@ -11,7 +11,6 @@ import {
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import logEvent from 'api/common/logEvent';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
import type { TableColumnDef } from 'components/TanStackTableView/types';
@@ -87,7 +86,7 @@ function ListView({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
initialOptions: {
selectColumns: defaultSelectedColumns,

View File

@@ -5,7 +5,7 @@ import { DrawerWrapper } from '@signozhq/ui/drawer';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { EQueryType } from 'types/common/dashboard';
import PanelTypeBrowser from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/components/PanelTypeBrowser/PanelTypeBrowser';
import PanelTypeBrowser from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/PanelTypeBrowser';
import { getPanelDefinition } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { JS_DRIVEN_SLIDE } from '../../../utils/drawerSlide';

View File

@@ -3,7 +3,7 @@ import { ChevronUp, Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import type { SectionOption } from '../../types';
import type { SectionOption } from './types';
import styles from './AddPanelSplitButton.module.scss';

View File

@@ -88,6 +88,76 @@
gap: 12px;
}
.tile {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
border: 1px solid var(--l1-border);
border-radius: 6px;
background: var(--l2-background);
font: inherit;
text-align: left;
cursor: pointer;
transition:
background-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1),
border-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1);
&:hover {
background: var(--l3-background);
}
}
.tileSelected {
border-color: var(--bg-robin-500);
box-shadow: inset 0 0 0 1px var(--bg-robin-500);
background: var(--l3-background);
}
.tileDisabled {
opacity: 0.45;
cursor: not-allowed;
&:hover {
background: var(--l2-background);
}
}
.tileText {
display: flex;
flex-direction: column;
gap: 3px;
}
.tileTitle {
display: flex;
align-items: center;
gap: 7px;
}
.tileName {
color: var(--l1-foreground);
font-size: 13.5px;
font-weight: 500;
}
.newBadge {
padding: 2px 6px;
border: 1px solid var(--bg-sienna-400);
border-radius: 3px;
color: var(--bg-sienna-400);
font-size: 9.5px;
font-weight: 500;
letter-spacing: 0.6px;
text-transform: uppercase;
}
.tileDescription {
color: var(--l2-foreground);
font-size: 12px;
line-height: 1.45;
}
.empty {
display: flex;
flex-direction: column;

View File

@@ -4,12 +4,12 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import cx from 'classnames';
import type { PanelKind } from '../../../../../Panels/types/panelKind';
import type { PanelKind } from '../../../Panels/types/panelKind';
import {
filterPanelTypeGroups,
type PanelTypeGroupId,
} from './panelTypeCatalog';
import PanelTypeTile from '../PanelTypeTile/PanelTypeTile';
import PanelTypeTile from './PanelTypeTile';
import styles from './PanelTypeBrowser.module.scss';

View File

@@ -1,4 +1,4 @@
import type { PanelKind } from '../../../../../Panels/types/panelKind';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { PANEL_TYPE_PREVIEWS } from './panelTypePreviews';
import styles from './PanelTypePreview.module.scss';

View File

@@ -7,11 +7,11 @@ import { releasePanelPickerTarget } from '../../../store/usePanelPickerTargetSto
import type { NewPanelTarget } from '../../../patchOps';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { JS_DRIVEN_SLIDE } from '../../../utils/drawerSlide';
import AddPanelSplitButton from './components/AddPanelSplitButton/AddPanelSplitButton';
import NewSectionNameInput from './components/NewSectionNameInput/NewSectionNameInput';
import PanelTypeBrowser from './components/PanelTypeBrowser/PanelTypeBrowser';
import { usePanelPickerDraftSection } from './hooks/usePanelPickerDraftSection';
import { usePanelPickerTarget } from './hooks/usePanelPickerTarget';
import AddPanelSplitButton from './AddPanelSplitButton';
import NewSectionNameInput from './NewSectionNameInput';
import PanelTypeBrowser from './PanelTypeBrowser';
import { usePanelPickerDraftSection } from './usePanelPickerDraftSection';
import { usePanelPickerTarget } from './usePanelPickerTarget';
import { buildSectionOptions, resolveDefaultSectionValue } from './utils';
import styles from './PanelTypeSelectionModal.module.scss';

View File

@@ -1,10 +1,10 @@
import { Tooltip } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import type { PanelTypeItem } from '../PanelTypeBrowser/panelTypeCatalog';
import PanelTypePreview from '../PanelTypePreview/PanelTypePreview';
import type { PanelTypeItem } from './panelTypeCatalog';
import PanelTypePreview from './PanelTypePreview';
import styles from './PanelTypeTile.module.scss';
import styles from './PanelTypeBrowser.module.scss';
interface PanelTypeTileProps {
item: PanelTypeItem;

View File

@@ -1,69 +0,0 @@
.tile {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
border: 1px solid var(--l1-border);
border-radius: 6px;
background: var(--l2-background);
font: inherit;
text-align: left;
cursor: pointer;
transition:
background-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1),
border-color 200ms cubic-bezier(0.08, 0.52, 0.52, 1);
&:hover {
background: var(--l3-background);
}
}
.tileSelected {
border-color: var(--bg-robin-500);
box-shadow: inset 0 0 0 1px var(--bg-robin-500);
background: var(--l3-background);
}
.tileDisabled {
opacity: 0.45;
cursor: not-allowed;
&:hover {
background: var(--l2-background);
}
}
.tileText {
display: flex;
flex-direction: column;
gap: 3px;
}
.tileTitle {
display: flex;
align-items: center;
gap: 7px;
}
.tileName {
color: var(--l1-foreground);
font-size: 13.5px;
font-weight: 500;
}
.newBadge {
padding: 2px 6px;
border: 1px solid var(--bg-sienna-400);
border-radius: 3px;
color: var(--bg-sienna-400);
font-size: 9.5px;
font-weight: 500;
letter-spacing: 0.6px;
text-transform: uppercase;
}
.tileDescription {
color: var(--l2-foreground);
font-size: 12px;
line-height: 1.45;
}

View File

@@ -1,8 +1,5 @@
import {
PANEL_OPTIONS,
type PanelOption,
} from '../../../../../Panels/registry';
import type { PanelKind } from '../../../../../Panels/types/panelKind';
import { PANEL_OPTIONS, type PanelOption } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
export type PanelTypeGroupId =
| 'trends'

View File

@@ -1,4 +1,4 @@
import type { PanelKind } from '../../../../../Panels/types/panelKind';
import type { PanelKind } from '../../../Panels/types/panelKind';
import PreviewBars from './PreviewBars';
import PreviewRows, { type PreviewCell } from './PreviewRows';

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import type { PanelKind } from '../../../../Panels/types/panelKind';
import { usePanelPickerTargetStore } from '../../../../store/usePanelPickerTargetStore';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { usePanelPickerTargetStore } from '../../../store/usePanelPickerTargetStore';
export function usePanelPickerDraftSection(
title: string | null,

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import type { PanelKind } from '../../../../Panels/types/panelKind';
import { usePanelPickerTargetStore } from '../../../../store/usePanelPickerTargetStore';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { usePanelPickerTargetStore } from '../../../store/usePanelPickerTargetStore';
interface UsePanelPickerTargetArgs {
open: boolean;

View File

@@ -20,7 +20,6 @@ function makeSpan(
event: [],
resource: {},
attributes: {},
scope: {},
...overrides,
};
}

View File

@@ -12,7 +12,6 @@ export const MOCK_SPAN: FlamegraphSpan = {
event: [],
resource: {},
attributes: {},
scope: {},
};
/** Nested spans structure for findSpanById tests */

View File

@@ -518,7 +518,6 @@ const flamegraphSpan = (span: BuiltSpan): SpantypesFlamegraphSpanDTO => ({
level: span.level,
attributes: span.template.attributes,
resource: resourceOf(span),
scope: {},
event: [],
});

View File

@@ -73,7 +73,7 @@ function TracesExplorer(): JSX.Element {
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
initialOptions: {
selectColumns: defaultSelectedColumns,

View File

@@ -1,15 +1,15 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import { LogViewMode } from 'container/OptionsMenu/types';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { FontSize } from 'container/OptionsMenu/types';
import { render, screen, userEvent } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceContext } from '../context/PreferenceContextProvider';
import { PreferenceSourcePage } from '../types';
const ROUTE_LOGS = '/logs';
const ROUTE_TRACES = '/traces';
@@ -38,13 +38,14 @@ function getLocalStorageJSON<T>(key: string): T | null {
}
function Consumer({
sourcePage,
dataSource,
testIdPrefix,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
testIdPrefix: string;
}): JSX.Element {
const slice = usePreferenceContext()[sourcePage];
const ctx = usePreferenceContext();
const slice = dataSource === DataSource.TRACES ? ctx.traces : ctx.logs;
return (
<div>
@@ -57,7 +58,7 @@ function Consumer({
type="button"
onClick={(): void => {
const newCols: TelemetryFieldKey[] =
sourcePage === SavedviewtypesSourceDTO.traces
dataSource === DataSource.TRACES
? (defaultTraceSelectedColumns.slice(0, 1) as TelemetryFieldKey[])
: (defaultLogsSelectedColumns.slice(0, 1) as TelemetryFieldKey[]);
slice.updateColumns(newCols);
@@ -77,10 +78,7 @@ describe('PreferencesProvider integration', () => {
describe('Logs', () => {
it('loads defaults when no localStorage or url provided', () => {
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -103,10 +101,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -121,10 +116,7 @@ describe('PreferencesProvider integration', () => {
it('direct mode updateColumns persists to localStorage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -150,7 +142,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer sourcePage={SavedviewtypesSourceDTO.logs} testIdPrefix="logs" />,
<Consumer dataSource={DataSource.LOGS} testIdPrefix="logs" />,
undefined,
{
initialRoute: initialEntry,
@@ -183,10 +175,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -208,7 +197,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
function FormattingConsumer(): JSX.Element {
const logs = usePreferenceContext()[SavedviewtypesSourceDTO.logs];
const { logs } = usePreferenceContext();
return (
<button
data-testid="logs-update-formatting"
@@ -246,10 +235,7 @@ describe('PreferencesProvider integration', () => {
const initialEntry = `/logs?viewKey=${encodeURIComponent(viewKey)}`;
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{ initialRoute: initialEntry },
);
@@ -268,10 +254,7 @@ describe('PreferencesProvider integration', () => {
describe('Traces', () => {
it('loads defaults when no localStorage or url provided', () => {
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -290,10 +273,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -306,10 +286,7 @@ describe('PreferencesProvider integration', () => {
it('direct mode updateColumns persists to localStorage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -332,10 +309,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix="traces"
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix="traces" />,
undefined,
{
initialRoute: initialEntry,
@@ -364,10 +338,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{ initialRoute: ROUTE_TRACES },
);
@@ -384,7 +355,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
function TracesFormattingConsumer(): JSX.Element {
const traces = usePreferenceContext()[SavedviewtypesSourceDTO.traces];
const { traces } = usePreferenceContext();
return (
<button
data-testid="traces-update-formatting"
@@ -419,10 +390,7 @@ describe('PreferencesProvider integration', () => {
const initialEntry = `/traces?viewKey=${encodeURIComponent(viewKey)}`;
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{ initialRoute: initialEntry },
);

View File

@@ -1,9 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { renderHook, waitFor } from '@testing-library/react';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { FontSize } from 'container/OptionsMenu/types';
import { renderHook, waitFor } from 'tests/test-utils';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import logsLoaderConfig from '../configs/logsLoaderConfig';
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
@@ -45,18 +42,15 @@ jest.mock('../configs/tracesLoaderConfig', () => ({
}));
describe('usePreferenceLoader', () => {
const setReSync = jest.fn() as jest.MockedFunction<
Dispatch<SetStateAction<boolean>>
>;
beforeEach(() => {
jest.clearAllMocks();
});
it('should load logs preferences based on priority order', async () => {
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: false,
setReSync,
}),
@@ -77,9 +71,10 @@ describe('usePreferenceLoader', () => {
});
it('should load traces preferences', async () => {
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
reSync: false,
setReSync,
}),
@@ -98,11 +93,13 @@ describe('usePreferenceLoader', () => {
});
it('should call setReSync when reSync is true', async () => {
const setReSync = jest.fn();
// Test that the hook calls setReSync(false) when reSync is true
// We'll unmount quickly to avoid the infinite loop
const { unmount } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: true,
setReSync,
}),
@@ -121,25 +118,19 @@ describe('usePreferenceLoader', () => {
it('should handle errors during loading', async () => {
// Make first call succeed (initial state), second call throw in reSync effect
const localSpy = jest.spyOn(logsLoaderConfig, 'local');
// The loader reads stored columns by name, though `local()` types them as
// the older key-based shape.
const localSpy: jest.SpyInstance = jest.spyOn(logsLoaderConfig, 'local');
localSpy.mockImplementationOnce(() => ({
columns: [{ name: 'local-column' }] as unknown as BaseAutocompleteData[],
formatting: {
maxLines: 5,
format: 'table',
fontSize: FontSize.MEDIUM,
version: 1,
},
columns: [{ name: 'local-column' }],
formatting: { maxLines: 5, format: 'table', fontSize: 'medium', version: 1 },
}));
localSpy.mockImplementationOnce(() => {
throw new Error('Loading failed');
});
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: true,
setReSync,
}),

View File

@@ -1,3 +1,4 @@
import { renderHook } from '@testing-library/react';
import { useGetSavedView } from 'api/generated/services/saved-view';
import {
SavedviewtypesSavedViewDTO,
@@ -8,21 +9,32 @@ import {
defaultTraceSelectedColumns,
ensureLogsRequiredColumns,
} from 'container/OptionsMenu/constants';
import { renderHook } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
import { usePreferenceSync } from '../sync/usePreferenceSync';
import { PreferenceMode, Preferences } from '../types';
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
import { PreferenceMode } from '../types';
jest.mock('api/generated/services/saved-view');
jest.mock('../loader/usePreferenceLoader');
jest.mock('../updater/usePreferenceUpdater');
const loaderPreferences = { columns: [{ name: 'from-loader' }] };
jest.mock('../loader/usePreferenceLoader', () => ({
usePreferenceLoader: jest.fn(() => ({
preferences: loaderPreferences,
loading: false,
error: null,
})),
}));
const loaderPreferences: Preferences = { columns: [{ name: 'from-loader' }] };
jest.mock('../updater/usePreferenceUpdater', () => ({
usePreferenceUpdater: jest.fn(() => ({
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
})),
}));
const mockedUseGetSavedView = jest.mocked(useGetSavedView);
const mockedUseGetSavedView = useGetSavedView as jest.MockedFunction<
typeof useGetSavedView
>;
function makeView(
id: string,
@@ -64,19 +76,6 @@ function mockViews(views: SavedviewtypesSavedViewDTO[]): void {
describe('usePreferenceSync in saved view mode', () => {
beforeEach(() => {
mockedUseGetSavedView.mockReset();
jest.mocked(usePreferenceLoader).mockReturnValue({
preferences: loaderPreferences,
loading: false,
error: null,
});
jest.mocked(usePreferenceUpdater).mockReturnValue({
updateColumns: jest.fn() as jest.MockedFunction<
ReturnType<typeof usePreferenceUpdater>['updateColumns']
>,
updateFormatting: jest.fn() as jest.MockedFunction<
ReturnType<typeof usePreferenceUpdater>['updateFormatting']
>,
});
});
it('fetches the view only in saved view mode with a view id', () => {
@@ -85,7 +84,7 @@ describe('usePreferenceSync in saved view mode', () => {
renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.DIRECT,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: undefined,
}),
);
@@ -102,7 +101,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.DIRECT,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: undefined,
}),
);
@@ -121,7 +120,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'view-1',
}),
);
@@ -148,7 +147,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'view-1',
}),
);
@@ -175,14 +174,14 @@ describe('usePreferenceSync in saved view mode', () => {
const withFields = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
savedViewId: 'with-fields',
}),
);
const withoutFields = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
savedViewId: 'without-fields',
}),
);
@@ -203,7 +202,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'missing',
}),
);

View File

@@ -1,39 +1,45 @@
import { Dispatch, SetStateAction } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize } from 'container/OptionsMenu/types';
import {
FormattingOptions,
PreferenceMode,
Preferences,
} from 'providers/preferences/types';
import { act, renderHook } from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
jest.mock('../configs/logsUpdaterConfig');
jest.mock('../configs/tracesUpdaterConfig');
jest.mock('hooks/useUrlQueryData');
// Mock the config updaters
const mockUpdateColumns = jest.fn();
const mockUpdateFormatting = jest.fn();
type LogsUpdater = ReturnType<typeof getLogsUpdaterConfig>;
type TracesUpdater = ReturnType<typeof getTracesUpdaterConfig>;
jest.mock('../configs/logsUpdaterConfig', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
})),
}));
const mockLogsUpdateColumns = jest.fn() as jest.MockedFunction<
LogsUpdater['updateColumns']
>;
const mockLogsUpdateFormatting = jest.fn() as jest.MockedFunction<
LogsUpdater['updateFormatting']
>;
const mockTracesUpdateColumns = jest.fn() as jest.MockedFunction<
TracesUpdater['updateColumns']
>;
const mockTracesUpdateFormatting = jest.fn() as jest.MockedFunction<
TracesUpdater['updateFormatting']
>;
jest.mock('../configs/tracesUpdaterConfig', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
})),
}));
// Mock the URL query hook
jest.mock('hooks/useUrlQueryData', () => ({
__esModule: true,
default: jest.fn().mockReturnValue({
redirectWithQuery: jest.fn(),
}),
}));
describe('usePreferenceUpdater', () => {
const mockPreferences: Preferences = {
@@ -45,36 +51,18 @@ describe('usePreferenceUpdater', () => {
version: 1,
},
};
const setReSync = jest.fn() as jest.MockedFunction<
Dispatch<SetStateAction<boolean>>
>;
const setSavedViewPreferences = jest.fn() as jest.MockedFunction<
Dispatch<SetStateAction<Preferences | null>>
>;
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(getLogsUpdaterConfig).mockReturnValue({
updateColumns: mockLogsUpdateColumns,
updateFormatting: mockLogsUpdateFormatting,
});
jest.mocked(getTracesUpdaterConfig).mockReturnValue({
updateColumns: mockTracesUpdateColumns,
updateFormatting: mockTracesUpdateFormatting,
});
jest.mocked(useUrlQueryData).mockReturnValue({
query: null,
queryData: undefined,
redirectWithQuery: jest.fn() as jest.MockedFunction<
(newQueryData: unknown) => void
>,
});
});
it('should return updateColumns and updateFormatting functions', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -87,7 +75,9 @@ describe('usePreferenceUpdater', () => {
expect(typeof result.current.updateFormatting).toBe('function');
});
it('should call the logs updater for updateColumns with the logs list key', () => {
it('should call the logs updater for updateColumns with logs dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newColumns: TelemetryFieldKey[] = [
{
name: 'new-column',
@@ -98,7 +88,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -111,15 +101,16 @@ describe('usePreferenceUpdater', () => {
});
// Should call the logs updater
expect(mockLogsUpdateColumns).toHaveBeenCalledWith(
expect(mockUpdateColumns).toHaveBeenCalledWith(
newColumns,
PreferenceMode.DIRECT,
);
expect(mockTracesUpdateColumns).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the logs updater for updateFormatting with the logs list key', () => {
it('should call the logs updater for updateFormatting with logs dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newFormatting: FormattingOptions = {
maxLines: 10,
format: 'table' as LogViewMode,
@@ -129,7 +120,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -142,15 +133,16 @@ describe('usePreferenceUpdater', () => {
});
// Should call the logs updater
expect(mockLogsUpdateFormatting).toHaveBeenCalledWith(
expect(mockUpdateFormatting).toHaveBeenCalledWith(
newFormatting,
PreferenceMode.DIRECT,
);
expect(mockTracesUpdateFormatting).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the traces updater for updateColumns with the traces list key', () => {
it('should call the traces updater for updateColumns with traces dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newColumns: TelemetryFieldKey[] = [
{
name: 'new-trace-column',
@@ -161,7 +153,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -174,15 +166,16 @@ describe('usePreferenceUpdater', () => {
});
// Should call the traces updater
expect(mockTracesUpdateColumns).toHaveBeenCalledWith(
expect(mockUpdateColumns).toHaveBeenCalledWith(
newColumns,
PreferenceMode.DIRECT,
);
expect(mockLogsUpdateColumns).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the traces updater for updateFormatting with the traces list key', () => {
it('should call the traces updater for updateFormatting with traces dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newFormatting: FormattingOptions = {
maxLines: 10,
format: 'table' as LogViewMode,
@@ -192,7 +185,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -205,18 +198,20 @@ describe('usePreferenceUpdater', () => {
});
// Should call the traces updater
expect(mockTracesUpdateFormatting).toHaveBeenCalledWith(
expect(mockUpdateFormatting).toHaveBeenCalledWith(
newFormatting,
PreferenceMode.DIRECT,
);
expect(mockLogsUpdateFormatting).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should increment reSync counter when updates are called', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,

View File

@@ -1,20 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { isPreferenceSourcePage } from '../utils';
describe('isPreferenceSourcePage', () => {
it.each([SavedviewtypesSourceDTO.logs, SavedviewtypesSourceDTO.traces])(
'is true for %s',
(source) => {
expect(isPreferenceSourcePage(source)).toBe(true);
},
);
it.each([
SavedviewtypesSourceDTO.metrics,
SavedviewtypesSourceDTO.meter,
SavedviewtypesSourceDTO.ai_observability,
])('is false for %s', (source) => {
expect(isPreferenceSourcePage(source)).toBe(false);
});
});

View File

@@ -1,7 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
// Only the logs and traces explorers keep column and formatting preferences.
export const PREFERENCE_SOURCE_PAGES = [
SavedviewtypesSourceDTO.logs,
SavedviewtypesSourceDTO.traces,
] as const;

View File

@@ -5,12 +5,12 @@ import React, {
useContext,
useMemo,
} from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import useUrlQuery from 'hooks/useUrlQuery';
import {
PreferenceContextValue,
PreferenceMode,
} from 'providers/preferences/types';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceSync } from '../sync/usePreferenceSync';
@@ -38,19 +38,19 @@ export function PreferenceContextProvider({
const logsSlice = usePreferenceSync({
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
savedViewId: savedViewId || undefined,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
});
const tracesSlice = usePreferenceSync({
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
savedViewId: savedViewId || undefined,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
});
const value = useMemo<PreferenceContextValue>(
() => ({
[SavedviewtypesSourceDTO.logs]: logsSlice,
[SavedviewtypesSourceDTO.traces]: tracesSlice,
logs: logsSlice,
traces: tracesSlice,
}),
[logsSlice, tracesSlice],
);

View File

@@ -1,12 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { PreferenceSlice } from '../types';
import { isPreferenceSourcePage } from '../utils';
import { usePreferenceContext } from './PreferenceContextProvider';
export function usePreferenceSlice(
sourcePage: SavedviewtypesSourceDTO,
): PreferenceSlice | undefined {
const context = usePreferenceContext();
return isPreferenceSourcePage(sourcePage) ? context[sourcePage] : undefined;
}

View File

@@ -1,12 +1,12 @@
import { useEffect, useState } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { ensureLogsRequiredColumns } from 'container/OptionsMenu/constants';
import { has } from 'lodash-es';
import { DataSource } from 'types/common/queryBuilder';
import logsLoaderConfig from '../configs/logsLoaderConfig';
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
import { FormattingOptions, Preferences, PreferenceSourcePage } from '../types';
import { FormattingOptions, Preferences } from '../types';
const migrateColumns = (columns: any): any =>
columns.map((column: any) => {
@@ -62,21 +62,15 @@ function logsPreferencesLoader(): {
function tracesPreferencesLoader(): {
columns: TelemetryFieldKey[];
} {
const { columns } = preferencesLoader<Preferences>(tracesLoaderConfig);
return { columns };
return preferencesLoader(tracesLoaderConfig);
}
const PREFERENCE_LOADERS: Record<PreferenceSourcePage, () => Preferences> = {
[SavedviewtypesSourceDTO.logs]: logsPreferencesLoader,
[SavedviewtypesSourceDTO.traces]: tracesPreferencesLoader,
};
export function usePreferenceLoader({
sourcePage,
dataSource,
reSync,
setReSync,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
reSync: boolean;
setReSync: (value: boolean) => void;
}): {
@@ -84,10 +78,17 @@ export function usePreferenceLoader({
loading: boolean;
error: Error | null;
} {
const loadSlicePreferences = PREFERENCE_LOADERS[sourcePage];
const [preferences, setPreferences] = useState<Preferences | null>(
loadSlicePreferences,
);
const [preferences, setPreferences] = useState<Preferences | null>(() => {
if (dataSource === DataSource.LOGS) {
const { columns, formatting } = logsPreferencesLoader();
return { columns, formatting };
}
if (dataSource === DataSource.TRACES) {
const { columns } = tracesPreferencesLoader();
return { columns };
}
return null;
});
const [error, setError] = useState<Error | null>(null);
useEffect((): void => {
@@ -95,7 +96,15 @@ export function usePreferenceLoader({
setError(null);
try {
setPreferences(loadSlicePreferences());
if (dataSource === DataSource.LOGS) {
const { columns, formatting } = logsPreferencesLoader();
setPreferences({ columns, formatting });
}
if (dataSource === DataSource.TRACES) {
const { columns } = tracesPreferencesLoader();
setPreferences({ columns });
}
} catch (e) {
setError(e as Error);
} finally {
@@ -110,7 +119,7 @@ export function usePreferenceLoader({
if (reSync) {
loadPreferences();
}
}, [loadSlicePreferences, reSync, setReSync]);
}, [dataSource, reSync, setReSync]);
return { preferences, loading: false, error };
}

View File

@@ -3,23 +3,19 @@ import { useEffect, useRef, useState } from 'react';
import { useGetSavedView } from 'api/generated/services/saved-view';
import { TelemetryFieldKey } from 'api/v5/v5';
import { getViewColumnsAndFormatting } from 'container/SavedViews/utils/getViewColumnsAndFormatting';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
import {
FormattingOptions,
PreferenceMode,
Preferences,
PreferenceSourcePage,
} from '../types';
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
export function usePreferenceSync({
mode,
sourcePage,
dataSource,
savedViewId,
}: {
mode: PreferenceMode;
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
savedViewId: string | undefined;
}): {
preferences: Preferences | null;
@@ -45,25 +41,25 @@ export function usePreferenceSync({
if (view && view.id === savedViewId) {
if (seededViewId.current !== savedViewId) {
seededViewId.current = savedViewId;
setSavedViewPreferences(getViewColumnsAndFormatting(view.spec, sourcePage));
setSavedViewPreferences(getViewColumnsAndFormatting(view.spec, dataSource));
}
return;
}
seededViewId.current = undefined;
setSavedViewPreferences(getViewColumnsAndFormatting(undefined, sourcePage));
}, [viewData, sourcePage, savedViewId]);
setSavedViewPreferences(getViewColumnsAndFormatting(undefined, dataSource));
}, [viewData, dataSource, savedViewId]);
// We are using a reSync state because we have URL updates as well as local storage updates
// and we want to make sure we are always using the latest preferences
const [reSync, setReSync] = useState(false);
const { preferences, loading, error } = usePreferenceLoader({
sourcePage,
dataSource,
reSync,
setReSync,
});
const { updateColumns, updateFormatting } = usePreferenceUpdater({
sourcePage,
dataSource,
mode,
preferences,
setReSync,

View File

@@ -1,15 +1,12 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import { PREFERENCE_SOURCE_PAGES } from '../constants';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize } from 'container/OptionsMenu/types';
export enum PreferenceMode {
SAVED_VIEW = 'savedView',
DIRECT = 'direct',
}
export type PreferenceSourcePage = (typeof PREFERENCE_SOURCE_PAGES)[number];
export interface PreferenceSlice {
preferences: Preferences | null;
loading: boolean;
@@ -18,10 +15,10 @@ export interface PreferenceSlice {
updateFormatting: (newFormatting: FormattingOptions) => void;
}
export type PreferenceContextValue = Record<
PreferenceSourcePage,
PreferenceSlice
>;
export interface PreferenceContextValue {
logs: PreferenceSlice;
traces: PreferenceSlice;
}
export interface FormattingOptions {
maxLines?: number;

View File

@@ -1,5 +1,4 @@
import { Dispatch, SetStateAction } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultOptionsQuery,
@@ -7,41 +6,48 @@ import {
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { DataSource } from 'types/common/queryBuilder';
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
import { FormattingOptions, Preferences, PreferenceSourcePage } from '../types';
import { FormattingOptions, Preferences } from '../types';
const metricsUpdater = {
updateColumns: (): void => {}, // no-op for metrics
updateFormatting: (): void => {}, // no-op for metrics
};
const getUpdaterConfig = (
preferences: Preferences | null,
redirectWithOptionsData: (options: OptionsQuery) => void,
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
): Record<
PreferenceSourcePage,
DataSource,
{
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
}
> => ({
[SavedviewtypesSourceDTO.logs]: getLogsUpdaterConfig(
[DataSource.LOGS]: getLogsUpdaterConfig(
preferences,
redirectWithOptionsData,
setSavedViewPreferences,
),
[SavedviewtypesSourceDTO.traces]: getTracesUpdaterConfig(
[DataSource.TRACES]: getTracesUpdaterConfig(
redirectWithOptionsData,
setSavedViewPreferences,
),
[DataSource.METRICS]: metricsUpdater,
});
export function usePreferenceUpdater({
sourcePage,
dataSource,
mode,
preferences,
setReSync,
setSavedViewPreferences,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
mode: string;
preferences: Preferences | null;
setReSync: Dispatch<SetStateAction<boolean>>;
@@ -56,7 +62,7 @@ export function usePreferenceUpdater({
preferences,
redirectWithOptionsData,
setSavedViewPreferences,
)[sourcePage];
)[dataSource];
return {
updateColumns: (newColumns: TelemetryFieldKey[]): void => {

View File

@@ -1,12 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { PREFERENCE_SOURCE_PAGES } from './constants';
import { PreferenceSourcePage } from './types';
export function isPreferenceSourcePage(
sourcePage: SavedviewtypesSourceDTO,
): sourcePage is PreferenceSourcePage {
return (
PREFERENCE_SOURCE_PAGES as readonly SavedviewtypesSourceDTO[]
).includes(sourcePage);
}

View File

@@ -1,33 +0,0 @@
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { type MetricSeed, metricSeed } from './metrics';
/**
* One metric per type the query builder treats differently: the type decides
* the default time and space aggregation and which add-ons are offered.
*/
export const QUERY_BUILDER_METRICS: readonly MetricSeed[] = [
metricSeed('signoz_calls_total', 'Calls per operation'),
metricSeed('signoz_latency_count', 'Latency sample count'),
metricSeed(
'container_cpu_utilization',
'Container CPU utilisation',
'percent',
MetrictypesTypeDTO.gauge,
MetrictypesTemporalityDTO.unspecified,
),
metricSeed(
'signoz_latency_bucket',
'Latency histogram',
'ms',
MetrictypesTypeDTO.histogram,
),
];

View File

@@ -8,16 +8,10 @@ import { rest } from 'msw';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, SIGNAL_TO_DATA_SOURCE } from 'types/common/queryBuilder';
import { fieldValuesResponse } from './__story_mockdata__/fields';
import {
listMetricsResponse,
metricMetadataResponse,
} from './__story_mockdata__/metrics';
import {
autocompleteKeysResponse,
autocompleteValuesResponse,
} from './__story_mockdata__/queryBuilderAutocomplete';
import { QUERY_BUILDER_METRICS } from './__story_mockdata__/queryBuilderMetrics';
/**
* The v5 keys list, served from the same per-signal fixture as the v3 pair so
@@ -105,61 +99,6 @@ export const queryBuilderHandlers = [
),
),
),
// The AI query builder pins traces and reads the same shapes from its own
// endpoints.
rest.get(
'http://localhost/api/v1/ai_observability/fields/keys',
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json(
fieldKeysResponseForSignal(
TelemetrytypesSignalDTO.traces,
req.url.searchParams.get('searchText') ?? '',
),
),
),
),
...[
'http://localhost/api/v1/fields/values',
'http://localhost/api/v1/ai_observability/fields/values',
].map((url) =>
rest.get(url, (req, res, ctx) =>
res(
ctx.status(200),
ctx.json(
fieldValuesResponse(
autocompleteValuesResponse(
req.url.searchParams.get('name'),
req.url.searchParams.get('searchText') ?? '',
).data.stringAttributeValues ?? [],
),
),
),
),
),
rest.get('http://localhost/api/v2/metrics', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json(
listMetricsResponse(
QUERY_BUILDER_METRICS,
req.url.searchParams.get('searchText') ?? '',
),
),
),
),
rest.get('http://localhost/api/v2/metrics/metadata', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json(
metricMetadataResponse(
QUERY_BUILDER_METRICS,
req.url.searchParams.get('metricName') ?? '',
),
),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(req, res, ctx) => {

2
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/clickhouse-go-mock v0.14.0
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.144.6
github.com/SigNoz/signoz-otel-collector v0.144.16-0.20261102045710-f531882cb133
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/cespare/xxhash/v2 v2.3.0

4
go.sum
View File

@@ -100,8 +100,8 @@ github.com/SigNoz/expr v1.17.7-beta h1:FyZkleM5dTQ0O6muQfwGpoH5A2ohmN/XTasRCO72g
github.com/SigNoz/expr v1.17.7-beta/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/signoz-otel-collector v0.144.6 h1:AgblCIDlLKGtssS4ywvyEByH9fbJ7hFqZvPvUVJKwp8=
github.com/SigNoz/signoz-otel-collector v0.144.6/go.mod h1:YRQw6ETaq7fm8ts4Itgb6wC0bVhc0+nC6PFI0MaiH9k=
github.com/SigNoz/signoz-otel-collector v0.144.16-0.20261102045710-f531882cb133 h1:45SI7q1cTPE/rjDfLevdlTprXHiKcT5YiH5b48WdXCU=
github.com/SigNoz/signoz-otel-collector v0.144.16-0.20261102045710-f531882cb133/go.mod h1:aOTO7iet9Ga9d3v/EkgeSZdSXEkjgVq2L0pK4tjgTEE=
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=

View File

@@ -4,12 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"maps"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
@@ -25,14 +22,12 @@ import (
const unmappedModelsLookback = time.Hour
type module struct {
store llmpricingruletypes.Store
querier querier.Querier
telemetryMetadataStore telemetrytypes.MetadataStore
logger *slog.Logger
store llmpricingruletypes.Store
querier querier.Querier
}
func NewModule(store llmpricingruletypes.Store, querier querier.Querier, telemetryMetadataStore telemetrytypes.MetadataStore, providerSettings factory.ProviderSettings) llmpricingrule.Module {
return &module{store: store, querier: querier, telemetryMetadataStore: telemetryMetadataStore, logger: providerSettings.Logger}
func NewModule(store llmpricingruletypes.Store, querier querier.Querier) llmpricingrule.Module {
return &module{store: store, querier: querier}
}
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
@@ -56,36 +51,13 @@ func (module *module) ListUnmappedModels(ctx context.Context, orgID valuer.UUID)
return nil, err
}
return llmpricingruletypes.UnmappedModels(models, rules), nil
}
// Collect keeps the rule counts even when ClickHouse is unreachable: the
// counts that depend on it are skipped so the reporter retains their last value.
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
rules, err := module.listAllRules(ctx, orgID)
if err != nil {
return nil, err
}
stats := llmpricingruletypes.NewStatsFromRules(rules)
if models, err := module.discoverModels(ctx, orgID); err != nil {
module.logger.DebugContext(ctx, "failed to collect unmapped model stats", errors.Attr(err))
} else {
maps.Copy(stats, llmpricingruletypes.NewStatsFromUnmappedModels(llmpricingruletypes.UnmappedModels(models, rules)))
}
values := make(map[string]*telemetrytypes.TelemetryFieldValues)
for _, selector := range aiobservabilitytypes.StatsFieldValueSelectors(time.Now()) {
fieldValues, _, err := module.telemetryMetadataStore.GetAllValues(ctx, orgID, selector)
if err != nil {
module.logger.DebugContext(ctx, "failed to collect gen_ai attribute stats", slog.String("key", selector.Name), errors.Attr(err))
continue
unmapped := make([]*llmpricingruletypes.UnmappedModel, 0, len(models))
for _, m := range models {
if !llmpricingruletypes.ModelMatchesAnyRule(m.ModelName, rules) {
unmapped = append(unmapped, m)
}
values[selector.Name] = fieldValues
}
maps.Copy(stats, aiobservabilitytypes.NewStatsFromFieldValues(values))
return stats, nil
return unmapped, nil
}
// CreateOrUpdate saves a batch of pricing rules. isOverride decides how a rule

View File

@@ -5,7 +5,6 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -14,8 +13,6 @@ type Module interface {
// Since this module interacts with OpAMP, it must implement the AgentFeature interface.
agentConf.AgentFeature
statsreporter.StatsCollector
List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error)
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []*llmpricingruletypes.UpdatableLLMPricingRule) (err error)

View File

@@ -28,7 +28,7 @@ const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$
// JSON holds the attributes.
var storableSpanColumns = []string{
"DISTINCT ON (span_id) timestamp", "duration_nano", "span_id", "has_error", "kind",
colServiceName, "name", "attributes", "resources_string", "scope",
colServiceName, "name", "attributes", "resources_string",
"if(notEmpty(attributes), map(), attributes_string) AS attributes_string",
"if(notEmpty(attributes), map(), attributes_number) AS attributes_number",
"if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool",
@@ -382,7 +382,6 @@ func (s *traceStore) GetFlamegraphSpans(ctx context.Context, traceID string, sta
"if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number",
"if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool",
"any(resources_string) AS resources_string",
"any(scope) AS scope",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
conditions := []string{

View File

@@ -154,7 +154,7 @@ func TestGetSpanCountByField(t *testing.T) {
}
func TestGetTraceSpans(t *testing.T) {
expectedSQL := "SELECT DISTINCT ON (span_id) timestamp, duration_nano, span_id, has_error, kind, resource_string_service$$name, name, attributes, resources_string, scope, if(notEmpty(attributes), map(), attributes_string) AS attributes_string, if(notEmpty(attributes), map(), attributes_number) AS attributes_number, if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool, events, status_message, status_code_string, kind_string, parent_span_id, flags, is_remote, trace_state, status_code, db_name, db_operation, http_method, http_url, http_host, external_http_method, external_http_url, response_status_code, links as references FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp ASC, name ASC"
expectedSQL := "SELECT DISTINCT ON (span_id) timestamp, duration_nano, span_id, has_error, kind, resource_string_service$$name, name, attributes, resources_string, if(notEmpty(attributes), map(), attributes_string) AS attributes_string, if(notEmpty(attributes), map(), attributes_number) AS attributes_number, if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool, events, status_message, status_code_string, kind_string, parent_span_id, flags, is_remote, trace_state, status_code, db_name, db_operation, http_method, http_url, http_host, external_http_method, external_http_url, response_status_code, links as references FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp ASC, name ASC"
s := newTestStore(sqlmock.QueryMatcherEqual)
s.Mock().ExpectSelect(expectedSQL).
@@ -171,12 +171,12 @@ func TestGetFlamegraphSpans(t *testing.T) {
}{
{
name: "NoSpanIDs_GeneratesBaseSQL",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string, any(scope) AS scope FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY span_id ORDER BY timestamp ASC, name ASC",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY span_id ORDER BY timestamp ASC, name ASC",
},
{
name: "WithSpanIDs_GeneratesInClauseSQL",
spanIDs: []string{"span-1", "span-2"},
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string, any(scope) AS scope FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND span_id IN (?, ?) GROUP BY span_id ORDER BY timestamp ASC, name ASC",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND span_id IN (?, ?) GROUP BY span_id ORDER BY timestamp ASC, name ASC",
},
}

View File

@@ -43,15 +43,13 @@ func stripKeyAlias(name string) string {
// unwrapVariant decodes a scan envelope: the chcol.Variant the driver scans a Dynamic column (a JSON
// path such as body_v2.level) into, and the JSONValue a whole JSON column scans into. The attributes
// bag decodes flat so its dotted keys merge with the legacy attribute maps and a key stored at both a
// scalar and an object path stays two keys; the scope decodes flat too, keeping its attributes dotted;
// every other JSON column decodes nested.
// scalar and an object path stays two keys; every other JSON column decodes nested.
func unwrapVariant(name string, val any) any {
switch v := val.(type) {
case chcol.Variant:
return v.Any()
case telemetrystoretypes.JSONValue:
switch name {
case "attributes", "scope":
if name == "attributes" {
return v.Flatten()
}
return v.Nest()
@@ -628,9 +626,6 @@ func mergeSpanAttributeColumns(data map[string]any) {
if raw, ok := data["links"]; ok {
data["links"] = spantypes.ParseLinks(raw)
}
if column, ok := data["scope"].(map[string]any); ok {
data["scope"] = spantypes.NewScope(column)
}
}
// numericAsFloat converts numeric types to float64 efficiently.

View File

@@ -95,9 +95,6 @@ func jsonColumn(paths map[string]any) telemetrystoretypes.JSONValue {
func TestUnwrapVariant(t *testing.T) {
jsonCol := jsonColumn(map[string]any{"level": "error", "attrs.code": int64(500)})
nullCol := jsonColumn(map[string]any{"level": "error", "absent": chcol.Variant{}})
scopeAttrs := chcol.NewJSON()
scopeAttrs.SetValueAtPath("lib.lang", "go")
scopeCol := jsonColumn(map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": scopeAttrs})
testCases := []struct {
name string
@@ -111,7 +108,6 @@ func TestUnwrapVariant(t *testing.T) {
{name: "JSONColumnNested", column: "body_v2", input: jsonCol, want: map[string]any{"level": "error", "attrs": map[string]any{"code": float64(500)}}},
{name: "AttributesColumnFlat", column: "attributes", input: jsonCol, want: map[string]any{"level": "error", "attrs.code": float64(500)}},
{name: "AttributesColumnDropsNullPaths", column: "attributes", input: nullCol, want: map[string]any{"level": "error"}},
{name: "ScopeColumnFlat", column: "scope", input: scopeCol, want: map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}}},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -136,32 +132,6 @@ func TestMergeSpanAttributeColumns_EmptyEventsAndLinks(t *testing.T) {
}
}
func TestMergeSpanAttributeColumns_Scope(t *testing.T) {
testCases := []struct {
name string
scope map[string]any
want spantypes.Scope
}{
{
name: "SetFields_Kept",
scope: map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}},
want: spantypes.Scope{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}},
},
{
name: "UnsetDeclaredPaths_Empty",
scope: map[string]any{"name": "", "version": "", "attributes": map[string]any{}},
want: spantypes.Scope{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
data := map[string]any{"scope": testCase.scope}
mergeSpanAttributeColumns(data)
assert.Equal(t, testCase.want, data["scope"])
})
}
}
// readAsRaw decodes the JSON column into a flat bag of dotted keys before this runs; mergeSpanAttributeColumns
// copies that bag and overlays the legacy string/number/bool maps, which win on collision.
func TestMergeSpanAttributeColumns_JSONColumn(t *testing.T) {

View File

@@ -163,7 +163,7 @@ func NewModules(
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore, telemetryMetadataStore, fl), providerSettings, config.TraceDetail),
SpanMapper: spanMapper,
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier, telemetryMetadataStore, providerSettings),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier),
Tag: tagModule,
}
}

View File

@@ -267,7 +267,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddGCPConnectionMethodFactory(),
sqlmigration.NewDeleteOrphanSARolesFactory(),
sqlmigration.NewAddRoutePolicyTuplesFactory(sqlstore),
sqlmigration.NewRenameSavedViewDeprecatedTraceFieldsFactory(),
)
}

View File

@@ -570,7 +570,6 @@ func New(
cloudIntegrationModule,
modules.LogsPipeline,
modules.InfraMonitoring,
modules.LLMPricingRule,
querier,
authz,
}

View File

@@ -1,173 +0,0 @@
package sqlmigration
import (
"context"
"encoding/json"
"log/slog"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
)
// deprecatedTraceFields maps the legacy camelCase trace fields to their current name.
var deprecatedTraceFields = map[string]string{
"traceID": "trace_id",
"spanID": "span_id",
"parentSpanID": "parent_span_id",
"spanKind": "kind_string",
"durationNano": "duration_nano",
"statusCode": "status_code",
"statusMessage": "status_message",
"statusCodeString": "status_code_string",
"responseStatusCode": "response_status_code",
"externalHttpUrl": "external_http_url",
"httpUrl": "http_url",
"externalHttpMethod": "external_http_method",
"httpMethod": "http_method",
"httpHost": "http_host",
"dbName": "db_name",
"dbOperation": "db_operation",
"hasError": "has_error",
"isRemote": "is_remote",
"serviceName": "service.name",
"httpRoute": "http.route",
"msgSystem": "messaging.system",
"msgOperation": "messaging.operation",
"dbSystem": "db.system",
"rpcSystem": "rpc.system",
"rpcService": "rpc.service",
"rpcMethod": "rpc.method",
"peerService": "peer.service",
}
func NewRenameSavedViewDeprecatedTraceFieldsFactory() factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("rename_saved_view_trace_fields"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &renameSavedViewDeprecatedTraceFields{settings: ps}, nil
})
}
type storableSavedViewTraceFieldsRow struct {
bun.BaseModel `bun:"table:saved_view"`
ID string `bun:"id,pk,type:text"`
Source string `bun:"source,type:text"`
Data string `bun:"data,type:text"`
}
type renameSavedViewDeprecatedTraceFields struct {
settings factory.ProviderSettings
}
func (migration *renameSavedViewDeprecatedTraceFields) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *renameSavedViewDeprecatedTraceFields) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var rows []*storableSavedViewTraceFieldsRow
if err := tx.NewSelect().Model(&rows).Where("source = ?", "traces").Scan(ctx); err != nil {
return err
}
var renamed, skipped int
for _, row := range rows {
renamedData, changed, err := renameDeprecatedTraceFields(row.Data)
if err != nil {
migration.settings.Logger.WarnContext(ctx, "saved view data could not be parsed, leaving it untouched", slog.String("saved_view_id", row.ID), slog.Any("error", err))
skipped++
continue
}
if !changed {
continue
}
renamed++
if _, err := tx.NewUpdate().Model((*storableSavedViewTraceFieldsRow)(nil)).Set("data = ?", renamedData).Where("id = ?", row.ID).Exec(ctx); err != nil {
return err
}
}
migration.settings.Logger.InfoContext(ctx, "renamed deprecated trace fields in saved views", slog.Int("total", len(rows)), slog.Int("renamed", renamed), slog.Int("skipped", skipped))
return tx.Commit()
}
func (migration *renameSavedViewDeprecatedTraceFields) Down(context.Context, *bun.DB) error {
return nil
}
// renameDeprecatedTraceFields renames deprecated entries in spec.selectedFields and drops
// their fieldContext, which legacy entries carry as "attribute", so the query builder
// resolves it. Other keys of the entry are kept as is. A renamed entry whose new name is
// already selected is dropped. Returns changed=false if there was nothing to rename.
func renameDeprecatedTraceFields(data string) (string, bool, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(data), &raw); err != nil {
return "", false, err
}
var spec map[string]json.RawMessage
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
return "", false, err
}
var fields []map[string]json.RawMessage
if selectedFields, ok := spec["selectedFields"]; ok {
if err := json.Unmarshal(selectedFields, &fields); err != nil {
return "", false, err
}
}
names := make([]string, len(fields))
selected := make(map[string]struct{}, len(fields))
for i, field := range fields {
_ = json.Unmarshal(field["name"], &names[i])
selected[names[i]] = struct{}{}
}
changed := false
renamedFields := make([]map[string]json.RawMessage, 0, len(fields))
for i, field := range fields {
newName, ok := deprecatedTraceFields[names[i]]
if !ok {
renamedFields = append(renamedFields, field)
continue
}
changed = true
if _, ok := selected[newName]; ok {
continue
}
selected[newName] = struct{}{}
field["name"], _ = json.Marshal(newName)
delete(field, "fieldContext")
renamedFields = append(renamedFields, field)
}
if !changed {
return "", false, nil
}
var err error
if spec["selectedFields"], err = marshalUnescaped(renamedFields); err != nil {
return "", false, err
}
if raw["spec"], err = marshalUnescaped(spec); err != nil {
return "", false, err
}
renamedData, err := marshalUnescaped(raw)
if err != nil {
return "", false, err
}
return string(renamedData), true, nil
}

View File

@@ -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, scope
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')

View File

@@ -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, scope
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')

View File

@@ -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, attributes, scope 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, attributes, scope 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, attributes, scope 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, attributes, scope 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},
},
},

View File

@@ -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, attributes, scope 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, attributes, scope 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, attributes, scope 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, attributes, scope 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, attributes, scope 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, attributes, scope 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, attributes, scope 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,

View File

@@ -46,7 +46,6 @@ const (
SpanAttributesColumn = "attributes"
SpanAttributesPromotedColumn = "attributes_promoted"
SpanResourcesStringColumn = "resources_string"
SpanScopeColumn = "scope"
)
var (
@@ -456,7 +455,6 @@ var (
SpanAttributesBoolColumn,
SpanResourcesStringColumn,
SpanAttributesColumn,
SpanScopeColumn,
}
DefaultFields = map[string]telemetrytypes.TelemetryFieldKey{

View File

@@ -1,46 +0,0 @@
package aiobservabilitytypes
import (
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// statsLookback reaches into the previous day; the metadata store rounds the
// start down to a day boundary.
const statsLookback = 24 * time.Hour
// statsAttributeKeys are the gen_ai span attributes whose recent presence the
// stats reporter tracks under telemetry.traces.<key>.exists.
var statsAttributeKeys = []string{GenAIRequestModel, GenAIToolName, GenAIAgentName}
// StatsFieldValueSelectors look up whether each tracked attribute had a string
// value since the start of the previous day. One value is enough.
func StatsFieldValueSelectors(now time.Time) []*telemetrytypes.FieldValueSelector {
selectors := make([]*telemetrytypes.FieldValueSelector, 0, len(statsAttributeKeys))
for _, key := range statsAttributeKeys {
selectors = append(selectors, &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
StartUnixMilli: now.Add(-statsLookback).UnixMilli(),
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
Name: key,
},
Limit: 1,
})
}
return selectors
}
// NewStatsFromFieldValues reports whether each looked-up key had a value. Keys
// absent from values are left out, so a failed lookup never reports false.
func NewStatsFromFieldValues(values map[string]*telemetrytypes.TelemetryFieldValues) map[string]any {
stats := make(map[string]any, len(values))
for _, key := range statsAttributeKeys {
if fieldValues := values[key]; fieldValues != nil {
stats["telemetry.traces."+key+".exists"] = len(fieldValues.StringValues) > 0
}
}
return stats
}

View File

@@ -1,53 +0,0 @@
package aiobservabilitytypes
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
func TestNewStatsFromFieldValues(t *testing.T) {
testCases := []struct {
name string
values map[string]*telemetrytypes.TelemetryFieldValues
want map[string]any
}{
{
name: "NoLookups_NothingReported",
want: map[string]any{},
},
{
name: "EveryKeySeen_AllTrue",
values: map[string]*telemetrytypes.TelemetryFieldValues{
GenAIRequestModel: {StringValues: []string{"gpt-5-nano"}},
GenAIToolName: {StringValues: []string{"search"}},
GenAIAgentName: {StringValues: []string{"planner"}},
},
want: map[string]any{
"telemetry.traces.gen_ai.request.model.exists": true,
"telemetry.traces.gen_ai.tool.name.exists": true,
"telemetry.traces.gen_ai.agent.name.exists": true,
},
},
{
name: "EmptyAndMissingLookups_OnlyLookedUpKeysReported",
values: map[string]*telemetrytypes.TelemetryFieldValues{
GenAIRequestModel: {StringValues: []string{"gpt-5-nano"}},
GenAIToolName: {},
GenAIOperationName: {StringValues: []string{"chat"}},
},
want: map[string]any{
"telemetry.traces.gen_ai.request.model.exists": true,
"telemetry.traces.gen_ai.tool.name.exists": false,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, NewStatsFromFieldValues(testCase.values))
})
}
}

View File

@@ -1,7 +1,6 @@
package dashboardtypes
import (
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -11,7 +10,6 @@ const (
statKeyPanelTracesCount = "dashboard.panels.traces.count"
statKeyPanelMetricsCount = "dashboard.panels.metrics.count"
statKeyPanelLogsCount = "dashboard.panels.logs.count"
statKeyPanelAICount = "dashboard.panels.ai.count"
)
// panelSignalStatKeys maps a builder query's signal to the stat it contributes
@@ -24,17 +22,14 @@ var panelSignalStatKeys = map[telemetrytypes.Signal]string{
}
// NewStatsFromStorableDashboards reports the stats of stored dashboards. Rows that
// do not decode as v2 contribute to dashboard.count only. System dashboards are
// provisioned into every org, so they are left out of every count.
// do not decode as v2 contribute to dashboard.count only.
func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]any {
stats := newStats()
stats := newPanelStats()
for _, dashboard := range dashboards {
if dashboard == nil || dashboard.Source == SourceSystem {
if dashboard == nil {
continue
}
incrementStat(stats, statKeyDashboardCount)
dashboardV2, err := dashboard.ToDashboardV2(nil)
if err != nil {
continue
@@ -42,6 +37,7 @@ func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]
addPanelStats(&dashboardV2.Spec, stats)
}
stats[statKeyDashboardCount] = int64(len(dashboards))
return stats
}
@@ -49,26 +45,24 @@ func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]
// created, straight off the postable spec — the create path has no reason to make
// a storable round-trip just to be counted.
func NewStatsFromPostableDashboardV2(postable PostableDashboardV2) map[string]any {
stats := newStats()
stats[statKeyDashboardCount] = int64(1)
stats := newPanelStats()
addPanelStats(&postable.Spec, stats)
stats[statKeyDashboardCount] = int64(1)
return stats
}
func newStats() map[string]any {
func newPanelStats() map[string]any {
return map[string]any{
statKeyDashboardCount: int64(0),
statKeyPanelCount: int64(0),
statKeyPanelTracesCount: int64(0),
statKeyPanelMetricsCount: int64(0),
statKeyPanelLogsCount: int64(0),
statKeyPanelAICount: int64(0),
}
}
// addPanelStats counts the panels of a v2 spec, and each panel's queries against
// the signal they read. AI queries count only towards the AI stat.
// the signal they read.
func addPanelStats(spec *DashboardSpec, stats map[string]any) {
for _, panel := range spec.Panels {
if panel == nil {
@@ -82,10 +76,6 @@ func addPanelStats(spec *DashboardSpec, stats map[string]any) {
continue
}
for _, envelope := range composite.Queries {
if envelope.Type == qb.QueryTypeBuilderAI {
incrementStat(stats, statKeyPanelAICount)
continue
}
if key, ok := panelSignalStatKeys[envelope.GetSignal()]; ok {
incrementStat(stats, key)
}

View File

@@ -117,60 +117,20 @@ func TestNewStatsFromStorableDashboardsCountsCompositeSubQueries(t *testing.T) {
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
}
func statsAIBuilderQuery() string {
return `{
// An AI builder query is always a traces query, so it counts towards traces.
func TestNewStatsFromStorableDashboardsCountsAIBuilderQueries(t *testing.T) {
aiBuilder := `{
"kind": "time_series",
"spec": {"plugin": {"kind": "signoz/AIBuilderQuery", "spec": {"name": "A", "aggregations": [{"expression": "count()"}]}}}
}`
}
// An AI builder query reads traces but counts only towards the AI stat.
func TestNewStatsFromStorableDashboardsCountsAIBuilderQueries(t *testing.T) {
dashboard := newStatsStorableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
dashboard := newStatsStorableV2(t, `"p1": `+statsPanel(aiBuilder))
stats := NewStatsFromStorableDashboards([]*StorableDashboard{dashboard})
assert.Equal(t, int64(1), stats[statKeyPanelCount])
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(1), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
assert.Equal(t, int64(0), stats[statKeyPanelLogsCount])
assert.Equal(t, int64(1), stats[statKeyPanelAICount])
}
func TestNewStatsFromStorableDashboardsCountsAICompositeSubQueries(t *testing.T) {
composite := `{
"kind": "time_series",
"spec": {"plugin": {"kind": "signoz/CompositeQuery", "spec": {"queries": [
{"type": "builder_ai_query", "spec": {"name": "A", "aggregations": [{"expression": "count()"}]}},
{"type": "builder_query", "spec": ` + statsBuilderQuerySpec("B", "logs") + `}
]}}}
}`
dashboard := newStatsStorableV2(t, `
"p1": `+statsPanel(composite)+`,
"p2": `+statsPanel(statsAIBuilderQuery())+`
`)
stats := NewStatsFromStorableDashboards([]*StorableDashboard{dashboard})
assert.Equal(t, int64(2), stats[statKeyPanelCount])
assert.Equal(t, int64(2), stats[statKeyPanelAICount])
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
}
func TestNewStatsFromStorableDashboardsSkipsSystemDashboards(t *testing.T) {
system := newStatsStorableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
system.Source = SourceSystem
system.Name = SystemDashboardNamePrefix + "overview"
user := newStatsStorableV2(t, `"p1": `+statsPanel(statsBuilderQuery("logs")))
stats := NewStatsFromStorableDashboards([]*StorableDashboard{system, user})
assert.Equal(t, int64(1), stats[statKeyDashboardCount])
assert.Equal(t, int64(1), stats[statKeyPanelCount])
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
}
// promql and clickhouse queries carry no signal, so they land in the panel total
@@ -255,17 +215,6 @@ func TestNewStatsFromPostableDashboardV2(t *testing.T) {
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
assert.Equal(t, int64(1), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
}
func TestNewStatsFromPostableDashboardV2CountsAIBuilderQueries(t *testing.T) {
postable := newStatsPostableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
stats := NewStatsFromPostableDashboardV2(postable)
assert.Equal(t, int64(1), stats[statKeyDashboardCount])
assert.Equal(t, int64(1), stats[statKeyPanelCount])
assert.Equal(t, int64(1), stats[statKeyPanelAICount])
}
func TestNewStatsFromPostableDashboardV2WithNoPanels(t *testing.T) {
@@ -284,5 +233,4 @@ func TestNewStatsFromStorableDashboardsWithNoDashboards(t *testing.T) {
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
assert.Equal(t, int64(0), stats[statKeyPanelLogsCount])
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
}

View File

@@ -3,9 +3,9 @@ package llmpricingruletypes
import (
"database/sql/driver"
"encoding/json"
"path"
"time"
"github.com/SigNoz/signoz-otel-collector/pkg/llmpricing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/types"
@@ -220,18 +220,14 @@ func (u *UpdatableLLMPricingRule) UnmarshalJSON(data []byte) error {
return u.Validate()
}
// Validate mirrors the collector's pattern check: at least one pattern, none
// empty, all valid path.Match globs.
// Validate requires at least one pattern and holds each to the collector's rules.
func (u *UpdatableLLMPricingRule) Validate() error {
if len(u.ModelPattern) == 0 {
return errors.Newf(errors.TypeInvalidInput, ErrCodePricingRuleInvalidInput, "model %q: modelPattern must contain at least one pattern", u.Model)
}
for _, p := range u.ModelPattern {
if p == "" {
return errors.Newf(errors.TypeInvalidInput, ErrCodePricingRuleInvalidInput, "model %q: modelPattern must not contain an empty pattern", u.Model)
}
if _, err := path.Match(p, ""); err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodePricingRuleInvalidInput, "model %q: modelPattern %q is not a valid glob", u.Model, p)
if err := llmpricing.ValidatePattern(p); err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodePricingRuleInvalidInput, "model %q: %v", u.Model, err)
}
}
return nil
@@ -270,21 +266,10 @@ func NewLLMPricingRuleFromUpdatable(u *UpdatableLLMPricingRule, orgID valuer.UUI
func ModelMatchesAnyRule(model string, rules []*LLMPricingRule) bool {
for _, r := range rules {
for _, pattern := range r.ModelPattern {
if ok, err := path.Match(pattern, model); err == nil && ok {
if llmpricing.ParsePattern(pattern).Matches(model) {
return true
}
}
}
return false
}
// UnmappedModels keeps the models that no rule pattern matches.
func UnmappedModels(models []*UnmappedModel, rules []*LLMPricingRule) []*UnmappedModel {
unmapped := make([]*UnmappedModel, 0, len(models))
for _, model := range models {
if !ModelMatchesAnyRule(model.ModelName, rules) {
unmapped = append(unmapped, model)
}
}
return unmapped
}

View File

@@ -0,0 +1,32 @@
package llmpricingruletypes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestModelMatchesAnyRule(t *testing.T) {
opus5 := &LLMPricingRule{ModelPattern: StringSlice{"anthropic/claude-opus-5", "claude-opus-5"}}
opus55 := &LLMPricingRule{ModelPattern: StringSlice{"anthropic/claude-opus-5.5", "claude-opus-5.5", "claude-opus-5-5"}}
gpt5 := &LLMPricingRule{ModelPattern: StringSlice{"openai/gpt-5", "gpt-5"}}
testCases := []struct {
name string
rules []*LLMPricingRule
model string
want bool
}{
{name: "ProviderQualifiedModel", rules: []*LLMPricingRule{opus55}, model: "anthropic/claude-opus-5.5", want: true},
{name: "DatedSnapshot", rules: []*LLMPricingRule{opus55}, model: "claude-opus-5-5-20260921", want: true},
{name: "SiblingModel_NoMatch", rules: []*LLMPricingRule{gpt5}, model: "gpt-5-mini-2025-08-07", want: false},
{name: "SecondRuleMatches", rules: []*LLMPricingRule{opus5, opus55}, model: "claude-opus-5-5", want: true},
{name: "NoRules", rules: nil, model: "claude-opus-5-5", want: false},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, ModelMatchesAnyRule(testCase.model, testCase.rules))
})
}
}

View File

@@ -138,8 +138,7 @@ func TestUpdatableLLMPricingRuleUnmarshalJSON(t *testing.T) {
{name: "missing", pattern: ``, wantErr: true},
{name: "null", pattern: `null`, wantErr: true},
{name: "empty_list", pattern: `[]`, wantErr: true},
{name: "empty_entry", pattern: `["gpt-4o*", ""]`, wantErr: true},
{name: "bad_glob", pattern: `["gpt-["]`, wantErr: true},
{name: "mid_star", pattern: `["gpt-*-turbo"]`, wantErr: true},
}
for _, tc := range tests {

View File

@@ -1,24 +0,0 @@
package llmpricingruletypes
// NewStatsFromRules reports the rules users added themselves and the synced
// defaults users pinned.
func NewStatsFromRules(rules []*LLMPricingRule) map[string]any {
custom, overrides := int64(0), int64(0)
for _, rule := range rules {
switch {
case rule.SourceID == nil:
custom++
case rule.IsOverride:
overrides++
}
}
return map[string]any{
"llmpricingrule.custom.count": custom,
"llmpricingrule.override.count": overrides,
}
}
func NewStatsFromUnmappedModels(unmapped []*UnmappedModel) map[string]any {
return map[string]any{"llmpricingrule.unmapped_model.count": int64(len(unmapped))}
}

View File

@@ -1,53 +0,0 @@
package llmpricingruletypes
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/SigNoz/signoz/pkg/valuer"
)
func TestNewStatsFromRules(t *testing.T) {
sourceID := valuer.GenerateUUID()
testCases := []struct {
name string
rules []*LLMPricingRule
want map[string]any
}{
{
name: "NoRules",
want: map[string]any{
"llmpricingrule.custom.count": int64(0),
"llmpricingrule.override.count": int64(0),
},
},
{
name: "CustomPinnedAndSyncedRules_CountedSeparately",
rules: []*LLMPricingRule{
{IsOverride: true},
{IsOverride: false},
{SourceID: &sourceID, IsOverride: true},
{SourceID: &sourceID, IsOverride: false},
},
want: map[string]any{
"llmpricingrule.custom.count": int64(2),
"llmpricingrule.override.count": int64(1),
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, NewStatsFromRules(testCase.rules))
})
}
}
func TestNewStatsFromUnmappedModels(t *testing.T) {
unmapped := []*UnmappedModel{{ModelName: "gpt-5-nano"}, {ModelName: "claude-haiku-5-5"}}
assert.Equal(t, map[string]any{"llmpricingrule.unmapped_model.count": int64(2)}, NewStatsFromUnmappedModels(unmapped))
assert.Equal(t, map[string]any{"llmpricingrule.unmapped_model.count": int64(0)}, NewStatsFromUnmappedModels(nil))
}

View File

@@ -17,7 +17,6 @@ type FlamegraphSpan struct {
Events []Event `json:"event" required:"true" nullable:"false"`
Attributes map[string]any `json:"attributes" required:"true" nullable:"false"`
Resource map[string]string `json:"resource" required:"true" nullable:"false"`
Scope Scope `json:"scope" required:"true" nullable:"false"`
Children []*FlamegraphSpan `json:"-"` // internal tree use only
}
@@ -67,13 +66,11 @@ func NewFlamegraphSpanFromStorable(s *StorableSpan, level int64, selectFields []
Events: s.UnmarshalledEvents(),
Attributes: make(map[string]any),
Resource: make(map[string]string),
Scope: Scope{},
}
if len(selectFields) == 0 {
return span
}
var attributes map[string]any
var scopeFields []string
for _, field := range selectFields {
switch field.FieldContext {
case telemetrytypes.FieldContextResource:
@@ -87,13 +84,8 @@ func NewFlamegraphSpanFromStorable(s *StorableSpan, level int64, selectFields []
if v, ok := attributes[field.Name]; ok {
span.Attributes[field.Name] = v
}
case telemetrytypes.FieldContextScope:
scopeFields = append(scopeFields, field.Name)
}
}
if len(scopeFields) > 0 {
span.Scope = NewSelectedScope(s.Scope.Flatten(), scopeFields)
}
return span
}
@@ -104,7 +96,6 @@ func NewMissingParentFlamegraphSpan(node *FlamegraphSpan) *FlamegraphSpan {
Timestamp: node.Timestamp,
DurationNano: node.DurationNano,
Events: []Event{},
Scope: Scope{},
Children: []*FlamegraphSpan{node},
}
}

View File

@@ -1,11 +1,9 @@
package spantypes
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -17,7 +15,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
selectFields []telemetrytypes.TelemetryFieldKey
wantAttrs map[string]any
wantResource map[string]string
wantScope Scope
}{
{
name: "JSONColumn_ResolvesDottedPaths",
@@ -29,7 +26,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{"http.route": "/orders", "http.retry.count": float64(2)},
wantResource: map[string]string{},
wantScope: Scope{},
},
{
name: "LegacyMaps_Resolve",
@@ -41,7 +37,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{"http.route": "/cart", "cache.hit": true},
wantResource: map[string]string{},
wantScope: Scope{},
},
{
name: "ResourceFields_EmptyValueSkipped",
@@ -52,30 +47,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{},
wantResource: map[string]string{"service.name": "checkout"},
wantScope: Scope{},
},
{
name: "ScopeFields_DeclaredPathsAndAttributes",
span: StorableSpan{Scope: scopeColumn("io.opentelemetry.http", "1.2.0", map[string]any{"lib.lang": "go", "team": "payments"})},
selectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "scope.name", FieldContext: telemetrytypes.FieldContextScope},
{Name: "lib.lang", FieldContext: telemetrytypes.FieldContextScope},
{Name: "missing", FieldContext: telemetrytypes.FieldContextScope},
},
wantAttrs: map[string]any{},
wantResource: map[string]string{},
wantScope: Scope{"name": "io.opentelemetry.http", "attributes": map[string]any{"lib.lang": "go"}},
},
{
name: "ScopeFields_UnsetSelection_Empty",
span: StorableSpan{Scope: scopeColumn("io.opentelemetry.grpc", "", map[string]any{"rpc.system": "grpc"})},
selectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "scope.version", FieldContext: telemetrytypes.FieldContextScope},
{Name: "absent", FieldContext: telemetrytypes.FieldContextScope},
},
wantAttrs: map[string]any{},
wantResource: map[string]string{},
wantScope: Scope{},
},
}
for _, testCase := range testCases {
@@ -83,15 +54,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
span := NewFlamegraphSpanFromStorable(&testCase.span, 0, testCase.selectFields)
assert.Equal(t, testCase.wantAttrs, span.Attributes)
assert.Equal(t, testCase.wantResource, span.Resource)
assert.Equal(t, testCase.wantScope, span.Scope)
})
}
}
func TestNewMissingParentFlamegraphSpan_EmptyScope(t *testing.T) {
span := NewMissingParentFlamegraphSpan(&FlamegraphSpan{SpanID: "child", ParentSpanID: "parent"})
raw, err := json.Marshal(span)
require.NoError(t, err)
assert.Contains(t, string(raw), `"scope":{}`)
}

View File

@@ -1,72 +0,0 @@
package spantypes
import (
"strings"
"github.com/swaggest/jsonschema-go"
)
const (
scopeName = "name"
scopeVersion = "version"
scopeAttributes = "attributes"
)
var _ jsonschema.Preparer = Scope{}
// Scope is a span's instrumentation scope holding only the fields that are set: name, version and
// attributes, a flat bag of dotted keys like the span attributes.
type Scope map[string]any
// NewScope builds a Scope from the flattened scope column, where an unset declared path reads back
// as its zero value (name and version as empty strings).
func NewScope(column map[string]any) Scope {
scope := Scope{}
if name, _ := column[scopeName].(string); name != "" {
scope[scopeName] = name
}
if version, _ := column[scopeVersion].(string); version != "" {
scope[scopeVersion] = version
}
if attributes, _ := column[scopeAttributes].(map[string]any); len(attributes) > 0 {
scope[scopeAttributes] = attributes
}
return scope
}
// NewSelectedScope builds a Scope with only the selected fields from the flattened scope column:
// scope.name and scope.version select the declared paths, any other name a scope attribute.
func NewSelectedScope(column map[string]any, names []string) Scope {
attributes, _ := column[scopeAttributes].(map[string]any)
selected := map[string]any{}
selectedAttributes := map[string]any{}
for _, name := range names {
switch name {
case "scope.name":
selected[scopeName] = column[scopeName]
case "scope.version":
selected[scopeVersion] = column[scopeVersion]
default:
name = strings.TrimPrefix(name, "attribute.")
if v, ok := attributes[name]; ok {
selectedAttributes[name] = v
}
}
}
selected[scopeAttributes] = selectedAttributes
return NewScope(selected)
}
// PrepareJSONSchema types the map's fields. Scope is a map rather than a struct so unset fields are
// left out instead of sent empty; omitempty would do that but decodes back with nil fields.
func (Scope) PrepareJSONSchema(schema *jsonschema.Schema) error {
attributes := jsonschema.Schema{}
attributes.WithType(jsonschema.Object.Type())
attributes.WithAdditionalProperties(jsonschema.SchemaOrBool{TypeObject: &jsonschema.Schema{}})
schema.AdditionalProperties = nil
schema.WithPropertiesItem(scopeName, jsonschema.String.ToSchemaOrBool())
schema.WithPropertiesItem(scopeVersion, jsonschema.String.ToSchemaOrBool())
schema.WithPropertiesItem(scopeAttributes, attributes.ToSchemaOrBool())
return nil
}

View File

@@ -1,58 +0,0 @@
package spantypes
import (
"testing"
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
"github.com/stretchr/testify/assert"
"github.com/SigNoz/signoz/pkg/types/telemetrystoretypes"
)
// scopeColumn mirrors the driver's scan of the scope column: declared String paths as plain strings and the
// declared attributes path as a nested *chcol.JSON.
func scopeColumn(name, version string, attributes map[string]any) telemetrystoretypes.JSONValue {
j := chcol.NewJSON()
j.SetValueAtPath("name", name)
j.SetValueAtPath("version", version)
attrs := chcol.NewJSON()
for path, value := range attributes {
attrs.SetValueAtPath(path, value)
}
j.SetValueAtPath("attributes", attrs)
return telemetrystoretypes.NewJSONValue(*j)
}
func TestNewScope(t *testing.T) {
testCases := []struct {
name string
column telemetrystoretypes.JSONValue
want Scope
}{
{
name: "AttributesStayDotted",
column: scopeColumn("io.opentelemetry.http", "1.2.0", map[string]any{"lib.lang": "go", "lib.lang.version": "1.22", "retry": int64(3)}),
want: Scope{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go", "lib.lang.version": "1.22", "retry": float64(3)}},
},
{
name: "DropsNullAttributePaths_UnsetVersion",
column: scopeColumn("checkout", "", map[string]any{"team": "payments", "absent": chcol.Variant{}}),
want: Scope{"name": "checkout", "attributes": map[string]any{"team": "payments"}},
},
{
name: "UnsetDeclaredPaths_Empty",
column: scopeColumn("", "", nil),
want: Scope{},
},
{
name: "EmptyColumn",
column: telemetrystoretypes.JSONValue{},
want: Scope{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, NewScope(testCase.column.Flatten()))
})
}
}

View File

@@ -64,7 +64,6 @@ type WaterfallSpan struct {
Name string `json:"name"`
ParentSpanID string `json:"parent_span_id"`
Resource map[string]string `json:"resource"`
Scope Scope `json:"scope"`
SpanID string `json:"span_id"`
TimeUnix uint64 `json:"time_unix"`
TraceID string `json:"trace_id"`
@@ -114,7 +113,6 @@ type StorableSpan struct {
AttributesBool map[string]bool `ch:"attributes_bool"`
AttributesJSON telemetrystoretypes.JSONValue `ch:"attributes"`
ResourcesString map[string]string `ch:"resources_string"`
Scope telemetrystoretypes.JSONValue `ch:"scope"`
Events []string `ch:"events"`
StatusMessage string `ch:"status_message"`
StatusCodeString string `ch:"status_code_string"`
@@ -155,7 +153,6 @@ func (item *MinimalSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
HasError: item.HasError,
ServiceName: item.ServiceName,
Resource: map[string]string{"service.name": item.ServiceName},
Scope: Scope{},
Children: make([]*WaterfallSpan, 0),
Attributes: make(map[string]any),
Events: make([]Event, 0),
@@ -185,7 +182,6 @@ func NewMissingWaterfallSpan(spanID, traceID string, timeUnixNano, durationNano
Children: make([]*WaterfallSpan, 0),
Attributes: make(map[string]any),
Resource: make(map[string]string),
Scope: Scope{},
}
}
@@ -340,7 +336,6 @@ func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
Name: item.Name,
ParentSpanID: item.ParentSpanID,
Resource: resources,
Scope: NewScope(item.Scope.Flatten()),
ResponseStatusCode: item.ResponseStatusCode,
SpanID: item.SpanID,
StatusCode: item.StatusCode,

View File

@@ -39,7 +39,7 @@ func (v JSONValue) Flatten() map[string]any {
if isNull(value) {
continue
}
flat[path] = flatObject(value)
flat[path] = value
}
return unwrap(flat)
}
@@ -57,15 +57,6 @@ func (v JSONValue) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Flatten())
}
// flatObject renders a declared JSON sub-path, which the driver scans as a nested chcol.JSON (such as
// scope.attributes), as a flat attribute bag.
func flatObject(value any) any {
if object, ok := value.(*chcol.JSON); ok && object != nil {
return JSONValue{column: *object}
}
return value
}
func isNull(value any) bool {
if value == nil {
return true

View File

@@ -50,7 +50,6 @@ ALL_SELECT_FIELDS = [
# all contextual columns (merged in response layer)
"attributes",
"resource",
"scope",
]

View File

@@ -545,7 +545,7 @@ def test_traces_list_response_shape_values(
now = datetime.now(tz=UTC).replace(microsecond=0)
trace_id = TraceIdGenerator.trace_id()
def span(marker: str, *, kind: TracesKind, status_code: TracesStatusCode = TracesStatusCode.STATUS_CODE_UNSET, status_message: str = "", flags: int = 0, attributes: dict[str, Any], scope: dict[str, Any] | None = None) -> Traces:
def span(marker: str, *, kind: TracesKind, status_code: TracesStatusCode = TracesStatusCode.STATUS_CODE_UNSET, status_message: str = "", flags: int = 0, attributes: dict[str, Any]) -> Traces:
return Traces(
timestamp=now - timedelta(seconds=1),
duration=timedelta(seconds=1),
@@ -558,11 +558,10 @@ def test_traces_list_response_shape_values(
flags=flags,
resources={"service.name": "shape-service", **extra_resources},
attributes={"marker": marker, **attributes, **extra_attrs},
scope=scope or {},
)
spans = [
span("error", kind=TracesKind.SPAN_KIND_SERVER, status_code=TracesStatusCode.STATUS_CODE_ERROR, status_message="boom", attributes={"str_a": "x", "num_a": 7.5, "bool_a": True}, scope={"name": "io.signoz.shape", "version": "0.1.0", "attributes": {"lib.lang": "go"}}),
span("error", kind=TracesKind.SPAN_KIND_SERVER, status_code=TracesStatusCode.STATUS_CODE_ERROR, status_message="boom", attributes={"str_a": "x", "num_a": 7.5, "bool_a": True}),
span("client", kind=TracesKind.SPAN_KIND_CLIENT, attributes={"http.url": "https://api.example.com/v1/orders", "http.request.method": "GET"}),
span("db", kind=TracesKind.SPAN_KIND_CLIENT, attributes={"db.name": "orders_db", "db.operation": "SELECT"}),
span("host", kind=TracesKind.SPAN_KIND_PRODUCER, attributes={"server.address": "payments.internal:8080"}),
@@ -623,7 +622,6 @@ def test_traces_list_response_shape_values(
"is_remote": span_obj.is_remote,
"attributes": {**span_obj.attribute_string, **span_obj.attributes_number, **span_obj.attributes_bool},
"resource": span_obj.resources_string,
"scope": {k: v for k, v in span_obj.scope_json.items() if v},
}
# `==` treats True/1 and 7.5/7.5 as equal regardless of type, so pin the
# string/number/bool type preservation for the error span explicitly.
@@ -1156,7 +1154,6 @@ def test_traces_list_span_scope(
"is_remote": x[3].is_remote,
"attributes": {**x[3].attribute_string, **x[3].attributes_number, **x[3].attributes_bool},
"resource": x[3].resources_string,
"scope": {k: v for k, v in x[3].scope_json.items() if v},
},
id="order_timestamp_full_shape",
),
@@ -1194,7 +1191,6 @@ def test_traces_list_span_scope(
"is_remote": x[3].is_remote,
"attributes": {**x[3].attribute_string, **x[3].attributes_number, **x[3].attributes_bool},
"resource": x[3].resources_string,
"scope": {k: v for k, v in x[3].scope_json.items() if v},
},
id="order_attribute_timestamp_full_shape",
),

View File

@@ -21,13 +21,12 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
trace_id = TraceIdGenerator.trace_id()
legacy_id, json_id, dual_id = (TraceIdGenerator.span_id() for _ in range(3))
resources = {"service.name": "tracedetail-attribute-homes"}
scope = {"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": {"lib.lang": "go"}}
insert_traces(
[
Traces(timestamp=now - timedelta(seconds=10), duration=timedelta(seconds=9), trace_id=trace_id, span_id=legacy_id, name="GET /legacy", kind=TracesKind.SPAN_KIND_SERVER, resources=resources, attributes={"http.route": "/legacy", "http.retry.count": 1, "cache.hit": True}, attribute_write_mode="legacy_only"),
Traces(timestamp=now - timedelta(seconds=8), trace_id=trace_id, span_id=json_id, parent_span_id=legacy_id, name="db call", resources=resources, attributes={"http.route": "/json", "db.function": "node_refresh", "db.function.arg_count": 2}, attribute_write_mode="json_only"),
Traces(timestamp=now - timedelta(seconds=6), trace_id=trace_id, span_id=dual_id, parent_span_id=legacy_id, name="GET /dual", resources=resources, attributes={"http.route": "/dual"}, scope=scope),
Traces(timestamp=now - timedelta(seconds=6), trace_id=trace_id, span_id=dual_id, parent_span_id=legacy_id, name="GET /dual", resources=resources, attributes={"http.route": "/dual"}),
]
)
@@ -41,14 +40,11 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
timeout=10,
)
assert response.status_code == HTTPStatus.OK, response.text
spans = {span["span_id"]: span for span in response.json()["data"]["spans"]}
waterfall = {span_id: span["attributes"] for span_id, span in spans.items()}
waterfall = {span["span_id"]: span["attributes"] for span in response.json()["data"]["spans"]}
assert waterfall[legacy_id] == {"http.route": "/legacy", "http.retry.count": 1, "cache.hit": True}
# a key stored at both a scalar and an object path stays two flat keys
assert waterfall[json_id] == {"http.route": "/json", "db.function": "node_refresh", "db.function.arg_count": 2}
assert waterfall[dual_id] == {"http.route": "/dual"}
assert spans[dual_id]["scope"] == scope
assert spans[legacy_id]["scope"] == {}
response = requests.post(
signoz.self.host_configs["8080"].get(f"/api/v3/traces/{trace_id}/flamegraph"),
@@ -58,8 +54,6 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
{"name": "http.route", "fieldContext": "attribute"},
{"name": "db.function.arg_count", "fieldContext": "attribute"},
{"name": "service.name", "fieldContext": "resource"},
{"name": "scope.name", "fieldContext": "scope"},
{"name": "lib.lang", "fieldContext": "scope"},
],
},
headers=headers,
@@ -71,5 +65,3 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
assert flamegraph[json_id]["attributes"] == {"http.route": "/json", "db.function.arg_count": 2}
assert flamegraph[dual_id]["attributes"] == {"http.route": "/dual"}
assert all(span["resource"] == resources for span in flamegraph.values())
assert flamegraph[dual_id]["scope"] == {"name": "io.opentelemetry.http", "attributes": {"lib.lang": "go"}}
assert flamegraph[json_id]["scope"] == {}