mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-03 23:50:33 +01:00
Compare commits
2 Commits
v0.127.0
...
issue_5131
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcbd3e0747 | ||
|
|
987844dbc8 |
@@ -349,7 +349,7 @@ function convertV5DataByType(
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function convertV5ResponseToLegacy(
|
||||
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||
v5Response: SuccessResponse<MetricRangePayloadV5, QueryRangeRequestV5>,
|
||||
legendMap: Record<string, string>,
|
||||
formatForWeb?: boolean,
|
||||
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
|
||||
@@ -357,7 +357,7 @@ export function convertV5ResponseToLegacy(
|
||||
const v5Data = payload?.data;
|
||||
|
||||
const aggregationPerQuery =
|
||||
(params as QueryRangeRequestV5)?.compositeQuery?.queries
|
||||
params?.compositeQuery?.queries
|
||||
?.filter((query) => query.type === 'builder_query')
|
||||
.reduce(
|
||||
(acc, query) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { usePanelContextMenu } from '../usePanelContextMenu';
|
||||
|
||||
@@ -47,10 +46,7 @@ const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
|
||||
const mockQueryResponse = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
} as unknown as UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
} as unknown as UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
|
||||
describe('usePanelContextMenu', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -10,17 +10,13 @@ import {
|
||||
PopoverPosition,
|
||||
useCoordinates,
|
||||
} from 'periscope/components/ContextMenu';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseTimeSeriesContextMenuParams {
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryResponse: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
MetricQueryRangeSuccessResponse,
|
||||
MetricRangePayloadProps,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
@@ -21,10 +23,7 @@ export interface GraphVisibilityLegendEntryProps {
|
||||
|
||||
export interface WidgetGraphComponentProps {
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryResponse: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
errorMessage: string | undefined;
|
||||
version?: string;
|
||||
threshold?: ReactNode;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { ContextLinksData, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
export type GridValueComponentProps = {
|
||||
@@ -13,10 +12,7 @@ export type GridValueComponentProps = {
|
||||
thresholds?: ThresholdProps[];
|
||||
// Context menu related props
|
||||
widget?: Widgets;
|
||||
queryResponse?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryResponse?: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
contextLinks?: ContextLinksData;
|
||||
enableDrillDown?: boolean;
|
||||
};
|
||||
|
||||
@@ -28,9 +28,8 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function WidgetGraph({
|
||||
@@ -202,10 +201,7 @@ function WidgetGraph({
|
||||
|
||||
interface WidgetGraphProps {
|
||||
selectedWidget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryResponse: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
enableDrillDown?: boolean;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { QueryRangeRequestV5 } from 'api/v5/v5';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Column, QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function populateMultipleResults(
|
||||
responseData: SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
): SuccessResponse<MetricRangePayloadProps, unknown> {
|
||||
responseData: SuccessResponse<MetricRangePayloadProps, QueryRangeRequestV5>,
|
||||
): SuccessResponse<MetricRangePayloadProps, QueryRangeRequestV5> {
|
||||
const queryResults = responseData?.payload?.data?.newResult?.data?.result;
|
||||
const allFormattedResults: QueryData[] = [];
|
||||
|
||||
@@ -66,17 +67,19 @@ export function populateMultipleResults(
|
||||
}
|
||||
|
||||
// Create a copy instead of mutating the original
|
||||
const updatedResponseData: SuccessResponse<MetricRangePayloadProps, unknown> =
|
||||
{
|
||||
...responseData,
|
||||
payload: {
|
||||
...responseData.payload,
|
||||
data: {
|
||||
...responseData.payload.data,
|
||||
result: allFormattedResults,
|
||||
},
|
||||
const updatedResponseData: SuccessResponse<
|
||||
MetricRangePayloadProps,
|
||||
QueryRangeRequestV5
|
||||
> = {
|
||||
...responseData,
|
||||
payload: {
|
||||
...responseData.payload,
|
||||
data: {
|
||||
...responseData.payload.data,
|
||||
result: allFormattedResults,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return updatedResponseData;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
getSelectedWidgetIndex,
|
||||
} from 'providers/Dashboard/util';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ColumnUnit,
|
||||
ContextLinksData,
|
||||
@@ -61,7 +60,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -398,7 +397,7 @@ function NewWidget({
|
||||
|
||||
// State to hold query response for sharing between left and right containers
|
||||
const [queryResponse, setQueryResponse] = useState<
|
||||
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||
UseQueryResult<MetricQueryRangeSuccessResponse, Error>
|
||||
>(null as any);
|
||||
|
||||
// request data should be handled by the parent and the child components should consume the same
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { timePreferance } from './RightContainer/timeItems';
|
||||
|
||||
@@ -29,9 +28,7 @@ export interface WidgetGraphProps {
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
isLoadingPanelData: boolean;
|
||||
setQueryResponse?: Dispatch<
|
||||
SetStateAction<
|
||||
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||
>
|
||||
SetStateAction<UseQueryResult<MetricQueryRangeSuccessResponse, Error>>
|
||||
>;
|
||||
enableDrillDown?: boolean;
|
||||
dashboardData: Dashboard | undefined;
|
||||
@@ -39,12 +36,7 @@ export interface WidgetGraphProps {
|
||||
}
|
||||
|
||||
export type WidgetGraphContainerProps = {
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown> & {
|
||||
warning?: Warning;
|
||||
},
|
||||
Error
|
||||
>;
|
||||
queryResponse: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
selectedWidget: Widgets;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
import { QueryRangeRequestV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import { PanelWrapperProps } from './panelWrapper.types';
|
||||
|
||||
@@ -20,7 +19,7 @@ function TablePanelWrapper({
|
||||
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
|
||||
const { thresholds } = widget;
|
||||
|
||||
const queryRangeRequest = queryResponse.data?.params as QueryRangeRequestV5;
|
||||
const queryRangeRequest = queryResponse.data?.params;
|
||||
|
||||
return (
|
||||
<GridTableComponent
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useDashboardVarConfig from 'container/QueryTable/Drilldown/useDashboardVarConfig';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { ContextLinksData } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
@@ -44,10 +43,7 @@ const useAggregateDrilldown = ({
|
||||
aggregateData: AggregateData | null;
|
||||
contextLinks?: ContextLinksData;
|
||||
panelType?: PANEL_TYPES;
|
||||
queryRange?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryRange?: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
}): {
|
||||
aggregateDrilldownConfig: {
|
||||
header?: string | React.ReactNode;
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { ContextLinksData } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { isValidQueryName } from './drilldownUtils';
|
||||
@@ -20,10 +19,7 @@ interface UseGraphContextMenuProps {
|
||||
setSubMenu: (subMenu: string) => void;
|
||||
contextLinks?: ContextLinksData;
|
||||
panelType?: PANEL_TYPES;
|
||||
queryRange?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
queryRange?: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
}
|
||||
|
||||
export function useGraphContextMenu({
|
||||
|
||||
@@ -7,8 +7,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -19,16 +18,13 @@ export const useGetExplorerQueryRange = (
|
||||
requestData: Query | null,
|
||||
panelType: PANEL_TYPES | null,
|
||||
version: string,
|
||||
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
||||
options?: UseQueryOptions<MetricQueryRangeSuccessResponse, Error>,
|
||||
params?: Record<string, unknown>,
|
||||
isDependentOnQB = true,
|
||||
keyRef?: MutableRefObject<any>,
|
||||
headers?: Record<string, string>,
|
||||
selectedTimeInterval?: GetQueryResultsProps['globalSelectedInterval'],
|
||||
): UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps> & { warning?: Warning },
|
||||
Error
|
||||
> => {
|
||||
): UseQueryResult<MetricQueryRangeSuccessResponse, Error> => {
|
||||
const { isEnabledQuery } = useQueryBuilder();
|
||||
const {
|
||||
selectedTime: globalSelectedInterval,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
} from '../queryBuilder/queryBuilderData';
|
||||
import { ExecStats } from '../v5/queryRange';
|
||||
import { ExecStats, QueryRangeRequestV5 } from '../v5/queryRange';
|
||||
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
|
||||
|
||||
export type QueryRangePayload = {
|
||||
@@ -39,11 +39,16 @@ export interface MetricRangePayloadProps {
|
||||
meta?: ExecStats;
|
||||
}
|
||||
|
||||
/** Query range success response including optional warning and meta */
|
||||
export type MetricQueryRangeSuccessResponse = SuccessResponse<
|
||||
/** Query range success response. `params` is the request that produced the
|
||||
* payload; `warning` and `meta` are lifted from the payload to the top level
|
||||
* by `getQueryResults.ts` for consumer convenience. */
|
||||
export interface MetricQueryRangeSuccessResponse extends SuccessResponse<
|
||||
MetricRangePayloadProps,
|
||||
unknown
|
||||
> & { warning?: Warning; meta?: ExecStats };
|
||||
QueryRangeRequestV5
|
||||
> {
|
||||
warning?: Warning;
|
||||
meta?: ExecStats;
|
||||
}
|
||||
|
||||
export interface MetricRangePayloadV3 {
|
||||
data: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import store from 'store';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryRangeRequestV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
export const getTimeRangeFromQueryRangeRequest = (
|
||||
@@ -28,13 +27,9 @@ export const getTimeRangeFromQueryRangeRequest = (
|
||||
};
|
||||
|
||||
export const getTimeRange = (
|
||||
widgetQueryRange?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>,
|
||||
widgetQueryRange?: UseQueryResult<MetricQueryRangeSuccessResponse, Error>,
|
||||
): Record<string, number> => {
|
||||
const widgetParams =
|
||||
(widgetQueryRange?.data?.params as QueryRangeRequestV5) || null;
|
||||
const widgetParams = widgetQueryRange?.data?.params;
|
||||
|
||||
return getTimeRangeFromQueryRangeRequest(widgetParams);
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ func (c Config) Validate() error {
|
||||
if c.MaxConcurrentQueries <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "max_concurrent_queries must be positive, got %v", c.MaxConcurrentQueries)
|
||||
}
|
||||
if c.SkipResourceFingerprint.Enabled && c.SkipResourceFingerprint.Threshold == 0 {
|
||||
if c.SkipResourceFingerprint.Enabled && c.SkipResourceFingerprint.Threshold <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "skip_resource_fingerprint.threshold must be > 0 when enabled")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -716,6 +716,22 @@ func aggOrderBy(k qbtypes.OrderBy, q qbtypes.QueryBuilderQuery[qbtypes.LogAggreg
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// maybeAttachResourceFilter decides whether to pre-filter on the resource table.
|
||||
//
|
||||
// The resource table maps resource attributes (e.g. service.name) to fingerprints.
|
||||
// When it helps, we look up the matching fingerprints there first and feed them to the
|
||||
// main query as a CTE, so the main table only scans those fingerprints.
|
||||
//
|
||||
// There are three outcomes:
|
||||
//
|
||||
// 1. Skip it — the filter has nothing we can pre-resolve. This happens when the filter
|
||||
// is empty,no resource filter, or when its resource conditions sit under an OR (e.g.
|
||||
// `name='GET' OR service.name='abc'`), because then we can't reduce to a fixed set
|
||||
// of fingerprints. The main query filters on resource attributes inline instead.
|
||||
// 2. Skip it — too many fingerprints match (over the configured threshold), so the CTE
|
||||
// would not be selective enough to be worth it. Again, filter inline on the main table.
|
||||
// 3. Use it — attach the matching fingerprints as the __resource_filter CTE and join
|
||||
// the main table on resource_fingerprint.
|
||||
func (b *logQueryStatementBuilder) maybeAttachResourceFilter(
|
||||
ctx context.Context,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
|
||||
@@ -1230,7 +1230,7 @@ func TestSkipResourceFingerprintLogs(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
mock.ExpectQueryRow(`SELECT uniq\(fingerprint\) FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{uint64(2)}))
|
||||
@@ -1250,7 +1250,7 @@ func TestSkipResourceFingerprintLogs(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
mock.ExpectQueryRow(`SELECT uniq\(fingerprint\) FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{threshold}))
|
||||
|
||||
@@ -98,17 +98,7 @@ func (b *resourceFilterStatementBuilder[T]) Build(
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*qbtypes.Statement, error) {
|
||||
q := sqlbuilder.NewSelectBuilder()
|
||||
q.Select("fingerprint")
|
||||
q.From(fmt.Sprintf("%s.%s", b.dbName, b.tableName))
|
||||
|
||||
keySelectors := b.getKeySelectors(query)
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNoOp, err := b.addConditions(ctx, q, start, end, query, keys, variables)
|
||||
q, isNoOp, err := b.buildQuery(ctx, start, end, "fingerprint", query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,8 +117,37 @@ func (b *resourceFilterStatementBuilder[T]) Build(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildQuery selects selectExpr from the resource table and applies the filter and
|
||||
// time conditions. isNoOp is true when the filter resolves to no resource conditions.
|
||||
func (b *resourceFilterStatementBuilder[T]) buildQuery(
|
||||
ctx context.Context,
|
||||
start, end uint64,
|
||||
selectExpr string,
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*sqlbuilder.SelectBuilder, bool, error) {
|
||||
q := sqlbuilder.NewSelectBuilder()
|
||||
q.Select(selectExpr)
|
||||
q.From(fmt.Sprintf("%s.%s", b.dbName, b.tableName))
|
||||
|
||||
keySelectors := b.getKeySelectors(query)
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
isNoOp, err := b.addConditions(ctx, q, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return q, isNoOp, nil
|
||||
}
|
||||
|
||||
// BuildCount returns a statement that counts the distinct fingerprints matching
|
||||
// the resource filter. Returns (nil, nil) when the filter is a no-op.
|
||||
//
|
||||
// It uses uniq() rather than count() over a GROUP BY: uniq's approximation is well
|
||||
// within tolerance for a threshold check and is ~2x faster with far less memory.
|
||||
func (b *resourceFilterStatementBuilder[T]) BuildCount(
|
||||
ctx context.Context,
|
||||
start uint64,
|
||||
@@ -136,13 +155,18 @@ func (b *resourceFilterStatementBuilder[T]) BuildCount(
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*qbtypes.Statement, error) {
|
||||
inner, err := b.Build(ctx, start, end, qbtypes.RequestTypeRaw, query, variables)
|
||||
if err != nil || inner == nil {
|
||||
q, isNoOp, err := b.buildQuery(ctx, start, end, "uniq(fingerprint)", query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isNoOp {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
stmt, args := q.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: fmt.Sprintf("SELECT count() FROM (%s)", inner.Query),
|
||||
Args: inner.Args,
|
||||
Query: stmt,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -818,6 +818,22 @@ func aggOrderBy(k qbtypes.OrderBy, q qbtypes.QueryBuilderQuery[qbtypes.TraceAggr
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// maybeAttachResourceFilter decides whether to pre-filter on the resource table.
|
||||
//
|
||||
// The resource table maps resource attributes (e.g. service.name) to fingerprints.
|
||||
// When it helps, we look up the matching fingerprints there first and feed them to the
|
||||
// main query as a CTE, so the main table only scans those fingerprints.
|
||||
//
|
||||
// There are three outcomes:
|
||||
//
|
||||
// 1. Skip it — the filter has nothing we can pre-resolve. This happens when the filter
|
||||
// is empty,no resource filter, or when its resource conditions sit under an OR (e.g.
|
||||
// `name='GET' OR service.name='abc'`), because then we can't reduce to a fixed set
|
||||
// of fingerprints. The main query filters on resource attributes inline instead.
|
||||
// 2. Skip it — too many fingerprints match (over the configured threshold), so the CTE
|
||||
// would not be selective enough to be worth it. Again, filter inline on the main table.
|
||||
// 3. Use it — attach the matching fingerprints as the __resource_filter CTE and join
|
||||
// the main table on resource_fingerprint.
|
||||
func (b *traceQueryStatementBuilder) maybeAttachResourceFilter(
|
||||
ctx context.Context,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
|
||||
@@ -1629,7 +1629,7 @@ func TestSkipResourceFingerprint(t *testing.T) {
|
||||
|
||||
// Only the count query runs against the telemetry store; the CTE
|
||||
// itself is embedded as SQL in the main query (no extra round trip).
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
mock.ExpectQueryRow(`SELECT uniq\(fingerprint\) FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{uint64(2)}))
|
||||
@@ -1649,7 +1649,7 @@ func TestSkipResourceFingerprint(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
mock.ExpectQueryRow(`SELECT uniq\(fingerprint\) FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{threshold}))
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"""
|
||||
Transparency check for the skip_resource_fingerprint optimization (traces and logs).
|
||||
|
||||
At or above the configured fingerprint threshold the optimization pushes resource
|
||||
conditions onto the main spans/logs table instead of the fingerprint CTE. That
|
||||
rewrite must change ClickHouse performance, never the rows: each test runs the same
|
||||
query against the primary instance (optimization on, threshold=2) and
|
||||
`signoz_fingerprint` (optimization off) and asserts the responses are identical.
|
||||
The optimization changes how a query's resource conditions are resolved depending on
|
||||
how selective they are, but must change only ClickHouse performance, never the rows.
|
||||
Each test runs the same query against the primary instance (optimization on,
|
||||
threshold=2) and `signoz_fingerprint` (optimization off) and asserts the responses
|
||||
are identical, covering all three resolver outcomes:
|
||||
|
||||
- CTE: a filter matching fewer fingerprints than the threshold resolves through the
|
||||
fingerprint CTE.
|
||||
- Fallback: a filter matching at or above the threshold pushes resource conditions
|
||||
onto the main spans/logs table instead.
|
||||
- No-op: a filter with no resource conditions to pre-resolve (no resource field, or
|
||||
resource fields only under an OR) filters inline on the main table.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
@@ -65,6 +72,122 @@ def test_skip_resource_fingerprint_traces_fallback_matches_fingerprint(
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_traces_cte_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""A < 2-fingerprint filter resolves through the fingerprint CTE; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
# One service shares the env (1 fingerprint < threshold 2 -> CTE); two spans on it
|
||||
# exercise dedup. A second service in a different env must be excluded.
|
||||
env = {"deployment.environment": "skip-cte"}
|
||||
insert_traces(
|
||||
[
|
||||
Traces(timestamp=now - timedelta(seconds=10), resources={"service.name": "skip-cte-svc-a", **env}),
|
||||
Traces(timestamp=now - timedelta(seconds=9), resources={"service.name": "skip-cte-svc-a", **env}),
|
||||
Traces(timestamp=now - timedelta(seconds=8), resources={"service.name": "skip-cte-other", "deployment.environment": "skip-cte-other-env"}),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="traces",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="deployment.environment = 'skip-cte'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 2
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_traces_or_filter_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""A resource condition under an OR has no fixed fingerprint set, so it filters inline; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
# `name = ... OR service.name = ...` can't reduce to a fingerprint set (no-op path).
|
||||
# span-1 matches on name, span-2 on service.name, span-3 matches neither.
|
||||
env = {"deployment.environment": "skip-or"}
|
||||
insert_traces(
|
||||
[
|
||||
Traces(timestamp=now - timedelta(seconds=10), name="tr-or-name", resources={"service.name": "tr-or-svc-x", **env}),
|
||||
Traces(timestamp=now - timedelta(seconds=9), name="tr-or-other", resources={"service.name": "tr-or-svc-a", **env}),
|
||||
Traces(timestamp=now - timedelta(seconds=8), name="tr-or-other", resources={"service.name": "tr-or-svc-b", **env}),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="traces",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="name = 'tr-or-name' OR service.name = 'tr-or-svc-a'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 2
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_traces_no_resource_filter_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""A filter with no resource field has nothing to pre-resolve, so it filters inline; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
# Filtering only on the span name (an intrinsic, non-resource field) is a no-op for
|
||||
# the resolver; span-1 matches, span-2 does not.
|
||||
env = {"deployment.environment": "skip-nr"}
|
||||
insert_traces(
|
||||
[
|
||||
Traces(timestamp=now - timedelta(seconds=10), name="tr-nr-name", resources={"service.name": "tr-nr-svc-a", **env}),
|
||||
Traces(timestamp=now - timedelta(seconds=9), name="tr-nr-other", resources={"service.name": "tr-nr-svc-b", **env}),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="traces",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="name = 'tr-nr-name'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 1
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_fallback_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
@@ -103,3 +226,119 @@ def test_skip_resource_fingerprint_logs_fallback_matches_fingerprint(
|
||||
|
||||
assert len(get_rows(optimized)) == 3
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_cte_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""A < 2-fingerprint filter resolves through the fingerprint CTE; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
# One service shares the env (1 fingerprint < threshold 2 -> CTE); two logs on it
|
||||
# exercise dedup. A second service in a different env must be excluded.
|
||||
env = {"deployment.environment": "skip-logs-cte"}
|
||||
insert_logs(
|
||||
[
|
||||
Logs(timestamp=now - timedelta(seconds=10), resources={"service.name": "skip-logs-cte-svc-a", **env}, body="a"),
|
||||
Logs(timestamp=now - timedelta(seconds=9), resources={"service.name": "skip-logs-cte-svc-a", **env}, body="b"),
|
||||
Logs(timestamp=now - timedelta(seconds=8), resources={"service.name": "skip-logs-cte-other", "deployment.environment": "skip-logs-cte-other-env"}, body="noise"),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="logs",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="deployment.environment = 'skip-logs-cte'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource"), TelemetryFieldKey("body")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 2
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_or_filter_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""A resource condition under an OR has no fixed fingerprint set, so it filters inline; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
# `test.marker = ... OR service.name = ...` can't reduce to a fingerprint set (no-op path).
|
||||
# log-1 matches on the attribute, log-2 on service.name, log-3 matches neither.
|
||||
env = {"deployment.environment": "skip-logs-or"}
|
||||
insert_logs(
|
||||
[
|
||||
Logs(timestamp=now - timedelta(seconds=10), resources={"service.name": "logs-or-svc-x", **env}, attributes={"test.marker": "logs-or-hit"}, body="a"),
|
||||
Logs(timestamp=now - timedelta(seconds=9), resources={"service.name": "logs-or-svc-a", **env}, body="b"),
|
||||
Logs(timestamp=now - timedelta(seconds=8), resources={"service.name": "logs-or-svc-b", **env}, body="noise"),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="logs",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="test.marker = 'logs-or-hit' OR service.name = 'logs-or-svc-a'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource"), TelemetryFieldKey("body")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 2
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_no_resource_filter_matches_fingerprint(
|
||||
signoz: types.SigNoz,
|
||||
signoz_fingerprint: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""A filter with no resource field has nothing to pre-resolve, so it filters inline; rows must match the baseline."""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
# Filtering only on an attribute (a non-resource field) is a no-op for the resolver;
|
||||
# log-1 matches, log-2 does not.
|
||||
env = {"deployment.environment": "skip-logs-nr"}
|
||||
insert_logs(
|
||||
[
|
||||
Logs(timestamp=now - timedelta(seconds=10), resources={"service.name": "logs-nr-svc-a", **env}, attributes={"test.marker": "logs-nr-hit"}, body="a"),
|
||||
Logs(timestamp=now - timedelta(seconds=9), resources={"service.name": "logs-nr-svc-b", **env}, body="b"),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = BuilderQuery(
|
||||
signal="logs",
|
||||
limit=50,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
|
||||
filter_expression="test.marker = 'logs-nr-hit'",
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource"), TelemetryFieldKey("body")],
|
||||
).to_dict()
|
||||
|
||||
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
|
||||
|
||||
assert len(get_rows(optimized)) == 1
|
||||
assert_identical_query_response(optimized, fingerprint)
|
||||
|
||||
Reference in New Issue
Block a user