Compare commits

..

7 Commits

Author SHA1 Message Date
Nikhil Mantri
702831e26b Merge branch 'main' into feat/warnings_for_empty_data 2026-01-14 13:57:15 +05:30
nikhilmantri0902
5c6346df11 chore: remove array sort 2026-01-13 17:20:02 +05:30
nikhilmantri0902
860c3e70ac chore: added diagnostic columns logic 2026-01-13 14:10:21 +05:30
nikhilmantri0902
3d8bfd47fa fix: add alias to cached data and also set back alias from cache when present 2026-01-12 17:49:31 +05:30
nikhilmantri0902
f01b00bd5c chore: return warnings seperately 2026-01-10 23:15:51 +05:30
nikhilmantri0902
120317321c chore: return warnings seperately 2026-01-10 22:36:38 +05:30
nikhilmantri0902
895e92893c chore: query and postprocessmetric query modification 2026-01-10 18:56:14 +05:30
34 changed files with 302 additions and 305 deletions

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.107.0
image: signoz/signoz:v0.106.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.107.0
image: signoz/signoz:v0.106.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.107.0}
image: signoz/signoz:${VERSION:-v0.106.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.107.0}
image: signoz/signoz:${VERSION:-v0.106.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -1,16 +1,16 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
handleChangeSelectedView?: ChangeViewFunctionType;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -55,11 +55,11 @@ function LogDetailInner({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -365,10 +365,10 @@ function LogDetailInner({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -6,7 +6,6 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -109,7 +108,6 @@ type ListLogViewProps = {
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
};
function ListLogView({
@@ -120,7 +118,6 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -134,6 +131,7 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -257,7 +255,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
handleChangeSelectedView={handleChangeSelectedView}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</>
@@ -266,7 +264,6 @@ function ListLogView({
ListLogView.defaultProps = {
activeLog: null,
handleChangeSelectedView: undefined,
};
LogGeneralField.defaultProps = {

View File

@@ -39,7 +39,6 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -53,6 +52,7 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
@@ -224,12 +224,13 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</RawLogViewContainer>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,

View File

@@ -1,4 +1,3 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { MouseEvent } from 'react';
import { IField } from 'types/api/logs/fields';
@@ -15,7 +14,6 @@ export interface RawLogViewProps {
fontSize: FontSize;
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export interface RawLogContentProps {

View File

@@ -4,7 +4,6 @@ import { Switch, Typography } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
@@ -22,13 +21,7 @@ import { ILiveLogsLog } from '../LiveLogsList/types';
import LiveLogsListChart from '../LiveLogsListChart';
import { QueryHistoryState } from '../types';
interface LiveLogsContainerProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogsContainer({
handleChangeSelectedView,
}: LiveLogsContainerProps): JSX.Element {
function LiveLogsContainer(): JSX.Element {
const location = useLocation();
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
const { currentQuery, stagedQuery } = useQueryBuilder();
@@ -254,7 +247,6 @@ function LiveLogsContainer({
<LiveLogsList
logs={logs}
isLoading={initialLoading && logs.length === 0}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
@@ -264,8 +256,4 @@ function LiveLogsContainer({
);
}
LiveLogsContainer.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogsContainer;

View File

@@ -25,11 +25,7 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { LiveLogsListProps } from './types';
function LiveLogsList({
logs,
isLoading,
handleChangeSelectedView,
}: LiveLogsListProps): JSX.Element {
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { isConnectionLoading } = useEventSource();
@@ -40,6 +36,7 @@ function LiveLogsList({
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -75,7 +72,6 @@ function LiveLogsList({
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -89,12 +85,10 @@ function LiveLogsList({
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -153,7 +147,6 @@ function LiveLogsList({
appendTo: 'end',
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -177,11 +170,12 @@ function LiveLogsList({
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}
export default memo(LiveLogsList);

View File

@@ -1,4 +1,3 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {
@@ -9,5 +8,4 @@ export interface ILiveLogsLog {
export type LiveLogsListProps = {
logs: ILiveLogsLog[];
isLoading: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -12,13 +12,13 @@ import {
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
@@ -29,7 +29,7 @@ interface OverviewProps {
isListViewPanel?: boolean;
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
handleChangeSelectedView?: ChangeViewFunctionType;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
}
type Props = OverviewProps &
@@ -42,8 +42,8 @@ function Overview({
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -208,11 +208,11 @@ function Overview({
logData={logData}
onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
),
@@ -227,7 +227,7 @@ function Overview({
Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
handleChangeSelectedView: undefined,
onGroupByAttribute: undefined,
};
export default Overview;

View File

@@ -13,7 +13,6 @@ import AddToQueryHOC, {
import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
@@ -48,7 +47,7 @@ interface TableViewProps {
selectedOptions: OptionsQuery;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
handleChangeSelectedView?: ChangeViewFunctionType;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
}
type Props = TableViewProps &
@@ -62,8 +61,8 @@ function TableView({
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
@@ -296,7 +295,7 @@ function TableView({
isfilterInLoading={isfilterInLoading}
isfilterOutLoading={isfilterOutLoading}
onClickHandler={onClickHandler}
handleChangeSelectedView={handleChangeSelectedView}
onGroupByAttribute={onGroupByAttribute}
/>
),
},
@@ -339,7 +338,7 @@ function TableView({
TableView.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
handleChangeSelectedView: undefined,
onGroupByAttribute: undefined,
};
export interface DataType {

View File

@@ -7,24 +7,15 @@ import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import React, { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataType } from '../TableView';
import {
@@ -42,6 +33,7 @@ interface ITableViewActionsProps {
isListViewPanel: boolean;
isfilterInLoading: boolean;
isfilterOutLoading: boolean;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onClickHandler: (
operator: string,
fieldKey: string,
@@ -49,7 +41,6 @@ interface ITableViewActionsProps {
dataType: string | undefined,
logType: MetricsType | undefined,
) => () => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
// Memoized Tree Component
@@ -127,12 +118,10 @@ export default function TableViewActions(
isfilterInLoading,
isfilterOutLoading,
onClickHandler,
handleChangeSelectedView,
onGroupByAttribute,
} = props;
const { pathname } = useLocation();
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page
@@ -156,42 +145,6 @@ export default function TableViewActions(
const fieldFilterKey = filterKeyForField(fieldData.field);
const handleGroupByAttribute = useCallback((): void => {
if (!stagedQuery) return;
const normalizedDataType: DataTypes | undefined =
dataType && Object.values(DataTypes).includes(dataType as DataTypes)
? (dataType as DataTypes)
: undefined;
const updatedQuery = updateQueriesData(stagedQuery, 'queryData', (item) => {
const newGroupByItem: BaseAutocompleteData = {
key: fieldFilterKey,
type: fieldType || '',
dataType: normalizedDataType,
};
const updatedGroupBy = [...(item.groupBy || []), newGroupByItem];
return { ...item, groupBy: updatedGroupBy };
});
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
}, [
stagedQuery,
updateQueriesData,
fieldFilterKey,
fieldType,
dataType,
handleChangeSelectedView,
viewName,
]);
// Memoize textToCopy computation
const textToCopy = useMemo(() => {
let text = fieldData.value;
@@ -315,7 +268,9 @@ export default function TableViewActions(
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={handleGroupByAttribute}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
>
Group By Attribute
</Button>
@@ -393,7 +348,9 @@ export default function TableViewActions(
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={handleGroupByAttribute}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
>
Group By Attribute
</Button>
@@ -416,5 +373,5 @@ export default function TableViewActions(
}
TableViewActions.defaultProps = {
handleChangeSelectedView: undefined,
onGroupByAttribute: undefined,
};

View File

@@ -1,8 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import TableViewActions from '../TableViewActions';
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';
@@ -52,20 +49,6 @@ jest.mock('../useAsyncJSONProcessing', () => ({
default: jest.fn(),
}));
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
return {
...antd,
// Render popover content inline to make its children testable
Popover: ({ content, children }: any): JSX.Element => (
<div data-testid="popover">
<div data-testid="popover-content">{content}</div>
{children}
</div>
),
};
});
jest.mock('providers/Timezone', () => ({
useTimezone: (): {
formatTimezoneAdjustedTimestamp: (timestamp: string) => string;
@@ -88,35 +71,29 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('hooks/queryBuilder/useGetSearchQueryParam');
describe('TableViewActions', () => {
const TEST_VALUE = 'test value';
const TEST_FIELD = 'test-field';
const ACTION_BUTTON_TEST_ID = '.action-btn';
const defaultProps = {
fieldData: {
field: TEST_FIELD,
field: 'test-field',
value: TEST_VALUE,
},
record: {
key: 'test-key',
field: TEST_FIELD,
field: 'test-field',
value: TEST_VALUE,
},
isListViewPanel: false,
isfilterInLoading: false,
isfilterOutLoading: false,
onClickHandler: jest.fn(),
handleChangeSelectedView: jest.fn(),
onGroupByAttribute: jest.fn(),
};
beforeEach(() => {
mockCopyToClipboard = jest.fn();
mockNotificationsSuccess = jest.fn();
defaultProps.onClickHandler = jest.fn();
defaultProps.handleChangeSelectedView = jest.fn();
// Default mock for useAsyncJSONProcessing
const mockUseAsyncJSONProcessing = jest.mocked(useAsyncJSONProcessing);
@@ -125,24 +102,6 @@ describe('TableViewActions', () => {
treeData: null,
error: null,
});
// Default mock for useQueryBuilder
jest.mocked(useQueryBuilder).mockReturnValue({
stagedQuery: null,
updateQueriesData: jest.fn((query, type, callback) => {
const updatedBuilder = {
...query.builder,
[type]: query.builder[type].map(callback),
};
return {
...query,
builder: updatedBuilder,
};
}),
} as any);
// Default mock for useGetSearchQueryParam
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
});
it('should render without crashing', () => {
@@ -154,7 +113,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
expect(screen.getByText(TEST_VALUE)).toBeInTheDocument();
@@ -176,7 +135,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are not rendered for restricted fields
@@ -195,100 +154,13 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are rendered for non-restricted fields
expect(container.querySelector(ACTION_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should call handleChangeSelectedView when clicking group by', () => {
const mockStagedQuery = {
id: 'test-query-id',
queryType: 'queryBuilder',
builder: {
queryData: [
{
queryName: 'A',
dataSource: 'logs',
aggregateOperator: 'count',
functions: [],
filter: {},
groupBy: [],
expression: '',
disabled: false,
having: [],
limit: null,
stepInterval: null,
orderBy: [],
legend: '',
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const mockUpdateQueriesData = jest.fn((query, type, callback) => {
const section = query.builder?.[type];
if (!Array.isArray(section)) {
return query;
}
return {
...query,
builder: {
...query.builder,
[type]: section.map(callback),
},
};
});
jest.mocked(useQueryBuilder).mockReturnValue({
stagedQuery: mockStagedQuery,
updateQueriesData: mockUpdateQueriesData,
} as any);
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
fireEvent.click(screen.getByText('Group By Attribute'));
expect(defaultProps.handleChangeSelectedView).toHaveBeenCalledWith(
ExplorerViews.TIMESERIES,
expect.objectContaining({
name: '',
id: 'test-query-id',
query: expect.objectContaining({
builder: expect.objectContaining({
queryData: expect.arrayContaining([
expect.objectContaining({
groupBy: expect.arrayContaining([
expect.objectContaining({
key: TEST_FIELD,
type: '',
}),
]),
}),
]),
}),
}),
}),
);
});
it('should not render action buttons in list view panel', () => {
const { container } = render(
<TableViewActions
@@ -298,7 +170,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
onGroupByAttribute={defaultProps.onGroupByAttribute}
/>,
);
// Verify that action buttons are not rendered in list view panel
@@ -328,7 +200,7 @@ describe('TableViewActions', () => {
isfilterInLoading: false,
isfilterOutLoading: false,
onClickHandler: jest.fn(),
handleChangeSelectedView: jest.fn(),
onGroupByAttribute: jest.fn(),
};
// Render component with body field
@@ -340,7 +212,7 @@ describe('TableViewActions', () => {
isfilterInLoading={bodyProps.isfilterInLoading}
isfilterOutLoading={bodyProps.isfilterOutLoading}
onClickHandler={bodyProps.onClickHandler}
handleChangeSelectedView={bodyProps.handleChangeSelectedView}
onGroupByAttribute={bodyProps.onGroupByAttribute}
/>,
);

View File

@@ -47,6 +47,7 @@ function ColumnView({
onSetActiveLog: handleSetActiveLog,
onClearActiveLog: handleClearActiveLog,
onAddToQuery: handleAddToQuery,
onGroupByAttribute: handleGroupByAttribute,
} = useActiveLog();
const [showActiveLog, setShowActiveLog] = useState<boolean>(false);
@@ -270,6 +271,7 @@ function ColumnView({
onClose={handleLogDetailClose}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
onGroupByAttribute={handleGroupByAttribute}
/>
)}
</div>

View File

@@ -58,7 +58,7 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
{ isLoading, tableViewProps, infitiyTableProps },
ref,
): JSX.Element | null {
const {
@@ -72,6 +72,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const { dataSource, columns } = useTableView({
@@ -186,7 +187,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
handleChangeSelectedView={handleChangeSelectedView}
onGroupByAttribute={onGroupByAttribute}
/>
)}
<LogDetail
@@ -195,7 +196,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
onGroupByAttribute={onGroupByAttribute}
/>
</>
);

View File

@@ -1,5 +1,4 @@
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -7,5 +6,4 @@ export type InfinityTableProps = {
infitiyTableProps?: {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -1,4 +1,3 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -13,5 +12,4 @@ export type LogsExplorerListProps = {
error?: Error | APIError;
isFilterApplied: boolean;
isFrequencyChartVisible: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -48,7 +48,6 @@ function LogsExplorerList({
isError,
error,
isFilterApplied,
handleChangeSelectedView,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { activeLogId } = useCopyLogLink();
@@ -57,6 +56,7 @@ function LogsExplorerList({
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -100,7 +100,6 @@ function LogsExplorerList({
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -115,13 +114,11 @@ function LogsExplorerList({
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
activeLog,
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -152,10 +149,10 @@ function LogsExplorerList({
activeLogIndex,
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
function getMarginTop(): string {
switch (options.fontSize) {
case FontSize.SMALL:
@@ -198,7 +195,6 @@ function LogsExplorerList({
onEndReached,
getItemContent,
selectedFields,
handleChangeSelectedView,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -277,8 +273,8 @@ function LogsExplorerList({
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
)}

View File

@@ -447,9 +447,8 @@ function LogsExplorerViewsContainer({
)}
<div className="logs-explorer-views-type-content">
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}
{showLiveLogs && <LiveLogs />}
{selectedPanelType === PANEL_TYPES.LIST && !showLiveLogs && (
<LogsExplorerList
isLoading={isLoading}
@@ -461,9 +460,9 @@ function LogsExplorerViewsContainer({
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
@@ -484,6 +483,7 @@ function LogsExplorerViewsContainer({
/>
</div>
)}
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
<LogsExplorerTable
data={

View File

@@ -89,6 +89,7 @@ function LogsPanelComponent({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const handleRow = useCallback(
@@ -170,6 +171,7 @@ function LogsPanelComponent({
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>

View File

@@ -38,6 +38,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -156,6 +157,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onGroupByAttribute={onGroupByAttribute}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>

View File

@@ -14,6 +14,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
@@ -48,6 +49,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/>
</div>
);

View File

@@ -1,5 +1,4 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -7,11 +6,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useEffect } from 'react';
import { DataSource } from 'types/common/queryBuilder';
interface LiveLogsProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogs({ handleChangeSelectedView }: LiveLogsProps): JSX.Element {
function LiveLogs(): JSX.Element {
useShareBuilderUrl({ defaultValue: liveLogsCompositeQuery });
const { handleSetConfig } = useQueryBuilder();
@@ -19,13 +14,7 @@ function LiveLogs({ handleChangeSelectedView }: LiveLogsProps): JSX.Element {
handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS);
}, [handleSetConfig]);
return (
<LiveLogsContainer handleChangeSelectedView={handleChangeSelectedView} />
);
return <LiveLogsContainer />;
}
LiveLogs.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogs;

View File

@@ -79,19 +79,21 @@ function LogsExplorer(): JSX.Element {
const handleChangeSelectedView = useCallback(
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
const nextPanelType = defaultTo(
explorerViewToPanelType[view],
PANEL_TYPES.LIST,
handleSetConfig(
defaultTo(explorerViewToPanelType[view], PANEL_TYPES.LIST),
DataSource.LOGS,
);
handleSetConfig(nextPanelType, DataSource.LOGS);
setSelectedView(view);
if (view !== ExplorerViews.LIST) {
setShowLiveLogs(false);
}
handleExplorerTabChange(nextPanelType, querySearchParameters);
handleExplorerTabChange(
explorerViewToPanelType[view],
querySearchParameters,
);
},
[handleSetConfig, handleExplorerTabChange, setSelectedView],
);

View File

@@ -490,6 +490,7 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
key string
}
seriesMap := make(map[seriesKey]*qbtypes.TimeSeries, estimatedSeries)
aliasMap := make(map[int]string)
for _, bucket := range buckets {
var tsData *qbtypes.TimeSeriesData
@@ -499,6 +500,10 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
}
for _, aggBucket := range tsData.Aggregations {
if aggBucket.Alias != "" {
aliasMap[aggBucket.Index] = aggBucket.Alias
}
for _, series := range aggBucket.Series {
// Create series key from labels
key := seriesKey{
@@ -571,7 +576,13 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
}
}
var alias string
if aliasMap[index] != "" {
alias = aliasMap[index]
}
result.Aggregations = append(result.Aggregations, &qbtypes.AggregationBucket{
Alias: alias,
Index: index,
Series: seriesList,
})
@@ -736,6 +747,7 @@ func (bc *bucketCache) trimResultToFluxBoundary(result *qbtypes.Result, fluxBoun
for _, aggBucket := range tsData.Aggregations {
trimmedBucket := &qbtypes.AggregationBucket{
Index: aggBucket.Index,
Alias: aggBucket.Alias,
}
for _, series := range aggBucket.Series {

View File

@@ -200,7 +200,7 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return nil, err
}
result.Warnings = stmt.Warnings
result.Warnings = append(result.Warnings, stmt.Warnings...)
result.WarningsDocURL = stmt.WarningsDocURL
return result, nil
}

View File

@@ -12,17 +12,23 @@ import (
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/bytedance/sonic"
)
const (
diagnosticColumnIndexBase = 1000000
)
var (
aggRe = regexp.MustCompile(`^__result_(\d+)$`)
// legacyReservedColumnTargetAliases identifies result value from a user
// written clickhouse query. The column alias indcate which value is
// to be considered as final result (or target)
legacyReservedColumnTargetAliases = []string{"__result", "__value", "result", "res", "value"}
diagnosticColumnAliases = []string{telemetrymetrics.DiagnosticColumnCumulativeHistLeCount, telemetrymetrics.DiagnosticColumnCumulativeHistLeSum}
)
// consume reads every row and shapes it into the payload expected for the
@@ -69,6 +75,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
key string // deterministic join of label values
}
seriesMap := map[sKey]*qbtypes.TimeSeries{}
diagnosticSeriesMap := map[string]*qbtypes.TimeSeries{}
stepMs := uint64(step.Duration.Milliseconds())
@@ -113,12 +120,13 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
}
var (
ts int64
lblVals = make([]string, 0, lblValsCapacity)
lblObjs = make([]*qbtypes.Label, 0, lblValsCapacity)
aggValues = map[int]float64{} // all __result_N in this row
fallbackValue float64 // value when NO __result_N columns exist
fallbackSeen bool
ts int64
lblVals = make([]string, 0, lblValsCapacity)
lblObjs = make([]*qbtypes.Label, 0, lblValsCapacity)
aggValues = map[int]float64{} // all __result_N in this row
diagnosticValues = map[string]float64{} // all diagnostic columns in this row
fallbackValue float64 // value when NO __result_N columns exist
fallbackSeen bool
)
for idx, ptr := range slots {
@@ -130,7 +138,9 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
case *float64, *float32, *int64, *int32, *uint64, *uint32:
val := numericAsFloat(reflect.ValueOf(ptr).Elem().Interface())
if m := aggRe.FindStringSubmatch(name); m != nil {
if slices.Contains(diagnosticColumnAliases, name) {
diagnosticValues[name] = val
} else if m := aggRe.FindStringSubmatch(name); m != nil {
id, _ := strconv.Atoi(m[1])
aggValues[id] = val
} else if numericColsCount == 1 { // classic single-value query
@@ -152,7 +162,9 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
tempVal := reflect.ValueOf(ptr)
if tempVal.IsValid() && !tempVal.IsNil() && !tempVal.Elem().IsNil() {
val := numericAsFloat(tempVal.Elem().Elem().Interface())
if m := aggRe.FindStringSubmatch(name); m != nil {
if slices.Contains(diagnosticColumnAliases, name) {
diagnosticValues[name] = val
} else if m := aggRe.FindStringSubmatch(name); m != nil {
id, _ := strconv.Atoi(m[1])
aggValues[id] = val
} else if numericColsCount == 1 { // classic single-value query
@@ -195,6 +207,23 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
}
}
// fetch and store diagnostic values in diagnosticSeriesMap
for diagnosticColName, val := range diagnosticValues {
if math.IsNaN(val) || math.IsInf(val, 0) {
continue
}
diagSeries, ok := diagnosticSeriesMap[diagnosticColName]
if !ok {
diagSeries = &qbtypes.TimeSeries{}
diagnosticSeriesMap[diagnosticColName] = diagSeries
}
diagSeries.Values = append(diagSeries.Values, &qbtypes.TimeSeriesValue{
Timestamp: ts,
Value: val,
Partial: isPartialValue(ts),
})
}
// Edge-case: no __result_N columns, but a single numeric column present
if len(aggValues) == 0 && fallbackSeen {
aggValues[0] = fallbackValue
@@ -231,6 +260,20 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
return nil, err
}
diagnosticBuckets := make([]*qbtypes.AggregationBucket, 0)
// TODO(nikhilmantri0902, srikanthccv): below HACK - this is a temporary index introduced becausing caching grouping and merging happens on index
// Should we improve the caching grouping and merging to not depend on index?
diagNosticTemporayIndex := diagnosticColumnIndexBase
for diagColName, diagSeries := range diagnosticSeriesMap {
diagnosticBucket := &qbtypes.AggregationBucket{
Index: diagNosticTemporayIndex,
Alias: diagColName,
}
diagnosticBucket.Series = append(diagnosticBucket.Series, diagSeries)
diagnosticBuckets = append(diagnosticBuckets, diagnosticBucket)
diagNosticTemporayIndex++
}
maxAgg := -1
for k := range seriesMap {
if k.agg > maxAgg {
@@ -240,6 +283,8 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
if maxAgg < 0 {
return &qbtypes.TimeSeriesData{
QueryName: queryName,
// return with diagNostic buckets
Aggregations: diagnosticBuckets,
}, nil
}
@@ -261,6 +306,9 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
}
}
// add diagNostic buckets
nonEmpty = append(nonEmpty, diagnosticBuckets...)
return &qbtypes.TimeSeriesData{
QueryName: queryName,
Aggregations: nonEmpty,

View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -46,7 +47,7 @@ func getQueryName(spec any) string {
return getqueryInfo(spec).Name
}
func (q *querier) postProcessResults(ctx context.Context, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, error) {
func (q *querier) postProcessResults(ctx context.Context, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, []string, string, error) {
// Convert results to typed format for processing
typedResults := make(map[string]*qbtypes.Result)
for name, result := range results {
@@ -96,7 +97,7 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
for name, v := range typedResults {
retResult[name] = v.Value
}
return retResult, nil
return retResult, nil, "", nil
}
}
@@ -108,11 +109,11 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
firstQueryName := getQueryName(req.CompositeQuery.Queries[0].Spec)
if firstQueryName != "" && tableResult["table"] != nil {
// Return table under first query name
return map[string]any{firstQueryName: tableResult["table"]}, nil
return map[string]any{firstQueryName: tableResult["table"]}, nil, "", nil
}
}
return tableResult, nil
return tableResult, nil, "", nil
}
if req.RequestType == qbtypes.RequestTypeTimeSeries && req.FormatOptions != nil && req.FormatOptions.FillGaps {
@@ -155,7 +156,19 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
finalResults[name] = result.Value
}
return finalResults, nil
// collect postProcessWarnings from typed results
var postProcessWarnings []string
var postProcessWarningsDocURL string
for _, res := range typedResults {
if len(res.Warnings) > 0 {
postProcessWarnings = append(postProcessWarnings, res.Warnings...)
}
if res.WarningsDocURL != "" {
postProcessWarningsDocURL = res.WarningsDocURL
}
}
return finalResults, postProcessWarnings, postProcessWarningsDocURL, nil
}
// postProcessBuilderQuery applies postprocessing to a single builder query result
@@ -178,6 +191,86 @@ func postProcessBuilderQuery[T any](
return result
}
func removeDiagnosticSeriesAndCheckWarnings(result *qbtypes.Result) {
tsData, ok := result.Value.(*qbtypes.TimeSeriesData)
if !ok {
return
}
if tsData == nil {
return
}
var nonDiagnosticBuckets []*qbtypes.AggregationBucket
var bucketSum, bucketCount *qbtypes.AggregationBucket
// First pass: identify diagnostic buckets and separate them from non-diagnostic ones
for _, b := range tsData.Aggregations {
if !slices.Contains(diagnosticColumnAliases, b.Alias) {
nonDiagnosticBuckets = append(nonDiagnosticBuckets, b)
continue
}
// warning columns
switch b.Alias {
case telemetrymetrics.DiagnosticColumnCumulativeHistLeSum:
bucketSum = b
case telemetrymetrics.DiagnosticColumnCumulativeHistLeCount:
bucketCount = b
}
}
// Second pass: calculate warnings based on diagnostic buckets (only once, after identifying both)
allZero := false
if bucketSum != nil {
allZero = true
if len(bucketSum.Series) == 0 {
allZero = false // dont calculate for no values
} else {
for _, s := range bucketSum.Series {
for _, v := range s.Values {
if v.Value > 0 {
allZero = false
break
}
}
if !allZero {
break
}
}
}
}
allSparse := false
if bucketCount != nil {
allSparse = true
if len(bucketCount.Series) == 0 {
allSparse = false // dont calculate for no values
} else {
for _, s := range bucketCount.Series {
for _, v := range s.Values {
if v.Value >= 2 {
allSparse = false
break
}
}
if !allSparse {
break
}
}
}
}
if allZero {
result.Warnings = append(result.Warnings, "No change observed for this cumulative metric in the selected range.")
}
if allSparse {
result.Warnings = append(result.Warnings, "Only +Inf bucket present; add finite buckets to compute quantiles.")
}
tsData.Aggregations = nonDiagnosticBuckets
result.Value = tsData
}
// postProcessMetricQuery applies postprocessing to a metric query result
func postProcessMetricQuery(
q *querier,
@@ -186,6 +279,8 @@ func postProcessMetricQuery(
req *qbtypes.QueryRangeRequest,
) *qbtypes.Result {
removeDiagnosticSeriesAndCheckWarnings(result)
config := query.Aggregations[0]
spaceAggOrderBy := fmt.Sprintf("%s(%s)", config.SpaceAggregation.StringValue(), config.MetricName)
timeAggOrderBy := fmt.Sprintf("%s(%s)", config.TimeAggregation.StringValue(), config.MetricName)

View File

@@ -588,11 +588,19 @@ func (q *querier) run(
}
}
processedResults, err := q.postProcessResults(ctx, results, req)
processedResults, processedResultsWarnings, processedResultsDocsURL, err := q.postProcessResults(ctx, results, req)
if err != nil {
return nil, err
}
// Merge warnings collected during post-processing.
if len(processedResultsWarnings) > 0 {
warnings = append(warnings, processedResultsWarnings...)
}
if processedResultsDocsURL != "" {
warningsDocURL = processedResultsDocsURL
}
// attach step interval to metadata so client can make informed decisions, ex: width of the bar
// or go to related logs/traces from a point in line/bar chart with correct time range
stepIntervals := make(map[string]uint64, len(steps))

View File

@@ -2,6 +2,11 @@ package telemetrymetrics
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
const (
DiagnosticColumnCumulativeHistLeSum = "__diagnostic_cumulative_hist_le_sum"
DiagnosticColumnCumulativeHistLeCount = "__diagnostic_cumulative_hist_le_count"
)
var IntrinsicFields = []string{
"__normalized",
"temporality",

View File

@@ -559,12 +559,8 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
cteArgs [][]any,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
) (*qbtypes.Statement, error) {
combined := querybuilder.CombineCTEs(cteFragments)
var combined string
var args []any
for _, a := range cteArgs {
args = append(args, a...)
}
sb := sqlbuilder.NewSelectBuilder()
@@ -574,25 +570,59 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
}
if quantile != 0 && query.Aggregations[0].Type != metrictypes.ExpHistogramType {
// Build diagnostics CTE to pre-aggregate arrays and counts per ts/group.
diag := sqlbuilder.NewSelectBuilder()
diag.Select("ts")
for _, g := range query.GroupBy {
diag.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
diag.SelectMore("groupArray(le) AS les")
diag.SelectMore("groupArray(value) AS vals")
diag.SelectMore("arraySum(groupArray(value)) AS bucket_sum")
diag.SelectMore("countDistinct(le) AS bucket_count")
diag.From("__spatial_aggregation_cte")
for _, g := range query.GroupBy {
diag.GroupBy(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
diag.GroupBy("ts")
diagQ, diagArgs := diag.BuildWithFlavor(sqlbuilder.ClickHouse)
cteFragments = append(cteFragments, fmt.Sprintf("__diagnostics_cte AS (%s)", diagQ))
cteArgs = append(cteArgs, diagArgs)
combined = querybuilder.CombineCTEs(cteFragments)
for _, a := range cteArgs {
args = append(args, a...)
}
// Final select from diagnostics cte
sb.Select("ts")
for _, g := range query.GroupBy {
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
// expose quantile and diagnostics as __result_* so they are parsed, then stripped in post-processing
// TODO(nikhilmantri0902): putting below as __result_0, __result_1, __result_2,
// but is this correct and not a hack to extract the warnings?
// what if the aliases are used by client, there will be collision in select statements ...
sb.SelectMore(fmt.Sprintf(
"histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), %.3f) AS value",
"histogramQuantile(arrayMap(x -> toFloat64(x), les), vals, %.3f) AS __result_0",
quantile,
))
sb.From("__spatial_aggregation_cte")
for _, g := range query.GroupBy {
sb.GroupBy(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
sb.GroupBy("ts")
sb.SelectMore(fmt.Sprintf("bucket_sum AS %s", DiagnosticColumnCumulativeHistLeSum))
sb.SelectMore(fmt.Sprintf("bucket_count AS %s", DiagnosticColumnCumulativeHistLeCount))
sb.From("__diagnostics_cte")
if query.Having != nil && query.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
sb.Having(rewrittenExpr)
}
} else {
combined = querybuilder.CombineCTEs(cteFragments)
for _, a := range cteArgs {
args = append(args, a...)
}
sb.Select("*")
sb.From("__spatial_aggregation_cte")
if query.Having != nil && query.Having.Expression != "" {
@@ -603,5 +633,8 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
}
q, a := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{Query: combined + q, Args: append(args, a...)}, nil
return &qbtypes.Statement{
Query: combined + q,
Args: append(args, a...),
}, nil
}