mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-22 00:00:27 +01:00
Compare commits
2 Commits
tvats-fat-
...
issue-5341
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc1cfc19d3 | ||
|
|
b8567664da |
@@ -1,67 +1,29 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import {
|
||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { cloneDeep, isArray, isFunction } from 'lodash-es';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import { isKeyMatch } from './utils';
|
||||
import useActiveQueryIndex from './useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from './useCheckboxFilterState';
|
||||
import useCheckboxFilterValues from './useCheckboxFilterValues';
|
||||
|
||||
import './Checkbox.styles.scss';
|
||||
|
||||
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
}
|
||||
|
||||
function setDefaultValues(
|
||||
values: string[],
|
||||
trueOrFalse: boolean,
|
||||
): Record<string, boolean> {
|
||||
const defaultState: Record<string, boolean> = {};
|
||||
values.forEach((val) => {
|
||||
defaultState[val] = trueOrFalse;
|
||||
});
|
||||
return defaultState;
|
||||
}
|
||||
interface ICheckboxProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
@@ -72,194 +34,39 @@ interface ICheckboxProps {
|
||||
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
const { source, filter, onFilterChange } = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
// null = no user action, true = user opened, false = user closed
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
const {
|
||||
lastUsedQuery,
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
|
||||
// Determine if we're in ListView mode
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
|
||||
// Otherwise use lastUsedQuery for non-ListView modes
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
|
||||
// Check if this filter has active filters in the query
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
// Derive isOpen from filter state + user action
|
||||
const isOpen = useMemo(() => {
|
||||
// If user explicitly toggled, respect that
|
||||
if (userToggleState !== null) {
|
||||
return userToggleState;
|
||||
}
|
||||
|
||||
// Auto-open if this filter has active filters in the query
|
||||
if (isSomeFilterPresentForCurrentAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise use default behavior (first 2 filters open)
|
||||
return filter.defaultOpen;
|
||||
}, [
|
||||
userToggleState,
|
||||
isOpen,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
filter.defaultOpen,
|
||||
]);
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
} = useCheckboxDisclosure({ filter, activeQueryIndex });
|
||||
|
||||
const { data, isLoading } = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||
dataSource: filter.dataSource || DataSource.LOGS,
|
||||
aggregateAttribute: filter.aggregateAttribute || '',
|
||||
attributeKey: filter.attributeKey.key,
|
||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||
tagType: filter.attributeKey.type || '',
|
||||
searchText: searchText ?? '',
|
||||
},
|
||||
{
|
||||
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
const { attributeValues, isLoading } = useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
});
|
||||
|
||||
const { data: keyValueSuggestions, isLoading: isLoadingKeyValueSuggestions } =
|
||||
useGetQueryKeyValueSuggestions({
|
||||
key: filter.attributeKey.key,
|
||||
signal: filter.dataSource || DataSource.LOGS,
|
||||
signalSource: 'meter',
|
||||
options: {
|
||||
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
});
|
||||
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
|
||||
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
|
||||
|
||||
const attributeValues: string[] = useMemo(() => {
|
||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||
|
||||
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||
// Process the response data
|
||||
const responseData = keyValueSuggestions?.data as any;
|
||||
const values = responseData.data?.values || {};
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
.filter(
|
||||
(value: string | null | undefined): value is string =>
|
||||
value !== null && value !== undefined && value !== '',
|
||||
);
|
||||
|
||||
// Generate options from number values
|
||||
const numberOptions = numberValues
|
||||
.filter(
|
||||
(value: number | null | undefined): value is number =>
|
||||
value !== null && value !== undefined,
|
||||
)
|
||||
.map((value: number) => value.toString());
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
return [...stringOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
return (data?.payload?.[key] || []).filter(
|
||||
(val) => val !== undefined && val !== null,
|
||||
);
|
||||
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
});
|
||||
|
||||
const setSearchTextDebounced = useDebouncedFn((...args) => {
|
||||
setSearchText(args[0] as string);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
|
||||
// also we need to keep a note of last focussed query.
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const currentFilterState = useMemo(() => {
|
||||
let filterState: Record<string, boolean> = setDefaultValues(
|
||||
attributeValues,
|
||||
false,
|
||||
);
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items.find((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (filterSync) {
|
||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = true;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = true;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
}
|
||||
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = false;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = false;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
}
|
||||
return filterState;
|
||||
}, [
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
]);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// variable to check if the current filter has multiple values to its name in the key op value section
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
// Sort checked items to the top, then unchecked items
|
||||
const currentAttributeKeys = useMemo(() => {
|
||||
const checkedValues = attributeValues.filter(
|
||||
@@ -277,293 +84,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
[currentAttributeKeys, currentFilterState],
|
||||
);
|
||||
|
||||
const handleClearFilterAttribute = (): void => {
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(preparedQuery);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): void => {
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only';
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
} else if (query?.filters?.items) {
|
||||
if (
|
||||
query.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)
|
||||
) {
|
||||
// if there is already a running filter for the current attribute key then
|
||||
// we split the cases by which particular operator is present right now!
|
||||
const currentFilter = query.filters?.items?.find((q) =>
|
||||
isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (currentFilter) {
|
||||
const runningOperator = currentFilter?.op;
|
||||
switch (runningOperator) {
|
||||
case 'in':
|
||||
if (checked) {
|
||||
// if it's an IN operator then if we are checking another value it get's added to the
|
||||
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// if the current state wasn't an array we make it one and add our value
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (!checked) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// if not an array remove the whole thing altogether!
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'nin':
|
||||
case 'not in':
|
||||
// if the current running operator is NIN then when unchecking the value it gets
|
||||
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||
if (!checked) {
|
||||
// in case of array add the currentUnselectedValue to the list.
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// in case of not an array make it one!
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (checked) {
|
||||
// opposite of above!
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
if (checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (!checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// case - when there is no filter for the current key that means all are selected right now.
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
}
|
||||
const finalQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(finalQuery);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(finalQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptyStateWithDocsEnabled =
|
||||
SOURCES_WITH_EMPTY_STATE_ENABLED.includes(source) &&
|
||||
!searchText &&
|
||||
@@ -571,48 +91,19 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="checkbox-filter">
|
||||
<section
|
||||
className="filter-header-checkbox"
|
||||
onClick={(): void => {
|
||||
if (isOpen) {
|
||||
setUserToggleState(false);
|
||||
setVisibleItemsCount(10);
|
||||
} else {
|
||||
setUserToggleState(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section className="left-action">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={13} cursor="pointer" />
|
||||
) : (
|
||||
<ChevronRight size={13} cursor="pointer" />
|
||||
)}
|
||||
<Typography.Text className="title">{filter.title}</Typography.Text>
|
||||
<CheckboxFilterHeader
|
||||
title={filter.title}
|
||||
isOpen={isOpen}
|
||||
showClearAll={!!attributeValues.length}
|
||||
onToggleOpen={onToggleOpen}
|
||||
onClear={onClear}
|
||||
/>
|
||||
{isOpen && isLoading && !attributeValues.length && (
|
||||
<section className="loading">
|
||||
<Skeleton paragraph={{ rows: 4 }} />
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && !!attributeValues.length && (
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleClearFilterAttribute();
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
{isOpen &&
|
||||
(isLoading || isLoadingKeyValueSuggestions) &&
|
||||
!attributeValues.length && (
|
||||
<section className="loading">
|
||||
<Skeleton paragraph={{ rows: 4 }} />
|
||||
</section>
|
||||
)}
|
||||
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
|
||||
)}
|
||||
{isOpen && !isLoading && (
|
||||
<>
|
||||
{!isEmptyStateWithDocsEnabled && (
|
||||
<section className="search">
|
||||
@@ -634,48 +125,24 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
data-testid="filter-separator"
|
||||
/>
|
||||
)}
|
||||
<div className="value">
|
||||
<Checkbox
|
||||
onChange={(checked): void =>
|
||||
onChange(value, checked === true, false)
|
||||
}
|
||||
value={currentFilterState[value]}
|
||||
disabled={isFilterDisabled}
|
||||
className="check-box"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'checkbox-value-section',
|
||||
isFilterDisabled ? 'filter-disabled' : '',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
if (isFilterDisabled) {
|
||||
return;
|
||||
}
|
||||
onChange(value, currentFilterState[value], true);
|
||||
}}
|
||||
>
|
||||
<div className={`${filter.title} label-${value}`} />
|
||||
{filter.customRendererForValue ? (
|
||||
filter.customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only'}
|
||||
</Button>
|
||||
<Button type="text" className="toggle-btn">
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CheckboxValueRow
|
||||
value={value}
|
||||
checked={currentFilterState[value]}
|
||||
disabled={isFilterDisabled}
|
||||
title={filter.title}
|
||||
onlyButtonLabel={
|
||||
isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only'
|
||||
}
|
||||
customRendererForValue={filter.customRendererForValue}
|
||||
onCheckboxChange={(checked): void => onChange(value, checked, false)}
|
||||
onOnlyOrAllClick={(): void =>
|
||||
onChange(value, currentFilterState[value], true)
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</section>
|
||||
@@ -688,10 +155,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
)}
|
||||
{visibleItemsCount < attributeValues?.length && (
|
||||
<section className="show-more">
|
||||
<Typography.Text
|
||||
className="show-more-text"
|
||||
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||
>
|
||||
<Typography.Text className="show-more-text" onClick={onShowMore}>
|
||||
Show More...
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
|
||||
interface CheckboxFilterHeaderProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
showClearAll: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
function CheckboxFilterHeader({
|
||||
title,
|
||||
isOpen,
|
||||
showClearAll,
|
||||
onToggleOpen,
|
||||
onClear,
|
||||
}: CheckboxFilterHeaderProps): JSX.Element {
|
||||
return (
|
||||
<section className="filter-header-checkbox" onClick={onToggleOpen}>
|
||||
<section className="left-action">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={13} cursor="pointer" />
|
||||
) : (
|
||||
<ChevronRight size={13} cursor="pointer" />
|
||||
)}
|
||||
<Typography.Text className="title">{title}</Typography.Text>
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && showClearAll && (
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckboxFilterHeader;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Button } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
interface CheckboxValueRowProps {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
title: string;
|
||||
onlyButtonLabel: string;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
onCheckboxChange: (checked: boolean) => void;
|
||||
onOnlyOrAllClick: () => void;
|
||||
}
|
||||
|
||||
function CheckboxValueRow({
|
||||
value,
|
||||
checked,
|
||||
disabled,
|
||||
title,
|
||||
onlyButtonLabel,
|
||||
customRendererForValue,
|
||||
onCheckboxChange,
|
||||
onOnlyOrAllClick,
|
||||
}: CheckboxValueRowProps): JSX.Element {
|
||||
return (
|
||||
<div className="value">
|
||||
<Checkbox
|
||||
onChange={(isChecked): void => onCheckboxChange(isChecked === true)}
|
||||
value={checked}
|
||||
disabled={disabled}
|
||||
className="check-box"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cx('checkbox-value-section', disabled ? 'filter-disabled' : '')}
|
||||
onClick={(): void => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onOnlyOrAllClick();
|
||||
}}
|
||||
>
|
||||
<div className={`${title} label-${value}`} />
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{onlyButtonLabel}
|
||||
</Button>
|
||||
<Button type="text" className="toggle-btn">
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxValueRow.defaultProps = {
|
||||
customRendererForValue: undefined,
|
||||
};
|
||||
|
||||
export default CheckboxValueRow;
|
||||
@@ -0,0 +1,417 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
export function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
}
|
||||
|
||||
function setDefaultValues(
|
||||
values: string[],
|
||||
trueOrFalse: boolean,
|
||||
): Record<string, boolean> {
|
||||
const defaultState: Record<string, boolean> = {};
|
||||
values.forEach((val) => {
|
||||
defaultState[val] = trueOrFalse;
|
||||
});
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the checked/unchecked state for each attribute value by reading the
|
||||
* active filter clause for this attribute key out of the query.
|
||||
*
|
||||
* - No matching clause -> every value is checked (all selected).
|
||||
* - IN / `=` clause -> only the listed values are checked.
|
||||
* - NOT IN / `!=` clause -> every value is checked except the excluded ones.
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems,
|
||||
filterKey,
|
||||
}: {
|
||||
attributeValues: string[];
|
||||
filterItems: TagFilterItem[] | undefined;
|
||||
filterKey: string;
|
||||
}): Record<string, boolean> {
|
||||
let filterState: Record<string, boolean> = setDefaultValues(
|
||||
attributeValues,
|
||||
false,
|
||||
);
|
||||
const filterSync = filterItems?.find((item) =>
|
||||
isKeyMatch(item.key?.key, filterKey),
|
||||
);
|
||||
|
||||
if (filterSync) {
|
||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = true;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = true;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
}
|
||||
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = false;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = false;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
}
|
||||
return filterState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new query with every clause for this attribute key removed, both
|
||||
* from the structured filter items and the raw filter expression.
|
||||
*/
|
||||
export function clearFilterFromQuery({
|
||||
currentQuery,
|
||||
filter,
|
||||
activeQueryIndex,
|
||||
}: {
|
||||
currentQuery: Query;
|
||||
filter: IQuickFiltersConfig;
|
||||
activeQueryIndex: number;
|
||||
}): Query {
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
isOnlyOrAllClicked,
|
||||
}: {
|
||||
currentQuery: Query;
|
||||
activeQueryIndex: number;
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
value: string;
|
||||
checked: boolean;
|
||||
isOnlyOrAllClicked: boolean;
|
||||
}): Query {
|
||||
const activeItems =
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
const currentFilterState = deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems: activeItems,
|
||||
filterKey: filter.attributeKey.key,
|
||||
});
|
||||
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only';
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(query.filter.expression, [
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
} else if (query?.filters?.items) {
|
||||
if (
|
||||
query.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)
|
||||
) {
|
||||
// if there is already a running filter for the current attribute key then
|
||||
// we split the cases by which particular operator is present right now!
|
||||
const currentFilter = query.filters?.items?.find((q) =>
|
||||
isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (currentFilter) {
|
||||
const runningOperator = currentFilter?.op;
|
||||
switch (runningOperator) {
|
||||
case 'in':
|
||||
if (checked) {
|
||||
// if it's an IN operator then if we are checking another value it get's added to the
|
||||
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// if the current state wasn't an array we make it one and add our value
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (!checked) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// if not an array remove the whole thing altogether!
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'nin':
|
||||
case 'not in':
|
||||
// if the current running operator is NIN then when unchecking the value it gets
|
||||
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||
if (!checked) {
|
||||
// in case of array add the currentUnselectedValue to the list.
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// in case of not an array make it one!
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (checked) {
|
||||
// opposite of above!
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
if (checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (!checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// case - when there is no filter for the current key that means all are selected right now.
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
/**
|
||||
* Resolves which query-builder query index the checkbox filter reads from and
|
||||
* writes to.
|
||||
*
|
||||
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
|
||||
* mode track the last focused query.
|
||||
*/
|
||||
function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
const { lastUsedQuery, panelType } = useQueryBuilder();
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
const DEFAULT_VISIBLE_ITEMS_COUNT = 10;
|
||||
|
||||
interface UseCheckboxDisclosureProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
activeQueryIndex: number;
|
||||
}
|
||||
|
||||
interface UseCheckboxDisclosureReturn {
|
||||
isOpen: boolean;
|
||||
isSomeFilterPresentForCurrentAttribute: boolean;
|
||||
visibleItemsCount: number;
|
||||
onToggleOpen: () => void;
|
||||
onShowMore: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the open/collapsed state of a checkbox filter section and how many
|
||||
* values are visible.
|
||||
*
|
||||
* Auto-opens when the query already has a clause for this attribute, otherwise
|
||||
* falls back to `filter.defaultOpen`. An explicit user toggle always wins.
|
||||
* Collapsing resets the visible count.
|
||||
*/
|
||||
function useCheckboxDisclosure({
|
||||
filter,
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxDisclosureProps): UseCheckboxDisclosureReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
// null = no user action, true = user opened, false = user closed
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(
|
||||
DEFAULT_VISIBLE_ITEMS_COUNT,
|
||||
);
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => {
|
||||
// If user explicitly toggled, respect that
|
||||
if (userToggleState !== null) {
|
||||
return userToggleState;
|
||||
}
|
||||
|
||||
// Auto-open if this filter has active filters in the query
|
||||
if (isSomeFilterPresentForCurrentAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise use default behavior (first 2 filters open)
|
||||
return filter.defaultOpen;
|
||||
}, [
|
||||
userToggleState,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
filter.defaultOpen,
|
||||
]);
|
||||
|
||||
const onToggleOpen = (): void => {
|
||||
if (isOpen) {
|
||||
setUserToggleState(false);
|
||||
setVisibleItemsCount(DEFAULT_VISIBLE_ITEMS_COUNT);
|
||||
} else {
|
||||
setUserToggleState(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onShowMore = (): void => {
|
||||
setVisibleItemsCount((prev) => prev + DEFAULT_VISIBLE_ITEMS_COUNT);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxDisclosure;
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
applyCheckboxToggle,
|
||||
clearFilterFromQuery,
|
||||
} from './checkboxFilterQuery';
|
||||
|
||||
interface UseCheckboxFilterActionsProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
onFilterChange?: ((query: Query) => void) | null;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterActionsReturn {
|
||||
onChange: (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the pure checkbox query algebra to query-builder dispatch: the
|
||||
* caller-provided `onFilterChange` when present, otherwise a URL redirect.
|
||||
*/
|
||||
function useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
}: UseCheckboxFilterActionsProps): UseCheckboxFilterActionsReturn {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const dispatch = (query: Query): void => {
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(query);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(query);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
): void => {
|
||||
dispatch(
|
||||
applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
isOnlyOrAllClicked,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onClear = (): void => {
|
||||
dispatch(clearFilterFromQuery({ currentQuery, filter, activeQueryIndex }));
|
||||
};
|
||||
|
||||
return { onChange, onClear };
|
||||
}
|
||||
|
||||
export default useCheckboxFilterActions;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { deriveCheckboxState } from './checkboxFilterQuery';
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
interface UseCheckboxFilterStateProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterStateReturn {
|
||||
currentFilterState: Record<string, boolean>;
|
||||
isFilterDisabled: boolean;
|
||||
isMultipleValuesTrueForTheKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the active query and derives the per-value checked state for this
|
||||
* attribute, whether the filter is disabled (same key used more than once in
|
||||
* the filter bar), and whether more than one value is currently selected.
|
||||
*/
|
||||
function useCheckboxFilterState({
|
||||
filter,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
// derive the state of each filter key here and keep it in sync with current query
|
||||
const currentFilterState = useMemo(
|
||||
() =>
|
||||
deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems:
|
||||
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterKey: filter.attributeKey.key,
|
||||
}),
|
||||
[
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
],
|
||||
);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// whether the current filter has multiple values to its name in the key op value section
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
return {
|
||||
currentFilterState,
|
||||
isFilterDisabled,
|
||||
isMultipleValuesTrueForTheKey,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxFilterState;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseCheckboxFilterValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
searchText: string;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterValuesReturn {
|
||||
attributeValues: string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
}: UseCheckboxFilterValuesProps): UseCheckboxFilterValuesReturn {
|
||||
const { data, isLoading } = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||
dataSource: filter.dataSource || DataSource.LOGS,
|
||||
aggregateAttribute: filter.aggregateAttribute || '',
|
||||
attributeKey: filter.attributeKey.key,
|
||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||
tagType: filter.attributeKey.type || '',
|
||||
searchText: searchText ?? '',
|
||||
},
|
||||
{
|
||||
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: keyValueSuggestions, isLoading: isLoadingKeyValueSuggestions } =
|
||||
useGetQueryKeyValueSuggestions({
|
||||
key: filter.attributeKey.key,
|
||||
signal: filter.dataSource || DataSource.LOGS,
|
||||
signalSource: 'meter',
|
||||
options: {
|
||||
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const attributeValues: string[] = useMemo(() => {
|
||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||
|
||||
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||
// Process the response data
|
||||
const responseData = keyValueSuggestions?.data as any;
|
||||
const values = responseData.data?.values || {};
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
.filter(
|
||||
(value: string | null | undefined): value is string =>
|
||||
value !== null && value !== undefined && value !== '',
|
||||
);
|
||||
|
||||
// Generate options from number values
|
||||
const numberOptions = numberValues
|
||||
.filter(
|
||||
(value: number | null | undefined): value is number =>
|
||||
value !== null && value !== undefined,
|
||||
)
|
||||
.map((value: number) => value.toString());
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
return [...stringOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
return (data?.payload?.[key] || []).filter(
|
||||
(val) => val !== undefined && val !== null,
|
||||
);
|
||||
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||
|
||||
return {
|
||||
attributeValues,
|
||||
isLoading: isLoading || isLoadingKeyValueSuggestions,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxFilterValues;
|
||||
@@ -3,15 +3,16 @@ package flagger
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
var (
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
@@ -88,6 +89,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableMetricsReduction,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether metrics cardinality reduction (buffer/reduced tables) is read by the querier",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -341,12 +341,12 @@ func alignedMetricWindow(startMs, endMs int64) (
|
||||
}
|
||||
|
||||
tsAdjustedStartMs, _, distributedTSTable, localTSTable := telemetrymetrics.WhichTSTableToUse(
|
||||
samplesAdjustedStartMs, flooredEndMs, nil,
|
||||
samplesAdjustedStartMs, flooredEndMs, false, nil,
|
||||
)
|
||||
|
||||
distributedSamplesTable, localSamplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
samplesAdjustedStartMs, flooredEndMs,
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil,
|
||||
)
|
||||
|
||||
return samplesAdjustedStartMs, flooredEndMs, tsAdjustedStartMs, distributedTSTable, localTSTable, distributedSamplesTable, localSamplesTable
|
||||
|
||||
@@ -141,7 +141,7 @@ func (m *module) listMetrics(ctx context.Context, orgID valuer.UUID, params *met
|
||||
sb.Select("DISTINCT metric_name")
|
||||
|
||||
if params.Start != nil && params.End != nil {
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(*params.Start), uint64(*params.End), nil)
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(*params.Start), uint64(*params.End), false, nil)
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTsTable))
|
||||
sb.Where(sb.Between("unix_milli", start, end))
|
||||
} else {
|
||||
@@ -527,7 +527,7 @@ func (m *module) InspectMetrics(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tsStart, _, tsTable, _ := telemetrymetrics.WhichTSTableToUse(start, end, nil)
|
||||
tsStart, _, tsTable, _ := telemetrymetrics.WhichTSTableToUse(start, end, false, nil)
|
||||
tsSb := sqlbuilder.NewSelectBuilder()
|
||||
tsSb.Select("fingerprint", "labels")
|
||||
tsSb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, tsTable))
|
||||
@@ -971,8 +971,8 @@ func (m *module) fetchMetricsStatsWithSamples(
|
||||
}
|
||||
}
|
||||
|
||||
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
|
||||
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), false, nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil)
|
||||
countExp := telemetrymetrics.CountExpressionForSamplesTable(distributedSamplesTable)
|
||||
|
||||
// Timeseries counts per metric
|
||||
@@ -1100,7 +1100,7 @@ func (m *module) computeTimeseriesTreemap(ctx context.Context, req *metricsexplo
|
||||
}
|
||||
}
|
||||
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), nil)
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), false, nil)
|
||||
|
||||
totalTSBuilder := sqlbuilder.NewSelectBuilder()
|
||||
totalTSBuilder.Select("uniq(fingerprint) AS total_time_series")
|
||||
@@ -1176,8 +1176,8 @@ func (m *module) computeSamplesTreemap(ctx context.Context, req *metricsexplorer
|
||||
}
|
||||
}
|
||||
|
||||
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil)
|
||||
start, end, distributedTsTable, localTsTable := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), false, nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil)
|
||||
countExp := telemetrymetrics.CountExpressionForSamplesTable(distributedSamplesTable)
|
||||
|
||||
candidateLimit := req.Limit + 50
|
||||
|
||||
@@ -91,13 +91,22 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
if a.ComparisonSpaceAggregationParam != nil {
|
||||
spaceAggParamStr = a.ComparisonSpaceAggregationParam.StringValue()
|
||||
}
|
||||
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s:%s",
|
||||
part := fmt.Sprintf("%s:%s:%s:%s:%s",
|
||||
a.MetricName,
|
||||
a.Temporality.StringValue(),
|
||||
a.TimeAggregation.StringValue(),
|
||||
a.SpaceAggregation.StringValue(),
|
||||
spaceAggParamStr,
|
||||
))
|
||||
)
|
||||
if a.Reduced {
|
||||
oneDay := uint64(24 * time.Hour.Milliseconds())
|
||||
route := "reduced"
|
||||
if q.toMS-q.fromMS < oneDay && q.fromMS >= uint64(time.Now().UnixMilli())-oneDay {
|
||||
route = "buffer"
|
||||
}
|
||||
part += ":" + route
|
||||
}
|
||||
aggParts = append(aggParts, part)
|
||||
}
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("aggs=[%s]", strings.Join(aggParts, ",")))
|
||||
|
||||
@@ -92,6 +92,11 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
NumberOfQueries: len(req.CompositeQuery.Queries),
|
||||
@@ -111,7 +116,10 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
// We need to set if it is unspecified or adjust it if value is not within recommended range
|
||||
intervalWarnings := q.adjustStepInterval(req.CompositeQuery.Queries, req.Start, req.End)
|
||||
|
||||
missingMetricQueries, metricWarnings, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
missingMetricQueries, metricWarnings, err := q.resolveMetricMetadata(ctx, orgID, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,64 +128,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
missingMetricQuerySet[name] = true
|
||||
}
|
||||
|
||||
queries, steps, err := q.buildQueries(req, dependencyQueries, missingMetricQuerySet, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) buildQueries(
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
dependencyQueries map[string]bool,
|
||||
missingMetricQuerySet map[string]bool,
|
||||
event *qbtypes.QBEvent,
|
||||
) (map[string]qbtypes.Query, map[string]qbtypes.Step, error) {
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
queryName := query.GetQueryName()
|
||||
|
||||
@@ -190,7 +140,7 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypePromQL:
|
||||
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
}
|
||||
promqlQuery := newPromqlQuery(q.logger, q.promEngine, promQuery, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[promQuery.Name] = promqlQuery
|
||||
@@ -198,14 +148,14 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
chQuery, ok := query.Spec.(qbtypes.ClickHouseQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
}
|
||||
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[chQuery.Name] = chSQLQuery
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
traceOpQuery, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
}
|
||||
toq := &traceOperatorQuery{
|
||||
telemetryStore: q.telemetryStore,
|
||||
@@ -258,12 +208,46 @@ func (q *querier) buildQueries(
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
default:
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queries, steps, nil
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.QueryEnvelope) {
|
||||
@@ -320,7 +304,7 @@ func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.Quer
|
||||
// resolved: never-seen metrics and dormant metrics (seen but no data in
|
||||
// the query window).
|
||||
// - err: Internal when a metadata fetch fails.
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, metricWarnings []string, err error) {
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, metricWarnings []string, err error) {
|
||||
metricNames := make([]string, 0)
|
||||
for idx := range queries {
|
||||
if queries[idx].Type != qbtypes.QueryTypeBuilder {
|
||||
@@ -341,7 +325,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
metricTemporality, metricTypes, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, start, end, metricNames...)
|
||||
metricTemporality, metricTypes, reducedMetricsSet, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, orgID, start, end, metricNames...)
|
||||
if err != nil {
|
||||
q.logger.WarnContext(ctx, "failed to fetch metric temporality", errors.Attr(err), slog.Any("metrics", metricNames))
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
|
||||
@@ -378,6 +362,9 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
if err := spec.Aggregations[i].ValidateForType(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if reducedMetricsSet[spec.Aggregations[i].MetricName] {
|
||||
spec.Aggregations[i].Reduced = true
|
||||
}
|
||||
presentAggregations = append(presentAggregations, spec.Aggregations[i])
|
||||
}
|
||||
if len(presentAggregations) == 0 {
|
||||
|
||||
@@ -2136,12 +2136,12 @@ func (t *telemetryMetaStore) GetAllValues(ctx context.Context, fieldValueSelecto
|
||||
return values, complete, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
if metricName == "" {
|
||||
return metrictypes.Unknown, errors.Newf(errors.TypeInternal, errors.CodeInternal, "metric name cannot be empty")
|
||||
}
|
||||
|
||||
temporalityMap, err := t.FetchTemporalityMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricName)
|
||||
temporalityMap, err := t.FetchTemporalityMulti(ctx, orgID, queryTimeRangeStartTs, queryTimeRangeEndTs, metricName)
|
||||
if err != nil {
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
@@ -2154,25 +2154,27 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, queryTimeRang
|
||||
return temporality, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
temporalities, _, err := t.FetchTemporalityAndTypeMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
temporalities, _, _, err := t.FetchTemporalityAndTypeMulti(ctx, orgID, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
return temporalities, err
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, map[string]bool, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), nil
|
||||
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), make(map[string]bool), nil
|
||||
}
|
||||
|
||||
reductionEnabled := t.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
|
||||
temporalities := make(map[string]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
metricsTemporality, metricTypes, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
metricsTemporality, metricTypes, reduced, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, reductionEnabled, metricNames...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
meterMetricsTemporality, meterMetricsTypes, err := t.fetchMeterSourceMetricsTemporalityAndType(ctx, metricNames...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// For metrics not found in the database, set to Unknown
|
||||
@@ -2197,10 +2199,10 @@ func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, q
|
||||
}
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return temporalities, types, reduced, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, reductionEnabled bool, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, map[string]bool, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalMetrics.StringValue(),
|
||||
instrumentationtypes.CodeNamespace: "metadata",
|
||||
@@ -2208,48 +2210,58 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
|
||||
})
|
||||
temporalities := make(map[string][]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
reduced := make(map[string]bool)
|
||||
|
||||
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, nil)
|
||||
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, false, nil)
|
||||
|
||||
// Build query to fetch temporality for all metrics
|
||||
// We use attr_string_value where attr_name = '__temporality__'
|
||||
// Note: The columns are mixed in the current data - temporality column contains metric_name
|
||||
// and metric_name column contains temporality value, so we use the correct mapping
|
||||
sb := sqlbuilder.Select(
|
||||
"metric_name",
|
||||
"temporality",
|
||||
"any(type) AS type",
|
||||
"any(is_monotonic) as is_monotonic",
|
||||
).
|
||||
From(t.metricsDBName + "." + tsTableName)
|
||||
cols := []string{"metric_name", "temporality", "any(type) AS type", "any(is_monotonic) as is_monotonic"}
|
||||
|
||||
// Filter by metric names (in the temporality column due to data mix-up)
|
||||
// When reduction is enabled, fold the reduced-catalog presence check into the
|
||||
// same query so a metric's reduced status comes back in one round trip.
|
||||
var reducedArgs []any
|
||||
if reductionEnabled {
|
||||
rs := sqlbuilder.NewSelectBuilder()
|
||||
rs.Select("metric_name")
|
||||
rs.From(t.metricsDBName + "." + telemetrymetrics.TimeseriesV4ReducedTableName)
|
||||
rs.Where(rs.In("metric_name", metricNames), rs.GTE("unix_milli", adjustedStartTs), rs.LT("unix_milli", adjustedEndTs))
|
||||
rs.GroupBy("metric_name")
|
||||
rsQuery, rsArgs := rs.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
cols = append(cols, fmt.Sprintf("metric_name GLOBAL IN (%s) AS reduced", rsQuery))
|
||||
reducedArgs = rsArgs
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(cols...)
|
||||
sb.From(t.metricsDBName + "." + tsTableName)
|
||||
sb.Where(
|
||||
sb.In("metric_name", metricNames),
|
||||
sb.GTE("unix_milli", adjustedStartTs),
|
||||
sb.LT("unix_milli", adjustedEndTs),
|
||||
)
|
||||
|
||||
sb.GroupBy("metric_name", "temporality")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, reducedArgs...)
|
||||
|
||||
t.logger.DebugContext(ctx, "fetching metric temporality", slog.String("query", query), slog.Any("args", args))
|
||||
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
|
||||
return nil, nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process results
|
||||
for rows.Next() {
|
||||
var metricName string
|
||||
var temporality metrictypes.Temporality
|
||||
var metricType metrictypes.Type
|
||||
var isMonotonic bool
|
||||
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
var isReduced uint8
|
||||
dest := []any{&metricName, &temporality, &metricType, &isMonotonic}
|
||||
if reductionEnabled {
|
||||
dest = append(dest, &isReduced)
|
||||
}
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
return nil, nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
}
|
||||
if temporality != metrictypes.Unknown {
|
||||
temporalities[metricName] = append(temporalities[metricName], temporality)
|
||||
@@ -2258,12 +2270,15 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
|
||||
metricType = metrictypes.GaugeType
|
||||
}
|
||||
types[metricName] = metricType
|
||||
if isReduced != 0 {
|
||||
reduced[metricName] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
return nil, nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return temporalities, types, reduced, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
|
||||
157
pkg/telemetrymetrics/reduced_test.go
Normal file
157
pkg/telemetrymetrics/reduced_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package telemetrymetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func reducedQuery(metric string, ty metrictypes.Type, temp metrictypes.Temporality, ta metrictypes.TimeAggregation, sa metrictypes.SpaceAggregation) qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation] {
|
||||
return qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
StepInterval: qbtypes.Step{Duration: 5 * time.Minute},
|
||||
Aggregations: []qbtypes.MetricAggregation{{
|
||||
MetricName: metric,
|
||||
Type: ty,
|
||||
Temporality: temp,
|
||||
TimeAggregation: ta,
|
||||
SpaceAggregation: sa,
|
||||
Reduced: true,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestReducedStatementBuilder(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]
|
||||
expected qbtypes.Statement
|
||||
}{
|
||||
{
|
||||
name: "gauge_sum_latest",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationLatest, metrictypes.SpaceAggregationSum),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, anyLast(last) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, argMax(value, unix_milli) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_avg_avg",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationAvg, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_min_min",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationMin, metrictypes.SpaceAggregationMin),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(min) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, min(value) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`min`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gauge_max_max",
|
||||
query: reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationMax, metrictypes.SpaceAggregationMax),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(value) AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`max`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_sum_rate",
|
||||
query: reducedQuery("test.metric", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationRate, metrictypes.SpaceAggregationSum),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) / 300 AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0, "test.metric", uint64(1746999600000), uint64(1747172760000), "test.metric", uint64(1746999600000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_avg_increase",
|
||||
query: reducedQuery("test.metric", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationIncrease, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value, per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0, "test.metric", uint64(1746999600000), uint64(1747172760000), "test.metric", uint64(1746999600000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_min_omitted",
|
||||
query: reducedQuery("test.metric", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationRate, metrictypes.SpaceAggregationMin),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, min(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "counter_max_omitted",
|
||||
query: reducedQuery("test.metric", metrictypes.SumType, metrictypes.Cumulative, metrictypes.TimeAggregationRate, metrictypes.SpaceAggregationMax),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, max(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999600000), uint64(1747172760000), 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "histogram_p99",
|
||||
query: reducedQuery("test.metric", metrictypes.HistogramType, metrictypes.Cumulative, metrictypes.TimeAggregationUnspecified, metrictypes.SpaceAggregationPercentile99),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT ts, `le`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, max(max) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `le` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, `le`, sum(value) / 300 AS per_series_value FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum`, computed_at) AS value FROM signoz_metrics.distributed_samples_v4_reduced_sum_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'le') AS `le` FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint, `le`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts, `le`), __spatial_aggregation_cte AS (SELECT ts, `le`, sum(per_series_value) AS value FROM __temporal_aggregation_cte GROUP BY ts, `le`) SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) AS value FROM __spatial_aggregation_cte GROUP BY ts ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "cumulative", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "summary_avg",
|
||||
query: reducedQuery("test.metric", metrictypes.SummaryType, metrictypes.Unspecified, metrictypes.TimeAggregationAvg, metrictypes.SpaceAggregationAvg),
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, sum(sum) / sum(count) AS per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts ORDER BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, avg(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) UNION ALL SELECT * FROM (WITH __temporal_aggregation_cte AS (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) AS ts, avg(value) AS per_series_value, avg(weight) AS per_series_weight FROM (SELECT reduced_fingerprint AS fingerprint, unix_milli, argMax(`sum_last`, computed_at) AS value, argMax(`count_series`, computed_at) AS weight FROM signoz_metrics.distributed_samples_v4_reduced_last_60s WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY reduced_fingerprint, unix_milli) AS points INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v4_reduced WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND __normalized = ? GROUP BY fingerprint) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint GROUP BY fingerprint, ts), __spatial_aggregation_cte AS (SELECT ts, sum(per_series_value) / sum(per_series_weight) AS value FROM __temporal_aggregation_cte GROUP BY ts) SELECT * FROM __spatial_aggregation_cte ORDER BY ts) ORDER BY ts",
|
||||
Args: []any{"test.metric", uint64(1746921600000), uint64(1747172760000), "unspecified", false, "test.metric", uint64(1746999900000), uint64(1747172760000), 0, "test.metric", uint64(1746999900000), uint64(1747172760000), "test.metric", uint64(1746999900000), uint64(1747172760000), false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
cb := NewConditionBuilder(fm)
|
||||
fl, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
|
||||
require.NoError(t, err)
|
||||
sb := NewMetricQueryStatementBuilder(instrumentationtest.New().ToProviderSettings(), telemetrytypestest.NewMockMetadataStore(), fm, cb, fl)
|
||||
|
||||
const start, end = uint64(1747000000000), uint64(1747172800000)
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, err := sb.Build(context.Background(), start, end, qbtypes.RequestTypeTimeSeries, c.query, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected.Query, got.Query)
|
||||
require.Equal(t, c.expected.Args, got.Args)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("buffer_recent_window", func(t *testing.T) {
|
||||
now := time.Now().UnixMilli()
|
||||
q := reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationLatest, metrictypes.SpaceAggregationSum)
|
||||
got, err := sb.Build(context.Background(), uint64(now-2*time.Hour.Milliseconds()), uint64(now), qbtypes.RequestTypeTimeSeries, q, nil)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, got.Query, "signoz_metrics.distributed_samples_v4_buffer")
|
||||
require.Contains(t, got.Query, "signoz_metrics.time_series_v4_buffer")
|
||||
require.Contains(t, got.Query, "is_reduced")
|
||||
require.NotContains(t, got.Query, "UNION ALL")
|
||||
})
|
||||
|
||||
t.Run("not_reduced", func(t *testing.T) {
|
||||
q := reducedQuery("test.metric", metrictypes.GaugeType, metrictypes.Unspecified, metrictypes.TimeAggregationLatest, metrictypes.SpaceAggregationSum)
|
||||
q.Aggregations[0].Reduced = false
|
||||
got, err := sb.Build(context.Background(), start, end, qbtypes.RequestTypeTimeSeries, q, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, got.Query, "UNION ALL")
|
||||
require.NotContains(t, got.Query, "reduced")
|
||||
require.NotContains(t, got.Query, "buffer")
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
@@ -180,19 +181,30 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
query.Aggregations[0].SpaceAggregation = metrictypes.SpaceAggregationSum
|
||||
}
|
||||
|
||||
agg := query.Aggregations[0]
|
||||
|
||||
// A reduced metric reads the raw buffer for recent short windows, and
|
||||
// samples_v4/agg (unioned with the reduced tables) otherwise. The buffer is
|
||||
// shaped exactly like samples_v4 / time_series_v4, so once the table names are
|
||||
// chosen the rest of the pipeline is unchanged.
|
||||
useBuffer := agg.Reduced &&
|
||||
end-start < oneDayInMilliseconds &&
|
||||
start >= uint64(time.Now().UnixMilli())-oneDayInMilliseconds
|
||||
|
||||
samplesTable, _ := WhichSamplesTableToUse(start, end, agg.Type, agg.TimeAggregation, useBuffer, agg.TableHints)
|
||||
tsStart, tsEnd, _, tsTable := WhichTSTableToUse(start, end, useBuffer, agg.TableHints)
|
||||
|
||||
var timeSeriesCTE string
|
||||
var timeSeriesCTEArgs []any
|
||||
var err error
|
||||
|
||||
// time_series_cte
|
||||
// this is applicable for all the queries
|
||||
if timeSeriesCTE, timeSeriesCTEArgs, err = b.buildTimeSeriesCTE(ctx, start, end, query, keys, variables); err != nil {
|
||||
if timeSeriesCTE, timeSeriesCTEArgs, err = b.buildTimeSeriesCTE(ctx, tsStart, tsEnd, query, keys, variables, tsTable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if qbtypes.CanShortCircuitDelta(query.Aggregations[0]) {
|
||||
// spatial_aggregation_cte directly for certain delta queries
|
||||
if frag, args, err := b.buildTemporalAggDeltaFastPath(start, end, query, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
if frag, args, err := b.buildTemporalAggDeltaFastPath(start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
return nil, err
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
@@ -200,7 +212,7 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
}
|
||||
} else {
|
||||
// temporal_aggregation_cte
|
||||
if frag, args, err := b.buildTemporalAggregationCTE(ctx, start, end, query, keys, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
if frag, args, err := b.buildTemporalAggregationCTE(ctx, start, end, query, keys, samplesTable, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
return nil, err
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
@@ -214,18 +226,188 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
}
|
||||
}
|
||||
|
||||
var reducedFragments []string
|
||||
var reducedArgs [][]any
|
||||
if agg.Reduced && !useBuffer {
|
||||
var tsCTE string
|
||||
var tsArgs []any
|
||||
if tsCTE, tsArgs, err = b.buildReducedTimeSeriesCTE(ctx, start, end, query, keys, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if temporalFrag, temporalArgs, ok := b.buildReducedTemporalAggregationCTE(start, end, query, tsCTE, tsArgs); ok {
|
||||
spatialFrag, spatialArgs := b.buildReducedSpatialAggregationCTE(query)
|
||||
reducedFragments = []string{temporalFrag, spatialFrag}
|
||||
reducedArgs = [][]any{temporalArgs, spatialArgs}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the query to the original state
|
||||
query.Aggregations[0].SpaceAggregation = origSpaceAgg
|
||||
query.Aggregations[0].TimeAggregation = origTimeAgg
|
||||
query.GroupBy = origGroupBy
|
||||
|
||||
// final SELECT
|
||||
return b.BuildFinalSelect(cteFragments, cteArgs, query)
|
||||
mainStmt, err := b.BuildFinalSelect(cteFragments, cteArgs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reducedFragments == nil {
|
||||
return mainStmt, nil
|
||||
}
|
||||
reducedStmt, err := b.BuildFinalSelect(reducedFragments, reducedArgs, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unionStatements(mainStmt, reducedStmt, query)
|
||||
}
|
||||
|
||||
func unionStatements(main, reduced *qbtypes.Statement, query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) (*qbtypes.Statement, error) {
|
||||
orderBy := "ts"
|
||||
for _, g := range query.GroupBy {
|
||||
orderBy = fmt.Sprintf("`%s`, ", g.Name) + orderBy
|
||||
}
|
||||
q := fmt.Sprintf("SELECT * FROM (%s) UNION ALL SELECT * FROM (%s) ORDER BY %s", main.Query, reduced.Query, orderBy)
|
||||
args := append(append([]any{}, main.Args...), reduced.Args...)
|
||||
warnings := append(append([]string{}, main.Warnings...), reduced.Warnings...)
|
||||
return &qbtypes.Statement{Query: q, Args: args, Warnings: warnings}, nil
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildReducedTimeSeriesCTE(
|
||||
ctx context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
Logger: b.logger,
|
||||
FieldMapper: b.fm,
|
||||
ConditionBuilder: b.cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, TimeseriesV4ReducedTableName))
|
||||
sb.Select("fingerprint")
|
||||
for _, g := range query.GroupBy {
|
||||
col, err := b.fm.ColumnExpressionFor(ctx, start, end, &g.TelemetryFieldKey, keys)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
sb.SelectMore(col)
|
||||
}
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
sb.GTE("unix_milli", start),
|
||||
sb.LTE("unix_milli", end),
|
||||
sb.EQ("__normalized", false),
|
||||
)
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
sb.GroupBy("fingerprint")
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
|
||||
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return fmt.Sprintf("(%s) AS filtered_time_series", q), args, nil
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildReducedTemporalAggregationCTE(
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, bool) {
|
||||
agg := query.Aggregations[0]
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
|
||||
value, weight, ok := ReducedValueColumn(agg.Type, agg.SpaceAggregation)
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// dedup recomputed buckets: latest computed_at wins per (series, 60s bucket)
|
||||
dedup := sqlbuilder.NewSelectBuilder()
|
||||
dedup.Select("reduced_fingerprint AS fingerprint", "unix_milli")
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, computed_at) AS value", value))
|
||||
if weight != "" {
|
||||
dedup.SelectMore(fmt.Sprintf("argMax(%s, computed_at) AS weight", weight))
|
||||
}
|
||||
dedup.From(fmt.Sprintf("%s.%s", DBName, WhichReducedSamplesTableToUse(agg.Type)))
|
||||
dedup.Where(
|
||||
dedup.In("metric_name", agg.MetricName),
|
||||
dedup.GTE("unix_milli", start),
|
||||
dedup.LT("unix_milli", end),
|
||||
)
|
||||
dedup.GroupBy("reduced_fingerprint", "unix_milli")
|
||||
dedupQuery, dedupArgs := dedup.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("fingerprint")
|
||||
sb.SelectMore(fmt.Sprintf("toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(%d)) AS ts", stepSec))
|
||||
for _, g := range query.GroupBy {
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
sb.SelectMore(fmt.Sprintf("%s AS per_series_value", ReducedTimeAggregationColumn(agg.TimeAggregation, stepSec)))
|
||||
if weight != "" {
|
||||
// count_series is a series count, not additive over time, so the avg
|
||||
// denominator is reduced with avg
|
||||
sb.SelectMore("avg(weight) AS per_series_weight")
|
||||
}
|
||||
sb.From(fmt.Sprintf("(%s) AS points", dedupQuery))
|
||||
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.GroupBy("fingerprint", "ts")
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
|
||||
initArgs := append(append([]any{}, dedupArgs...), timeSeriesCTEArgs...)
|
||||
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, initArgs...)
|
||||
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", q), args, true
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildReducedSpatialAggregationCTE(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
) (string, []any) {
|
||||
spatial := "sum(per_series_value)"
|
||||
switch query.Aggregations[0].SpaceAggregation {
|
||||
case metrictypes.SpaceAggregationAvg:
|
||||
spatial = "sum(per_series_value) / sum(per_series_weight)"
|
||||
case metrictypes.SpaceAggregationMin:
|
||||
spatial = "min(per_series_value)"
|
||||
case metrictypes.SpaceAggregationMax:
|
||||
spatial = "max(per_series_value)"
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("ts")
|
||||
for _, g := range query.GroupBy {
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
sb.SelectMore(spatial + " AS value")
|
||||
sb.From("__temporal_aggregation_cte")
|
||||
sb.GroupBy("ts")
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
|
||||
q, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return fmt.Sprintf("__spatial_aggregation_cte AS (%s)", q), args
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -242,8 +424,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(
|
||||
start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality,
|
||||
query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints,
|
||||
samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation,
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
@@ -260,8 +441,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
|
||||
sb.SelectMore(fmt.Sprintf("%s AS value", aggCol))
|
||||
|
||||
tbl, _ := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -281,6 +461,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
tsTable string,
|
||||
) (string, []any, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
@@ -304,8 +485,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
}
|
||||
}
|
||||
|
||||
start, end, _, tbl := WhichTSTableToUse(start, end, query.Aggregations[0].TableHints)
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, tbl))
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, tsTable))
|
||||
|
||||
sb.Select("fingerprint")
|
||||
for _, g := range query.GroupBy {
|
||||
@@ -331,6 +511,12 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
sb.EQ("__normalized", false),
|
||||
)
|
||||
|
||||
// the buffer holds both raw rows and the reduced catalog rows; the raw read
|
||||
// only wants the original series
|
||||
if tsTable == TimeseriesV4BufferLocalTableName {
|
||||
sb.Where(sb.EQ("is_reduced", false))
|
||||
}
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
@@ -347,21 +533,23 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggregationCTE(
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
_ map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
if query.Aggregations[0].Temporality == metrictypes.Delta {
|
||||
return b.buildTemporalAggDelta(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggDelta(ctx, start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
} else if query.Aggregations[0].Temporality != metrictypes.Multiple {
|
||||
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
}
|
||||
return b.buildTemporalAggForMultipleTemporalities(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggForMultipleTemporalities(ctx, start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
|
||||
_ context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -378,7 +566,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
aggCol, err := AggregationColumnForSamplesTable(samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -389,8 +577,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
|
||||
|
||||
sb.SelectMore(fmt.Sprintf("%s AS per_series_value", aggCol))
|
||||
|
||||
tbl, _ := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -409,6 +596,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
_ context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -424,14 +612,13 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
baseSb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
aggCol, err := AggregationColumnForSamplesTable(samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
baseSb.SelectMore(fmt.Sprintf("%s AS per_series_value", aggCol))
|
||||
|
||||
tbl, _ := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
baseSb.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
|
||||
baseSb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
baseSb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
baseSb.Where(
|
||||
baseSb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -475,6 +662,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
_ context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -489,11 +677,11 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggForDeltaTemporality, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, metrictypes.Delta, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
aggForDeltaTemporality, err := AggregationColumnForSamplesTable(samplesTable, metrictypes.Delta, query.Aggregations[0].TimeAggregation)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
aggForCumulativeTemporality, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, metrictypes.Cumulative, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
aggForCumulativeTemporality, err := AggregationColumnForSamplesTable(samplesTable, metrictypes.Cumulative, query.Aggregations[0].TimeAggregation)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -521,8 +709,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
sb.SelectMore(expr)
|
||||
}
|
||||
|
||||
tbl, _ := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
|
||||
@@ -30,6 +30,17 @@ const (
|
||||
TimeseriesV41weekLocalTableName = "time_series_v4_1week"
|
||||
AttributesMetadataTableName = "distributed_metadata"
|
||||
AttributesMetadataLocalTableName = "metadata"
|
||||
|
||||
// The buffer holds raw points for ~24h; the reduced tables hold 60s
|
||||
// aggregates of dropped-label series.
|
||||
SamplesV4BufferTableName = "distributed_samples_v4_buffer"
|
||||
SamplesV4BufferLocalTableName = "samples_v4_buffer"
|
||||
TimeseriesV4BufferTableName = "distributed_time_series_v4_buffer"
|
||||
TimeseriesV4BufferLocalTableName = "time_series_v4_buffer"
|
||||
SamplesV4ReducedLastTableName = "distributed_samples_v4_reduced_last_60s"
|
||||
SamplesV4ReducedSumTableName = "distributed_samples_v4_reduced_sum_60s"
|
||||
TimeseriesV4ReducedTableName = "distributed_time_series_v4_reduced"
|
||||
TimeseriesV4ReducedLocalTableName = "time_series_v4_reduced"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,8 +60,16 @@ var (
|
||||
// in that order.
|
||||
func WhichTSTableToUse(
|
||||
start, end uint64,
|
||||
useBuffer bool,
|
||||
tableHints *metrictypes.MetricTableHints,
|
||||
) (uint64, uint64, string, string) {
|
||||
// the buffer holds the recent raw window for reduced metrics and has the same
|
||||
// shape as time_series_v4; round the start to the hour like the v4 table.
|
||||
if useBuffer {
|
||||
start = start - (start % (oneHourInMilliseconds))
|
||||
return start, end, TimeseriesV4BufferTableName, TimeseriesV4BufferLocalTableName
|
||||
}
|
||||
|
||||
// if we have a hint for the table, we need to use it
|
||||
// the hint will be used to override the default table selection logic
|
||||
if tableHints != nil {
|
||||
@@ -149,14 +168,20 @@ func WhichSamplesTableToUse(
|
||||
start, end uint64,
|
||||
metricType metrictypes.Type,
|
||||
timeAggregation metrictypes.TimeAggregation,
|
||||
useBuffer bool,
|
||||
tableHints *metrictypes.MetricTableHints,
|
||||
) (string, string) {
|
||||
// the buffer holds the recent raw window for reduced metrics; same shape as samples_v4
|
||||
if useBuffer {
|
||||
return SamplesV4BufferTableName, SamplesV4BufferLocalTableName
|
||||
}
|
||||
|
||||
// if we have a hint for the table, we need to use it
|
||||
// the hint will be used to override the default table selection logic.
|
||||
// SamplesTableName is the distributed name; derive the local via switch.
|
||||
if tableHints != nil && tableHints.SamplesTableName != "" {
|
||||
switch tableHints.SamplesTableName {
|
||||
case SamplesV4TableName:
|
||||
case SamplesV4TableName, SamplesV4BufferTableName:
|
||||
return SamplesV4TableName, SamplesV4LocalTableName
|
||||
case SamplesV4Agg5mTableName:
|
||||
return SamplesV4Agg5mTableName, SamplesV4Agg5mLocalTableName
|
||||
@@ -188,13 +213,10 @@ func WhichSamplesTableToUse(
|
||||
}
|
||||
|
||||
func AggregationColumnForSamplesTable(
|
||||
start, end uint64,
|
||||
metricType metrictypes.Type,
|
||||
tableName string,
|
||||
temporality metrictypes.Temporality,
|
||||
timeAggregation metrictypes.TimeAggregation,
|
||||
tableHints *metrictypes.MetricTableHints,
|
||||
) (string, error) {
|
||||
tableName, _ := WhichSamplesTableToUse(start, end, metricType, timeAggregation, tableHints)
|
||||
var aggregationColumn string
|
||||
switch temporality {
|
||||
case metrictypes.Delta:
|
||||
@@ -202,7 +224,7 @@ func AggregationColumnForSamplesTable(
|
||||
// although it doesn't make sense to use anyLast, avg, min, max, count on delta metrics,
|
||||
// we are keeping it here to make sure that query will not be invalid
|
||||
switch tableName {
|
||||
case SamplesV4TableName:
|
||||
case SamplesV4TableName, SamplesV4BufferTableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -244,7 +266,7 @@ func AggregationColumnForSamplesTable(
|
||||
// for cumulative metrics, we only support `RATE`/`INCREASE`. The max value in window is
|
||||
// used to calculate the sum which is then divided by the window size to get the rate
|
||||
switch tableName {
|
||||
case SamplesV4TableName:
|
||||
case SamplesV4TableName, SamplesV4BufferTableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -284,7 +306,7 @@ func AggregationColumnForSamplesTable(
|
||||
}
|
||||
case metrictypes.Unspecified:
|
||||
switch tableName {
|
||||
case SamplesV4TableName:
|
||||
case SamplesV4TableName, SamplesV4BufferTableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -332,6 +354,65 @@ func AggregationColumnForSamplesTable(
|
||||
return aggregationColumn, nil
|
||||
}
|
||||
|
||||
// WhichReducedSamplesTableToUse returns the 60s reduced samples table for a metric
|
||||
// type: the last_60s table for gauge-like series, the sum_60s table for counters
|
||||
// and histograms.
|
||||
func WhichReducedSamplesTableToUse(metricType metrictypes.Type) string {
|
||||
if metricType == metrictypes.SumType || metricType == metrictypes.HistogramType {
|
||||
return SamplesV4ReducedSumTableName
|
||||
}
|
||||
return SamplesV4ReducedLastTableName
|
||||
}
|
||||
|
||||
// ReducedValueColumn returns the reduced value column (and the avg-denominator
|
||||
// weight) for a space aggregation. The reduced columns are pre-aggregated across
|
||||
// the original series, so the space aggregation picks the underlying value; the
|
||||
// sum table only has `sum`, so min/max across series have no column (ok=false).
|
||||
func ReducedValueColumn(metricType metrictypes.Type, space metrictypes.SpaceAggregation) (value, weight string, ok bool) {
|
||||
if metricType == metrictypes.SumType || metricType == metrictypes.HistogramType {
|
||||
switch space {
|
||||
case metrictypes.SpaceAggregationSum:
|
||||
return "`sum`", "", true
|
||||
case metrictypes.SpaceAggregationAvg:
|
||||
return "`sum`", "`count_series`", true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
switch space {
|
||||
case metrictypes.SpaceAggregationSum:
|
||||
return "`sum_last`", "", true
|
||||
case metrictypes.SpaceAggregationAvg:
|
||||
return "`sum_last`", "`count_series`", true
|
||||
case metrictypes.SpaceAggregationMin:
|
||||
return "`min`", "", true
|
||||
case metrictypes.SpaceAggregationMax:
|
||||
return "`max`", "", true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// ReducedTimeAggregationColumn applies the time aggregation to the reduced `value`
|
||||
// column over the step's 60s buckets. latest uses argMax over the bucket timestamp
|
||||
// (the buckets have no read order); rate divides the per-step sum by the step.
|
||||
func ReducedTimeAggregationColumn(timeAggregation metrictypes.TimeAggregation, stepSec int64) string {
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
return "argMax(value, unix_milli)"
|
||||
case metrictypes.TimeAggregationAvg:
|
||||
return "avg(value)"
|
||||
case metrictypes.TimeAggregationMin:
|
||||
return "min(value)"
|
||||
case metrictypes.TimeAggregationMax:
|
||||
return "max(value)"
|
||||
case metrictypes.TimeAggregationCount:
|
||||
return "count(value)"
|
||||
case metrictypes.TimeAggregationRate:
|
||||
return fmt.Sprintf("sum(value) / %d", stepSec)
|
||||
default: // sum, increase
|
||||
return "sum(value)"
|
||||
}
|
||||
}
|
||||
|
||||
func AggregationQueryForHistogramCountWithParams(param *metrictypes.ComparisonSpaceAggregationParam) (string, error) {
|
||||
if param == nil {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "no aggregation param provided for histogram count")
|
||||
|
||||
@@ -480,7 +480,9 @@ type MetricAggregation struct {
|
||||
// value filter to apply to the query
|
||||
ValueFilter *metrictypes.MetricValueFilter `json:"-"`
|
||||
// reduce to operator for metric scalar requests
|
||||
ReduceTo ReduceTo `json:"reduceTo,omitempty"`
|
||||
ReduceTo ReduceTo `json:"reduceTo,omitzero"`
|
||||
|
||||
Reduced bool `json:"-"`
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of MetricAggregation.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MetadataStore is the interface for the telemetry metadata store.
|
||||
@@ -26,12 +27,12 @@ type MetadataStore interface {
|
||||
GetAllValues(ctx context.Context, fieldValueSelector *FieldValueSelector) (*TelemetryFieldValues, bool, error)
|
||||
|
||||
// FetchTemporality fetches the temporality for metric
|
||||
FetchTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error)
|
||||
FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error)
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics
|
||||
FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error)
|
||||
FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error)
|
||||
|
||||
FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error)
|
||||
FetchTemporalityAndTypeMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, map[string]bool, error)
|
||||
|
||||
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
|
||||
ListLogsJSONIndexes(ctx context.Context, filters ...string) ([]TelemetryFieldKeySkipIndex, error)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MockMetadataStore implements the MetadataStore interface for testing purposes.
|
||||
@@ -16,6 +17,7 @@ type MockMetadataStore struct {
|
||||
AllValuesMap map[string]*telemetrytypes.TelemetryFieldValues
|
||||
TemporalityMap map[string]metrictypes.Temporality
|
||||
TypeMap map[string]metrictypes.Type
|
||||
ReducedMap map[string]bool
|
||||
PromotedPathsMap map[string]bool
|
||||
LogsJSONIndexes []telemetrytypes.TelemetryFieldKeySkipIndex
|
||||
ColumnEvolutionMetadataMap map[string][]*telemetrytypes.EvolutionEntry
|
||||
@@ -306,7 +308,7 @@ func (m *MockMetadataStore) SetAllValues(lookupKey string, values *telemetrytype
|
||||
}
|
||||
|
||||
// FetchTemporality fetches the temporality for a metric.
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
if temporality, exists := m.TemporalityMap[metricName]; exists {
|
||||
return temporality, nil
|
||||
}
|
||||
@@ -314,7 +316,7 @@ func (m *MockMetadataStore) FetchTemporality(ctx context.Context, queryTimeRange
|
||||
}
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics.
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
@@ -329,9 +331,10 @@ func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, queryTime
|
||||
}
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics.
|
||||
func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, map[string]bool, error) {
|
||||
temporalities := make(map[string]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
reduced := make(map[string]bool)
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
if temporality, exists := m.TemporalityMap[metricName]; exists {
|
||||
@@ -344,9 +347,12 @@ func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, qu
|
||||
} else {
|
||||
types[metricName] = metrictypes.UnspecifiedType
|
||||
}
|
||||
if m.ReducedMap[metricName] {
|
||||
reduced[metricName] = true
|
||||
}
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return temporalities, types, reduced, nil
|
||||
}
|
||||
|
||||
// SetTemporality sets the temporality for a metric in the mock store.
|
||||
|
||||
Reference in New Issue
Block a user