mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-22 00:00:27 +01:00
Compare commits
2 Commits
issue-5341
...
feat/test_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a50f46387 | ||
|
|
0e4f5d1c4e |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.129.0
|
||||
image: signoz/signoz:v0.128.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.129.0
|
||||
image: signoz/signoz:v0.128.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.129.0}
|
||||
image: signoz/signoz:${VERSION:-v0.128.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.129.0}
|
||||
image: signoz/signoz:${VERSION:-v0.128.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -7240,6 +7240,41 @@ components:
|
||||
- operation
|
||||
- priority
|
||||
type: object
|
||||
SpantypesSpanMappingPreviewGroup:
|
||||
properties:
|
||||
group:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperGroup'
|
||||
mappers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- group
|
||||
- mappers
|
||||
type: object
|
||||
SpantypesSpanMappingPreviewRequest:
|
||||
properties:
|
||||
groupId:
|
||||
nullable: true
|
||||
type: string
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMappingPreviewGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
span:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesSpanMappingPreviewResponse:
|
||||
properties:
|
||||
span:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -12797,6 +12832,69 @@ paths:
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/preview:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Previews how attribute mappings would transform a sample span.
|
||||
operationId: PreviewSpanMapping
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesSpanMappingPreviewRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesSpanMappingPreviewResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Preview span attribute mapping against a sample span
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/stats:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -8492,6 +8492,56 @@ export interface SpantypesSpanMapperDTO {
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMappingPreviewGroupDTO {
|
||||
group: SpantypesPostableSpanMapperGroupDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
mappers: SpantypesPostableSpanMapperDTO[] | null;
|
||||
}
|
||||
|
||||
export type SpantypesSpanMappingPreviewRequestDTOSpanAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMappingPreviewRequestDTOSpan =
|
||||
SpantypesSpanMappingPreviewRequestDTOSpanAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMappingPreviewRequestDTO {
|
||||
/**
|
||||
* @type string,null
|
||||
*/
|
||||
groupId?: string | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
groups?: SpantypesSpanMappingPreviewGroupDTO[] | null;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
span?: SpantypesSpanMappingPreviewRequestDTOSpan;
|
||||
}
|
||||
|
||||
export type SpantypesSpanMappingPreviewResponseDTOSpanAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMappingPreviewResponseDTOSpan =
|
||||
SpantypesSpanMappingPreviewResponseDTOSpanAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMappingPreviewResponseDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
span?: SpantypesSpanMappingPreviewResponseDTOSpan;
|
||||
}
|
||||
|
||||
export interface SpantypesUpdatableSpanMapperDTO {
|
||||
config?: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -9798,6 +9848,14 @@ export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type PreviewSpanMapping200 = {
|
||||
data: SpantypesSpanMappingPreviewResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetStats200Data = { [key: string]: unknown };
|
||||
|
||||
export type GetStats200 = {
|
||||
|
||||
@@ -27,9 +27,11 @@ import type {
|
||||
ListSpanMapperGroupsParams,
|
||||
ListSpanMappers200,
|
||||
ListSpanMappersPathParameters,
|
||||
PreviewSpanMapping200,
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesSpanMappingPreviewRequestDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
UpdateSpanMapperGroupPathParameters,
|
||||
@@ -780,3 +782,86 @@ export const useUpdateSpanMapper = <
|
||||
> => {
|
||||
return useMutation(getUpdateSpanMapperMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Previews how attribute mappings would transform a sample span.
|
||||
* @summary Preview span attribute mapping against a sample span
|
||||
*/
|
||||
export const previewSpanMapping = (
|
||||
spantypesSpanMappingPreviewRequestDTO?: BodyType<SpantypesSpanMappingPreviewRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PreviewSpanMapping200>({
|
||||
url: `/api/v1/span_mapper_groups/preview`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesSpanMappingPreviewRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPreviewSpanMappingMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesSpanMappingPreviewRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesSpanMappingPreviewRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['previewSpanMapping'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>,
|
||||
{ data?: BodyType<SpantypesSpanMappingPreviewRequestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return previewSpanMapping(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PreviewSpanMappingMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>
|
||||
>;
|
||||
export type PreviewSpanMappingMutationBody =
|
||||
| BodyType<SpantypesSpanMappingPreviewRequestDTO>
|
||||
| undefined;
|
||||
export type PreviewSpanMappingMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Preview span attribute mapping against a sample span
|
||||
*/
|
||||
export const usePreviewSpanMapping = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesSpanMappingPreviewRequestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof previewSpanMapping>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesSpanMappingPreviewRequestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPreviewSpanMappingMutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -1,29 +1,67 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
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 { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
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 CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import useActiveQueryIndex from './useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from './useCheckboxFilterState';
|
||||
import useCheckboxFilterValues from './useCheckboxFilterValues';
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
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;
|
||||
@@ -34,39 +72,194 @@ interface ICheckboxProps {
|
||||
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
const { source, filter, onFilterChange } = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
// null = no user action, true = user opened, false = user closed
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
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,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
} = useCheckboxDisclosure({ filter, activeQueryIndex });
|
||||
filter.defaultOpen,
|
||||
]);
|
||||
|
||||
const { attributeValues, isLoading } = useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
});
|
||||
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 { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
|
||||
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
|
||||
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 { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
});
|
||||
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 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(
|
||||
@@ -84,6 +277,293 @@ 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 &&
|
||||
@@ -91,19 +571,48 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="checkbox-filter">
|
||||
<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
|
||||
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>
|
||||
</section>
|
||||
)}
|
||||
{isOpen && !isLoading && (
|
||||
<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 && (
|
||||
<>
|
||||
{!isEmptyStateWithDocsEnabled && (
|
||||
<section className="search">
|
||||
@@ -125,24 +634,48 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
data-testid="filter-separator"
|
||||
/>
|
||||
)}
|
||||
<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)
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
</Fragment>
|
||||
))}
|
||||
</section>
|
||||
@@ -155,7 +688,10 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
)}
|
||||
{visibleItemsCount < attributeValues?.length && (
|
||||
<section className="show-more">
|
||||
<Typography.Text className="show-more-text" onClick={onShowMore}>
|
||||
<Typography.Text
|
||||
className="show-more-text"
|
||||
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||
>
|
||||
Show More...
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
@@ -1,68 +0,0 @@
|
||||
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;
|
||||
@@ -1,417 +0,0 @@
|
||||
/* 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;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
@@ -1,78 +0,0 @@
|
||||
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;
|
||||
@@ -1,71 +0,0 @@
|
||||
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;
|
||||
@@ -1,99 +0,0 @@
|
||||
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;
|
||||
@@ -20,7 +20,6 @@ import APIError from 'types/api/error';
|
||||
import DashboardActions from './DashboardActions/DashboardActions';
|
||||
import DashboardInfo from './DashboardInfo/DashboardInfo';
|
||||
import { useEditableTitle } from './DashboardInfo/useEditableTitle';
|
||||
import { usePublicDashboardMeta } from '../DashboardSettings/PublicDashboard/usePublicDashboardMeta';
|
||||
|
||||
import styles from './DashboardPageToolbar.module.scss';
|
||||
|
||||
@@ -53,10 +52,6 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
// Single global fetch of the public-sharing meta (the drawer reuses this cache);
|
||||
// drives the public-access badge.
|
||||
const { isPublic: isPublicDashboard } = usePublicDashboardMeta(id);
|
||||
|
||||
const isAuthor =
|
||||
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
|
||||
|
||||
@@ -122,7 +117,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
|
||||
image={image}
|
||||
tags={tags}
|
||||
description={description}
|
||||
isPublicDashboard={isPublicDashboard}
|
||||
isPublicDashboard={false}
|
||||
isDashboardLocked={isDashboardLocked}
|
||||
isEditing={isEditing}
|
||||
draft={draft}
|
||||
|
||||
@@ -1,15 +1,106 @@
|
||||
// Publish tab — "status strip" direction (Claude Design: Publish Drawer Final).
|
||||
// Fills the drawer height so the actions anchor a footer instead of floating.
|
||||
.publishTab {
|
||||
// settings card wrapper — mirrors the V1 public dashboard treatment
|
||||
.publicDashboardCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
.statusTitle {
|
||||
margin-bottom: 16px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeRangeSelectGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeRangeSelectLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.timeRangeSelect {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.urlGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.urlLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.urlContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.urlText {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.callout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 12px 8px;
|
||||
border-radius: 3px;
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
|
||||
.calloutIcon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-robin-300);
|
||||
}
|
||||
|
||||
.calloutText {
|
||||
color: var(--text-robin-300);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Globe, RefreshCw, Trash } from '@signozhq/icons';
|
||||
import { Globe, Trash } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
|
||||
import styles from './PublicDashboardActions.module.scss';
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
interface PublicDashboardActionsProps {
|
||||
isPublic: boolean;
|
||||
@@ -25,7 +25,7 @@ function PublicDashboardActions({
|
||||
onUnpublish,
|
||||
}: PublicDashboardActionsProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.actions}>
|
||||
{isPublic ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -33,22 +33,22 @@ function PublicDashboardActions({
|
||||
color="destructive"
|
||||
disabled={disabled}
|
||||
loading={isUnpublishing}
|
||||
prefix={<Trash size={15} />}
|
||||
prefix={<Trash size={14} />}
|
||||
testId="public-dashboard-unpublish"
|
||||
onClick={onUnpublish}
|
||||
>
|
||||
Unpublish Dashboard
|
||||
Unpublish dashboard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
loading={isUpdating}
|
||||
prefix={<RefreshCw size={15} />}
|
||||
prefix={<Globe size={14} />}
|
||||
testId="public-dashboard-update"
|
||||
onClick={onUpdate}
|
||||
>
|
||||
Update Dashboard
|
||||
Update published dashboard
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -57,11 +57,11 @@ function PublicDashboardActions({
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
loading={isPublishing}
|
||||
prefix={<Globe size={15} />}
|
||||
prefix={<Globe size={14} />}
|
||||
testId="public-dashboard-publish"
|
||||
onClick={onPublish}
|
||||
>
|
||||
Publish Dashboard
|
||||
Publish dashboard
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
.footer {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
function PublicDashboardCallout(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.callout}>
|
||||
<Info size={12} className={styles.calloutIcon} />
|
||||
<Typography.Text className={styles.calloutText}>
|
||||
Dashboard variables won't work in public dashboards
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardCallout;
|
||||
@@ -1,19 +0,0 @@
|
||||
.hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.hintIcon {
|
||||
flex: none;
|
||||
margin-top: 1px;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.hintText {
|
||||
color: var(--l3-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './PublicDashboardHint.module.scss';
|
||||
|
||||
function PublicDashboardHint(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.hint}>
|
||||
<Info size={14} className={styles.hintIcon} />
|
||||
<Typography.Text className={styles.hintText}>
|
||||
Dashboard variables aren't supported on public links.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardHint;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { SelectSimple } from '@signozhq/ui/select';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import styles from './PublicDashboardSettingsForm.module.scss';
|
||||
import { TIME_RANGE_PRESETS_OPTIONS } from './constants';
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
interface PublicDashboardSettingsFormProps {
|
||||
timeRangeEnabled: boolean;
|
||||
@@ -22,29 +22,28 @@ function PublicDashboardSettingsForm({
|
||||
}: PublicDashboardSettingsFormProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.switchRow}>
|
||||
<Switch
|
||||
testId="public-dashboard-time-range-toggle"
|
||||
value={timeRangeEnabled}
|
||||
disabled={disabled}
|
||||
onChange={onTimeRangeEnabledChange}
|
||||
>
|
||||
Enable time range
|
||||
</Switch>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="public-dashboard-enable-time-range"
|
||||
className={styles.checkbox}
|
||||
testId="public-dashboard-time-range-toggle"
|
||||
value={timeRangeEnabled}
|
||||
disabled={disabled}
|
||||
onChange={(checked): void => onTimeRangeEnabledChange(checked === true)}
|
||||
>
|
||||
Enable time range
|
||||
</Checkbox>
|
||||
|
||||
<div className={styles.fieldGroup}>
|
||||
<Typography.Text className={styles.fieldLabel}>
|
||||
<div className={styles.timeRangeSelectGroup}>
|
||||
<Typography.Text className={styles.timeRangeSelectLabel}>
|
||||
Default time range
|
||||
</Typography.Text>
|
||||
<SelectSimple
|
||||
className={styles.timeRangeSelect}
|
||||
testId="public-dashboard-default-time-range"
|
||||
placeholder="Select default time range"
|
||||
items={RelativeDurationOptions}
|
||||
items={TIME_RANGE_PRESETS_OPTIONS}
|
||||
value={defaultTimeRange}
|
||||
disabled={disabled}
|
||||
withPortal={false}
|
||||
onChange={(value): void => onDefaultTimeRangeChange(value as string)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1,34 +0,0 @@
|
||||
.switchRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fieldGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
// Render the (non-portaled) dropdown above the drawer.
|
||||
[data-radix-popper-content-wrapper] {
|
||||
z-index: 1100 !important;
|
||||
}
|
||||
|
||||
// Radix sets --radix-select-trigger-width on the content element (the wrapper's
|
||||
// child), so match it there to make the dropdown take the input's width.
|
||||
// SelectSimple exposes no content className, hence the descendant selector.
|
||||
[data-radix-popper-content-wrapper] > * {
|
||||
width: var(--radix-select-trigger-width);
|
||||
min-width: var(--radix-select-trigger-width);
|
||||
}
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.timeRangeSelect {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
interface PublicDashboardStatusProps {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
function PublicDashboardStatus({
|
||||
isPublic,
|
||||
}: PublicDashboardStatusProps): JSX.Element {
|
||||
return (
|
||||
<Typography.Text className={styles.statusTitle}>
|
||||
{isPublic
|
||||
? 'This dashboard is publicly accessible. Anyone with the link can view it.'
|
||||
: 'This dashboard is private. Publish it to make it accessible to anyone with the link.'}
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardStatus;
|
||||
@@ -1,67 +0,0 @@
|
||||
.statusStrip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.statusStripLive {
|
||||
border-color: var(--callout-primary-border);
|
||||
background: var(--callout-primary-background);
|
||||
}
|
||||
|
||||
.statusMedallion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.statusMedallionLive {
|
||||
border-color: var(--callout-primary-border);
|
||||
background: var(--callout-primary-background);
|
||||
color: var(--callout-primary-icon);
|
||||
}
|
||||
|
||||
.statusBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statusTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.statusSubtitle {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.statusSubtitleLive {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.statusBadgeDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
background: currentColor;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Globe, LockKeyhole } from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from './PublicDashboardStatus.module.scss';
|
||||
|
||||
interface PublicDashboardStatusProps {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
function PublicDashboardStatus({
|
||||
isPublic,
|
||||
}: PublicDashboardStatusProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.statusStrip, { [styles.statusStripLive]: isPublic })}
|
||||
>
|
||||
<span
|
||||
className={cx(styles.statusMedallion, {
|
||||
[styles.statusMedallionLive]: isPublic,
|
||||
})}
|
||||
>
|
||||
{isPublic ? <Globe size={18} /> : <LockKeyhole size={18} />}
|
||||
</span>
|
||||
|
||||
<div className={styles.statusBody}>
|
||||
<Typography.Text className={styles.statusTitle}>
|
||||
{isPublic ? 'This dashboard is live' : 'This dashboard is private'}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className={cx(styles.statusSubtitle, {
|
||||
[styles.statusSubtitleLive]: isPublic,
|
||||
})}
|
||||
>
|
||||
{isPublic
|
||||
? 'Anyone with the link can view it — no account needed.'
|
||||
: 'Publish it to share a read-only view with anyone who has the link.'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" color={isPublic ? 'robin' : 'secondary'}>
|
||||
<span className={styles.statusBadgeDot} />
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardStatus;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Copy, ExternalLink } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
interface PublicDashboardUrlProps {
|
||||
url: string;
|
||||
onCopy: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
function PublicDashboardUrl({
|
||||
url,
|
||||
onCopy,
|
||||
onOpen,
|
||||
}: PublicDashboardUrlProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.urlGroup}>
|
||||
<Typography.Text className={styles.urlLabel}>
|
||||
Public dashboard URL
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.urlContainer}>
|
||||
<Typography.Text className={styles.urlText}>{url}</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Copy public dashboard URL"
|
||||
testId="public-dashboard-copy-url"
|
||||
onClick={onCopy}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Open public dashboard in new tab"
|
||||
testId="public-dashboard-open-url"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardUrl;
|
||||
@@ -1,69 +0,0 @@
|
||||
.fieldGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.linkPlaceholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--l2-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.linkPlaceholderIcon {
|
||||
flex: none;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.linkPlaceholderText {
|
||||
color: var(--l3-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.linkField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 40px;
|
||||
padding: 0 5px 0 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
}
|
||||
|
||||
.linkUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--l2-foreground);
|
||||
font-family: var(--font-mono, 'Geist Mono'), monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.linkDivider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 4px;
|
||||
background: var(--l2-border);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Copy, ExternalLink, Link2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './PublicDashboardUrl.module.scss';
|
||||
|
||||
interface PublicDashboardUrlProps {
|
||||
isPublic: boolean;
|
||||
url: string;
|
||||
onCopy: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
function PublicDashboardUrl({
|
||||
isPublic,
|
||||
url,
|
||||
onCopy,
|
||||
onOpen,
|
||||
}: PublicDashboardUrlProps): JSX.Element {
|
||||
return (
|
||||
<div className={styles.fieldGroup}>
|
||||
<Typography.Text className={styles.fieldLabel}>Public link</Typography.Text>
|
||||
|
||||
{isPublic ? (
|
||||
<div className={styles.linkField}>
|
||||
<Typography.Text className={styles.linkUrl}>{url}</Typography.Text>
|
||||
<span className={styles.linkDivider} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Copy link"
|
||||
testId="public-dashboard-copy-url"
|
||||
onClick={onCopy}
|
||||
>
|
||||
<Copy size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Open link"
|
||||
testId="public-dashboard-open-url"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<ExternalLink size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.linkPlaceholder}>
|
||||
<Link2 size={15} className={styles.linkPlaceholderIcon} />
|
||||
<Typography.Text className={styles.linkPlaceholderText}>
|
||||
Your shareable link will appear here once published
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicDashboardUrl;
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface TimeRangePresetOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Default time-range presets offered for the public dashboard viewer.
|
||||
export const TIME_RANGE_PRESETS_OPTIONS: TimeRangePresetOption[] = [
|
||||
{ label: 'Last 5 minutes', value: '5m' },
|
||||
{ label: 'Last 15 minutes', value: '15m' },
|
||||
{ label: 'Last 30 minutes', value: '30m' },
|
||||
{ label: 'Last 1 hour', value: '1h' },
|
||||
{ label: 'Last 6 hours', value: '6h' },
|
||||
{ label: 'Last 1 day', value: '24h' },
|
||||
];
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import PublicDashboardActions from './PublicDashboardActions/PublicDashboardActions';
|
||||
import PublicDashboardHint from './PublicDashboardHint/PublicDashboardHint';
|
||||
import PublicDashboardSettingsForm from './PublicDashboardSettingsForm/PublicDashboardSettingsForm';
|
||||
import PublicDashboardStatus from './PublicDashboardStatus/PublicDashboardStatus';
|
||||
import PublicDashboardUrl from './PublicDashboardUrl/PublicDashboardUrl';
|
||||
import PublicDashboardActions from './PublicDashboardActions';
|
||||
import PublicDashboardCallout from './PublicDashboardCallout';
|
||||
import PublicDashboardSettingsForm from './PublicDashboardSettingsForm';
|
||||
import PublicDashboardStatus from './PublicDashboardStatus';
|
||||
import PublicDashboardUrl from './PublicDashboardUrl';
|
||||
import { usePublicDashboard } from './usePublicDashboard';
|
||||
import styles from './PublicDashboard.module.scss';
|
||||
|
||||
@@ -37,27 +37,22 @@ function PublicDashboardSettings({
|
||||
const controlsDisabled = isLoading || !isAdmin;
|
||||
|
||||
return (
|
||||
<div className={styles.publishTab}>
|
||||
<div className={styles.content}>
|
||||
<PublicDashboardStatus isPublic={isPublic} />
|
||||
<div className={styles.publicDashboardCard}>
|
||||
<PublicDashboardStatus isPublic={isPublic} />
|
||||
|
||||
<PublicDashboardUrl
|
||||
isPublic={isPublic}
|
||||
url={publicUrl}
|
||||
onCopy={onCopyUrl}
|
||||
onOpen={onOpenUrl}
|
||||
/>
|
||||
<PublicDashboardSettingsForm
|
||||
timeRangeEnabled={timeRangeEnabled}
|
||||
defaultTimeRange={defaultTimeRange}
|
||||
disabled={controlsDisabled}
|
||||
onTimeRangeEnabledChange={setTimeRangeEnabled}
|
||||
onDefaultTimeRangeChange={setDefaultTimeRange}
|
||||
/>
|
||||
|
||||
<PublicDashboardSettingsForm
|
||||
timeRangeEnabled={timeRangeEnabled}
|
||||
defaultTimeRange={defaultTimeRange}
|
||||
disabled={controlsDisabled}
|
||||
onTimeRangeEnabledChange={setTimeRangeEnabled}
|
||||
onDefaultTimeRangeChange={setDefaultTimeRange}
|
||||
/>
|
||||
</div>
|
||||
{isPublic && (
|
||||
<PublicDashboardUrl url={publicUrl} onCopy={onCopyUrl} onOpen={onOpenUrl} />
|
||||
)}
|
||||
|
||||
<PublicDashboardHint />
|
||||
<PublicDashboardCallout />
|
||||
|
||||
<PublicDashboardActions
|
||||
isPublic={isPublic}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
invalidateGetPublicDashboard,
|
||||
useCreatePublicDashboard,
|
||||
useDeletePublicDashboard,
|
||||
useGetPublicDashboard,
|
||||
useUpdatePublicDashboard,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
@@ -16,8 +17,6 @@ import { USER_ROLES } from 'types/roles';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { usePublicDashboardMeta } from './usePublicDashboardMeta';
|
||||
|
||||
export interface UsePublicDashboardReturn {
|
||||
isPublic: boolean;
|
||||
isAdmin: boolean;
|
||||
@@ -55,16 +54,22 @@ export function usePublicDashboard(
|
||||
const [defaultTimeRange, setDefaultTimeRange] =
|
||||
useState<string>(DEFAULT_TIME_RANGE);
|
||||
|
||||
// Read the shared public-meta cache — the GET is owned globally (toolbar), so the
|
||||
// drawer reuses it rather than issuing its own request.
|
||||
const {
|
||||
publicMeta,
|
||||
isPublic,
|
||||
data,
|
||||
isLoading: isLoadingMeta,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = usePublicDashboardMeta(dashboardId);
|
||||
} = useGetPublicDashboard(
|
||||
{ id: dashboardId },
|
||||
{ query: { enabled: !!dashboardId, retry: false } },
|
||||
);
|
||||
|
||||
// react-query retains the last successful `data` even after a refetch errors, so
|
||||
// after unpublishing (the refetch 404s) `data` still holds the old publicPath.
|
||||
// Gate on `!error` so the UI flips back to the private state.
|
||||
const publicMeta = error ? undefined : data?.data;
|
||||
const isPublic = !!publicMeta?.publicPath;
|
||||
|
||||
// Seed form state from the server config when published.
|
||||
useEffect(() => {
|
||||
@@ -98,7 +103,7 @@ export function usePublicDashboard(
|
||||
(message: string): void => {
|
||||
toast.success(message);
|
||||
void invalidateGetPublicDashboard(queryClient, { id: dashboardId });
|
||||
refetch();
|
||||
void refetch();
|
||||
},
|
||||
[queryClient, dashboardId, refetch],
|
||||
);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetPublicDashboard } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettablePublicDasbhboardDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
export interface UsePublicDashboardMetaReturn {
|
||||
publicMeta: DashboardtypesGettablePublicDasbhboardDTO | undefined;
|
||||
isPublic: boolean;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: unknown;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
// How long a fetched result stays fresh before a natural trigger may refresh it.
|
||||
const PUBLIC_META_STALE_TIME = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Single source of truth for a dashboard's public-sharing meta. Keyed by dashboard
|
||||
* id via the generated query, so the GET happens once globally (the toolbar mounts it
|
||||
* with the dashboard) and every other caller — the publish settings drawer — reads the
|
||||
* same cache instead of issuing its own request. A mutation that invalidates
|
||||
* getGetPublicDashboardQueryKey refreshes all consumers at once.
|
||||
*
|
||||
* Only fetched on cloud / enterprise tenants, where public dashboards are available.
|
||||
*/
|
||||
export function usePublicDashboardMeta(
|
||||
dashboardId: string,
|
||||
): UsePublicDashboardMetaReturn {
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
const enabled = !!dashboardId && (isCloudUser || isEnterpriseSelfHostedUser);
|
||||
|
||||
const { data, isLoading, isFetching, error, refetch } = useGetPublicDashboard(
|
||||
{ id: dashboardId },
|
||||
{
|
||||
query: {
|
||||
enabled,
|
||||
retry: false,
|
||||
// refetchOnMount: false stops opening the drawer / switching to the Publish
|
||||
// tab from refiring the GET — it reuses the toolbar's cached result. A finite
|
||||
// staleTime still lets it refresh naturally once the data ages, and mutations
|
||||
// invalidate the key to refresh the published state immediately.
|
||||
staleTime: PUBLIC_META_STALE_TIME,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// react-query retains the last successful `data` after a refetch errors (e.g. the
|
||||
// 404 once a dashboard is unpublished), so gate on the error to reflect the
|
||||
// private state.
|
||||
const publicMeta = error ? undefined : data?.data;
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
publicMeta,
|
||||
isPublic: !!publicMeta?.publicPath,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
}),
|
||||
[publicMeta, isLoading, isFetching, error, refetch],
|
||||
);
|
||||
}
|
||||
@@ -69,24 +69,19 @@ function stripUndefinedLabels(
|
||||
export function toPostableRuleDTO(
|
||||
local: PostableAlertRuleV2,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload: Record<keyof RuletypesPostableRuleDTO, any> = {
|
||||
const payload = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
condition: local.condition,
|
||||
annotations: local.annotations,
|
||||
labels: stripUndefinedLabels(local.labels),
|
||||
evalWindow: (local as unknown as RuletypesPostableRuleDTO).evalWindow,
|
||||
frequency: (local as unknown as RuletypesPostableRuleDTO).frequency,
|
||||
preferredChannels: (local as unknown as RuletypesPostableRuleDTO)
|
||||
.preferredChannels,
|
||||
notificationSettings: local.notificationSettings,
|
||||
evaluation: local.evaluation,
|
||||
schemaVersion: local.schemaVersion,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
description: (local as unknown as RuletypesPostableRuleDTO).description,
|
||||
};
|
||||
return payload as unknown as RuletypesPostableRuleDTO;
|
||||
}
|
||||
@@ -94,7 +89,7 @@ export function toPostableRuleDTO(
|
||||
export function toPostableRuleDTOFromAlertDef(
|
||||
local: AlertDef,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload: Record<keyof RuletypesPostableRuleDTO, any> = {
|
||||
const payload = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
@@ -104,16 +99,11 @@ export function toPostableRuleDTOFromAlertDef(
|
||||
evalWindow: local.evalWindow,
|
||||
frequency: local.frequency,
|
||||
preferredChannels: local.preferredChannels,
|
||||
notificationSettings: (local as unknown as RuletypesPostableRuleDTO)
|
||||
.notificationSettings,
|
||||
evaluation: (local as unknown as RuletypesPostableRuleDTO).evaluation,
|
||||
schemaVersion: (local as unknown as RuletypesPostableRuleDTO).schemaVersion,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
description: (local as unknown as RuletypesPostableRuleDTO).description,
|
||||
};
|
||||
return payload;
|
||||
return payload as unknown as RuletypesPostableRuleDTO;
|
||||
}
|
||||
|
||||
export function fromRuleDTOToPostableRuleV2(
|
||||
|
||||
@@ -51,6 +51,26 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/preview", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.spanMapperHandler.PreviewMapping),
|
||||
handler.OpenAPIDef{
|
||||
ID: "PreviewSpanMapping",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Preview span attribute mapping against a sample span",
|
||||
Description: "Previews how attribute mappings would transform a sample span.",
|
||||
Request: new(spantypes.SpanMappingPreviewRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.SpanMappingPreviewResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.UpdateGroup),
|
||||
handler.OpenAPIDef{
|
||||
|
||||
@@ -3,16 +3,15 @@ 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")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
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")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
@@ -89,14 +88,6 @@ 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, false, nil,
|
||||
samplesAdjustedStartMs, flooredEndMs, nil,
|
||||
)
|
||||
|
||||
distributedSamplesTable, localSamplesTable := telemetrymetrics.WhichSamplesTableToUse(
|
||||
samplesAdjustedStartMs, flooredEndMs,
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil,
|
||||
metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, nil,
|
||||
)
|
||||
|
||||
return samplesAdjustedStartMs, flooredEndMs, tsAdjustedStartMs, distributedTSTable, localTSTable, distributedSamplesTable, localSamplesTable
|
||||
|
||||
@@ -304,7 +304,7 @@ func TestCompositeKeyFromLabels(t *testing.T) {
|
||||
name: "daemonset and namespace group-by",
|
||||
labels: map[string]string{
|
||||
"k8s.daemonset.name": "web-1",
|
||||
"k8s.namespace.name": "ns-x",
|
||||
"k8s.namespace.name": "ns-x",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{daemonSetNameGroupByKey, namespaceNameGroupByKey},
|
||||
expected: "web-1\x00ns-x",
|
||||
@@ -330,47 +330,6 @@ func TestCompositeKeyFromLabels(t *testing.T) {
|
||||
groupBy: []qbtypes.GroupByKey{deploymentNameGroupByKey, namespaceNameGroupByKey, clusterNameGroupByKey},
|
||||
expected: "web-1\x00ns-x\x00",
|
||||
},
|
||||
{
|
||||
// volumes default group identity: (pvc, namespace, cluster).
|
||||
name: "pvc, namespace and cluster group-by",
|
||||
labels: map[string]string{
|
||||
"k8s.persistentvolumeclaim.name": "data-pg-0",
|
||||
"k8s.namespace.name": "ns-x",
|
||||
"k8s.cluster.name": "cluster-a",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{pvcNameGroupByKey, namespaceNameGroupByKey, clusterNameGroupByKey},
|
||||
expected: "data-pg-0\x00ns-x\x00cluster-a",
|
||||
},
|
||||
{
|
||||
// absent cluster label on a PVC -> empty trailing segment.
|
||||
name: "pvc missing cluster label yields empty trailing segment",
|
||||
labels: map[string]string{
|
||||
"k8s.persistentvolumeclaim.name": "data-pg-0",
|
||||
"k8s.namespace.name": "ns-x",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{pvcNameGroupByKey, namespaceNameGroupByKey, clusterNameGroupByKey},
|
||||
expected: "data-pg-0\x00ns-x\x00",
|
||||
},
|
||||
{
|
||||
// namespaces default group identity: (namespace, cluster) — namespaces are
|
||||
// cluster-scoped, so cluster is the only cross-cluster disambiguator.
|
||||
name: "namespace and cluster group-by",
|
||||
labels: map[string]string{
|
||||
"k8s.namespace.name": "ns-x",
|
||||
"k8s.cluster.name": "cluster-a",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{namespaceNameGroupByKey, clusterNameGroupByKey},
|
||||
expected: "ns-x\x00cluster-a",
|
||||
},
|
||||
{
|
||||
// absent cluster label on a namespace -> empty trailing segment.
|
||||
name: "namespace missing cluster label yields empty trailing segment",
|
||||
labels: map[string]string{
|
||||
"k8s.namespace.name": "ns-x",
|
||||
},
|
||||
groupBy: []qbtypes.GroupByKey{namespaceNameGroupByKey, clusterNameGroupByKey},
|
||||
expected: "ns-x\x00",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -360,7 +360,7 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{namespaceNameGroupByKey, clusterNameGroupByKey}
|
||||
req.GroupBy = []qbtypes.GroupByKey{namespaceNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
@@ -535,7 +535,7 @@ func (m *module) ListVolumes(ctx context.Context, orgID valuer.UUID, req *infram
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{pvcNameGroupByKey, namespaceNameGroupByKey, clusterNameGroupByKey}
|
||||
req.GroupBy = []qbtypes.GroupByKey{pvcNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
|
||||
@@ -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), false, nil)
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(*params.Start), uint64(*params.End), 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, false, nil)
|
||||
tsStart, _, tsTable, _ := telemetrymetrics.WhichTSTableToUse(start, end, 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), false, nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil)
|
||||
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)
|
||||
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), false, nil)
|
||||
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(req.Start), uint64(req.End), 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), false, nil)
|
||||
distributedSamplesTable, _ := telemetrymetrics.WhichSamplesTableToUse(uint64(req.Start), uint64(req.End), metrictypes.UnspecifiedType, metrictypes.TimeAggregationUnspecified, false, nil)
|
||||
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)
|
||||
countExp := telemetrymetrics.CountExpressionForSamplesTable(distributedSamplesTable)
|
||||
|
||||
candidateLimit := req.Limit + 50
|
||||
|
||||
@@ -273,6 +273,35 @@ func (h *handler) DeleteMapper(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// PreviewMapping handles POST /api/v1/span_mapper_groups/preview.
|
||||
// used to get preview of attributes/resources after remapping.
|
||||
func (h *handler) PreviewMapping(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
req := new(spantypes.SpanMappingPreviewRequest)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.PreviewMapping(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// groupIDFromPath extracts and validates the {id} or {groupId} path variable.
|
||||
func groupIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
@@ -102,6 +103,97 @@ func (module *module) DeleteMapper(ctx context.Context, orgID, groupID, id value
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreviewMapping resolves the mappings to preview (from the request body, a
|
||||
// saved group, or all enabled saved mappings) and returns the input span with
|
||||
// its "attributes" and "resource" maps transformed.
|
||||
func (module *module) PreviewMapping(ctx context.Context, orgID valuer.UUID, req *spantypes.SpanMappingPreviewRequest) (*spantypes.SpanMappingPreviewResponse, error) {
|
||||
groups, err := module.resolvePreviewGroups(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(req.Span) == 0 {
|
||||
return nil, errors.New(errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "'span' must be provided")
|
||||
}
|
||||
|
||||
outResource, outSpan := spantypes.SimulateMappingForAttributes(groups, spanAttrMap(req.Span["resource"]), spanAttrMap(req.Span["attributes"]))
|
||||
|
||||
result := make(map[string]any, len(req.Span))
|
||||
for k, v := range req.Span {
|
||||
result[k] = v
|
||||
}
|
||||
setAttrMap(result, "attributes", req.Span, outSpan)
|
||||
setAttrMap(result, "resource", req.Span, outResource)
|
||||
|
||||
return &spantypes.SpanMappingPreviewResponse{Span: result}, nil
|
||||
}
|
||||
|
||||
func spanAttrMap(v any) map[string]any {
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
return m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setAttrMap(dst map[string]any, key string, in map[string]any, transformed map[string]any) {
|
||||
if _, present := in[key]; present || len(transformed) > 0 {
|
||||
dst[key] = transformed
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePreviewGroups picks the mappings to preview against: the groups in the
|
||||
// request body, else a specific saved group (GroupID), else all of the org's
|
||||
// enabled saved mappings.
|
||||
func (module *module) resolvePreviewGroups(ctx context.Context, orgID valuer.UUID, req *spantypes.SpanMappingPreviewRequest) ([]*spantypes.SpanMapperGroupWithMappers, error) {
|
||||
hasGroups := len(req.Groups) > 0
|
||||
hasGroupID := req.GroupID != nil && *req.GroupID != ""
|
||||
|
||||
if hasGroups && hasGroupID {
|
||||
return nil, errors.New(errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "provide either 'groups' or 'groupId', not both")
|
||||
}
|
||||
|
||||
if hasGroups {
|
||||
groups := make([]*spantypes.SpanMapperGroupWithMappers, 0, len(req.Groups))
|
||||
for _, spec := range req.Groups {
|
||||
group := &spantypes.SpanMapperGroup{
|
||||
OrgID: orgID,
|
||||
Name: spec.Group.Name,
|
||||
Condition: spec.Group.Condition,
|
||||
Enabled: spec.Group.Enabled,
|
||||
}
|
||||
mappers := make([]*spantypes.SpanMapper, 0, len(spec.Mappers))
|
||||
for _, pm := range spec.Mappers {
|
||||
mappers = append(mappers, &spantypes.SpanMapper{
|
||||
Name: pm.Name,
|
||||
FieldContext: pm.FieldContext,
|
||||
Config: pm.Config,
|
||||
Enabled: pm.Enabled,
|
||||
})
|
||||
}
|
||||
groups = append(groups, &spantypes.SpanMapperGroupWithMappers{Group: group, Mappers: mappers})
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
if hasGroupID {
|
||||
id, err := valuer.NewUUID(*req.GroupID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "group id is not a valid uuid")
|
||||
}
|
||||
group, err := module.store.GetGroup(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mappers, err := module.store.ListMappers(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*spantypes.SpanMapperGroupWithMappers{{Group: group, Mappers: mappers}}, nil
|
||||
}
|
||||
|
||||
return module.listEnabledGroupsWithMappers(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
return spantypes.SpanAttrMappingFeatureType
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type Module interface {
|
||||
CreateMapper(ctx context.Context, orgID, groupID valuer.UUID, mapper *spantypes.SpanMapper) error
|
||||
UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, fieldContext spantypes.FieldContext, config *spantypes.SpanMapperConfig, enabled *bool, updatedBy string) error
|
||||
DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
|
||||
PreviewMapping(ctx context.Context, orgID valuer.UUID, req *spantypes.SpanMappingPreviewRequest) (*spantypes.SpanMappingPreviewResponse, error)
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for mapping group and mapper endpoints.
|
||||
@@ -42,4 +43,5 @@ type Handler interface {
|
||||
CreateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
UpdateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
DeleteMapper(rw http.ResponseWriter, r *http.Request)
|
||||
PreviewMapping(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
@@ -91,22 +91,13 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
if a.ComparisonSpaceAggregationParam != nil {
|
||||
spaceAggParamStr = a.ComparisonSpaceAggregationParam.StringValue()
|
||||
}
|
||||
part := fmt.Sprintf("%s:%s:%s:%s:%s",
|
||||
aggParts = append(aggParts, 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, ",")))
|
||||
|
||||
@@ -119,7 +119,11 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
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)
|
||||
// Resolve metric metadata once per request: patches each metric-aggregation
|
||||
// query's spec in place, returns the queries whose every aggregation was
|
||||
// missing (used for preseeded empty results), and any dormant-metric
|
||||
// warning string. NotFound errors for never-seen metrics are propagated.
|
||||
missingMetricQueries, dormantMetricsWarningMsg, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,15 +240,13 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if dormantMetricsWarningMsg != "" {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: dormantMetricsWarningMsg,
|
||||
})
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
@@ -300,11 +302,12 @@ func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.Quer
|
||||
// - missingMetricQueries: names of queries whose every aggregation was
|
||||
// missing. Used downstream to preseed empty result placeholders so the
|
||||
// response still has an entry per requested query name.
|
||||
// - metricWarnings: human-readable warnings for metrics that could not be
|
||||
// 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, orgID valuer.UUID, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, metricWarnings []string, err error) {
|
||||
// - dormantWarning: a human-readable warning describing metrics that exist in
|
||||
// the store but produced no data within the query window. Empty when no
|
||||
// such metrics are present.
|
||||
// - err: NotFound when one or more referenced metrics have never been seen,
|
||||
// or Internal when a metadata fetch fails.
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, dormantWarning string, err error) {
|
||||
metricNames := make([]string, 0)
|
||||
for idx := range queries {
|
||||
if queries[idx].Type != qbtypes.QueryTypeBuilder {
|
||||
@@ -322,13 +325,13 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
|
||||
if len(metricNames) == 0 {
|
||||
return nil, nil, nil
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
metricTemporality, metricTypes, reducedMetricsSet, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, orgID, start, end, metricNames...)
|
||||
metricTemporality, metricTypes, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, 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")
|
||||
return nil, "", errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
|
||||
}
|
||||
q.logger.DebugContext(ctx, "fetched metric temporalities and types", slog.Any("metric_temporality", metricTemporality), slog.Any("metric_types", metricTypes))
|
||||
|
||||
@@ -360,10 +363,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
// Type is resolved now; validate aggregation compatibility against it.
|
||||
if err := spec.Aggregations[i].ValidateForType(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if reducedMetricsSet[spec.Aggregations[i].MetricName] {
|
||||
spec.Aggregations[i].Reduced = true
|
||||
return nil, "", err
|
||||
}
|
||||
presentAggregations = append(presentAggregations, spec.Aggregations[i])
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
|
||||
if len(missingMetrics) == 0 {
|
||||
return missingMetricQueries, nil, nil
|
||||
return missingMetricQueries, "", nil
|
||||
}
|
||||
|
||||
isInternalMetric := func(n string) bool { return strings.HasPrefix(n, "signoz.") || strings.HasPrefix(n, "signoz_") }
|
||||
@@ -387,33 +387,29 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
}
|
||||
if len(externalMissingMetrics) == 0 {
|
||||
return missingMetricQueries, nil, nil
|
||||
// this means all missing metrics are internal, and since internal metrics
|
||||
// aren't user-controlled, skip errors/warnings for them since users can't act on them
|
||||
return missingMetricQueries, "", nil
|
||||
}
|
||||
|
||||
// Classify each missing metric: never-seen -> warning with empty result;
|
||||
// seen-but-no-data-in-window -> dormant warning.
|
||||
// Classify each missing metric: never-seen → NotFound error; seen-but-no-
|
||||
// data-in-window → dormant warning.
|
||||
lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, externalMissingMetrics...)
|
||||
var nonExistentMetrics []string
|
||||
var dormantMetrics []string
|
||||
nonExistentMetrics := []string{}
|
||||
for _, name := range externalMissingMetrics {
|
||||
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
|
||||
dormantMetrics = append(dormantMetrics, name)
|
||||
continue
|
||||
}
|
||||
nonExistentMetrics = append(nonExistentMetrics, name)
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
|
||||
// Never-seen metrics: the query already gets a preseeded empty result
|
||||
// via the aggregation-dropping path above; we just attach a warning.
|
||||
if len(nonExistentMetrics) == 1 {
|
||||
warnings = append(warnings, fmt.Sprintf("metric %s has never been received. Check the metric name and instrumentation", nonExistentMetrics[0]))
|
||||
} else if len(nonExistentMetrics) > 1 {
|
||||
warnings = append(warnings, fmt.Sprintf("the following metrics have never been received. Check the metric names and instrumentation: %s", strings.Join(nonExistentMetrics, ", ")))
|
||||
return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "could not find the metric %s", nonExistentMetrics[0])
|
||||
}
|
||||
if len(nonExistentMetrics) > 1 {
|
||||
return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "the following metrics were not found: %s", strings.Join(nonExistentMetrics, ", "))
|
||||
}
|
||||
|
||||
// Dormant metrics: seen before but no data in the query window.
|
||||
// All missing metrics are dormant — assemble the warning string.
|
||||
lastSeenStr := func(name string) string {
|
||||
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
|
||||
ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now")
|
||||
@@ -421,16 +417,16 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
return name
|
||||
}
|
||||
if len(dormantMetrics) == 1 {
|
||||
warnings = append(warnings, fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(dormantMetrics[0])))
|
||||
} else if len(dormantMetrics) > 1 {
|
||||
parts := make([]string, len(dormantMetrics))
|
||||
for i, m := range dormantMetrics {
|
||||
if len(externalMissingMetrics) == 1 {
|
||||
dormantWarning = fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0]))
|
||||
} else {
|
||||
parts := make([]string, len(externalMissingMetrics))
|
||||
for i, m := range externalMissingMetrics {
|
||||
parts[i] = lastSeenStr(m)
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", ")))
|
||||
dormantWarning = fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", "))
|
||||
}
|
||||
return missingMetricQueries, warnings, nil
|
||||
return missingMetricQueries, dormantWarning, nil
|
||||
}
|
||||
|
||||
func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (m *mockMetricStmtBuilder) Build(_ context.Context, _, _ uint64, _ qbtypes.
|
||||
|
||||
func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
// When a metric has UnspecifiedType and is not found in the metadata store,
|
||||
// the querier should return an empty result with a warning instead of an error.
|
||||
// the querier should return a not-found error, even if the request provides a temporality
|
||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||
metadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
|
||||
@@ -80,14 +80,9 @@ func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := q.QueryRange(context.Background(), valuer.GenerateUUID(), req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.Warning)
|
||||
|
||||
require.Len(t, resp.Warning.Warnings, 1)
|
||||
assert.Contains(t, resp.Warning.Warnings[0].Message, "unknown_metric")
|
||||
assert.Contains(t, resp.Warning.Warnings[0].Message, "has never been received")
|
||||
_, err := q.QueryRange(context.Background(), valuer.GenerateUUID(), req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "could not find the metric unknown_metric")
|
||||
}
|
||||
|
||||
func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
|
||||
@@ -2136,12 +2136,12 @@ func (t *telemetryMetaStore) GetAllValues(ctx context.Context, fieldValueSelecto
|
||||
return values, complete, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, 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, orgID, queryTimeRangeStartTs, queryTimeRangeEndTs, metricName)
|
||||
temporalityMap, err := t.FetchTemporalityMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricName)
|
||||
if err != nil {
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
@@ -2154,27 +2154,25 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, orgID valuer.
|
||||
return temporality, nil
|
||||
}
|
||||
|
||||
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...)
|
||||
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...)
|
||||
return temporalities, err
|
||||
}
|
||||
|
||||
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) {
|
||||
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), make(map[string]bool), nil
|
||||
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), 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, reduced, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, reductionEnabled, metricNames...)
|
||||
metricsTemporality, metricTypes, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
meterMetricsTemporality, meterMetricsTypes, err := t.fetchMeterSourceMetricsTemporalityAndType(ctx, metricNames...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// For metrics not found in the database, set to Unknown
|
||||
@@ -2199,10 +2197,10 @@ func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, o
|
||||
}
|
||||
}
|
||||
|
||||
return temporalities, types, reduced, nil
|
||||
return temporalities, types, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalMetrics.StringValue(),
|
||||
instrumentationtypes.CodeNamespace: "metadata",
|
||||
@@ -2210,58 +2208,48 @@ 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, false, nil)
|
||||
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, nil)
|
||||
|
||||
cols := []string{"metric_name", "temporality", "any(type) AS type", "any(is_monotonic) as is_monotonic"}
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
// Filter by metric names (in the temporality column due to data mix-up)
|
||||
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, reducedArgs...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
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, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
|
||||
return 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
|
||||
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 err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
}
|
||||
if temporality != metrictypes.Unknown {
|
||||
temporalities[metricName] = append(temporalities[metricName], temporality)
|
||||
@@ -2270,15 +2258,12 @@ 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, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
}
|
||||
|
||||
return temporalities, types, reduced, nil
|
||||
return temporalities, types, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
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,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
@@ -102,29 +101,9 @@ func (b *MetricQueryStatementBuilder) Build(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pairFallbackWarnings []string
|
||||
for _, sel := range keySelectors {
|
||||
if _, ok := keys[sel.Name]; !ok {
|
||||
keys[sel.Name] = []*telemetrytypes.TelemetryFieldKey{{
|
||||
Name: sel.Name,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
}}
|
||||
pairFallbackWarnings = append(pairFallbackWarnings,
|
||||
fmt.Sprintf("key `%s` not found on metric %s", sel.Name, query.Aggregations[0].MetricName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
start, end = querybuilder.AdjustedMetricTimeRange(start, end, uint64(query.StepInterval.Seconds()), query)
|
||||
|
||||
stmt, err := b.buildPipelineStatement(ctx, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmt.Warnings = append(stmt.Warnings, pairFallbackWarnings...)
|
||||
return stmt, nil
|
||||
return b.buildPipelineStatement(ctx, start, end, query, keys, variables)
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
@@ -181,30 +160,19 @@ 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
|
||||
|
||||
if timeSeriesCTE, timeSeriesCTEArgs, err = b.buildTimeSeriesCTE(ctx, tsStart, tsEnd, query, keys, variables, tsTable); err != nil {
|
||||
// time_series_cte
|
||||
// this is applicable for all the queries
|
||||
if timeSeriesCTE, timeSeriesCTEArgs, err = b.buildTimeSeriesCTE(ctx, start, end, query, keys, variables); 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, samplesTable, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
if frag, args, err := b.buildTemporalAggDeltaFastPath(start, end, query, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
return nil, err
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
@@ -212,7 +180,7 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
}
|
||||
} else {
|
||||
// temporal_aggregation_cte
|
||||
if frag, args, err := b.buildTemporalAggregationCTE(ctx, start, end, query, keys, samplesTable, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
if frag, args, err := b.buildTemporalAggregationCTE(ctx, start, end, query, keys, timeSeriesCTE, timeSeriesCTEArgs); err != nil {
|
||||
return nil, err
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
@@ -226,188 +194,18 @@ 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
|
||||
|
||||
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
|
||||
// final SELECT
|
||||
return b.BuildFinalSelect(cteFragments, cteArgs, query)
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -424,7 +222,8 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(
|
||||
samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation,
|
||||
start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality,
|
||||
query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints,
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
@@ -441,7 +240,8 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
|
||||
sb.SelectMore(fmt.Sprintf("%s AS value", aggCol))
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
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.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -461,7 +261,6 @@ 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()
|
||||
|
||||
@@ -485,7 +284,8 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
}
|
||||
}
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, tsTable))
|
||||
start, end, _, tbl := WhichTSTableToUse(start, end, query.Aggregations[0].TableHints)
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, tbl))
|
||||
|
||||
sb.Select("fingerprint")
|
||||
for _, g := range query.GroupBy {
|
||||
@@ -511,12 +311,6 @@ 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)
|
||||
}
|
||||
@@ -533,23 +327,21 @@ 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, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggDelta(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
} else if query.Aggregations[0].Temporality != metrictypes.Multiple {
|
||||
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
}
|
||||
return b.buildTemporalAggForMultipleTemporalities(ctx, start, end, query, samplesTable, timeSeriesCTE, timeSeriesCTEArgs)
|
||||
return b.buildTemporalAggForMultipleTemporalities(ctx, start, end, query, 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) {
|
||||
@@ -566,7 +358,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation)
|
||||
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -577,7 +369,8 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
|
||||
|
||||
sb.SelectMore(fmt.Sprintf("%s AS per_series_value", aggCol))
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
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.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -596,7 +389,6 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
_ context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -612,13 +404,14 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
baseSb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggCol, err := AggregationColumnForSamplesTable(samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation)
|
||||
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
baseSb.SelectMore(fmt.Sprintf("%s AS per_series_value", aggCol))
|
||||
|
||||
baseSb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
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.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
baseSb.Where(
|
||||
baseSb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
@@ -662,7 +455,6 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
_ context.Context,
|
||||
start, end uint64,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
samplesTable string,
|
||||
timeSeriesCTE string,
|
||||
timeSeriesCTEArgs []any,
|
||||
) (string, []any, error) {
|
||||
@@ -677,11 +469,11 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.Name))
|
||||
}
|
||||
|
||||
aggForDeltaTemporality, err := AggregationColumnForSamplesTable(samplesTable, metrictypes.Delta, query.Aggregations[0].TimeAggregation)
|
||||
aggForDeltaTemporality, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, metrictypes.Delta, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
aggForCumulativeTemporality, err := AggregationColumnForSamplesTable(samplesTable, metrictypes.Cumulative, query.Aggregations[0].TimeAggregation)
|
||||
aggForCumulativeTemporality, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, metrictypes.Cumulative, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -709,7 +501,8 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
|
||||
sb.SelectMore(expr)
|
||||
}
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s AS points", DBName, samplesTable))
|
||||
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.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
|
||||
sb.Where(
|
||||
sb.In("metric_name", query.Aggregations[0].MetricName),
|
||||
|
||||
@@ -217,39 +217,6 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "test_missing_key_falls_back_to_labels",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "signoz_calls_total",
|
||||
Type: metrictypes.SumType,
|
||||
Temporality: metrictypes.Cumulative,
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "k8s.statefulset.name = 'my-statefulset'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "k8s.statefulset.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, `k8s.statefulset.name`, 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(30)) AS ts, `k8s.statefulset.name`, max(value) AS per_series_value FROM signoz_metrics.distributed_samples_v4 AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'k8s.statefulset.name') AS `k8s.statefulset.name` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? AND JSONExtractString(labels, 'k8s.statefulset.name') = ? GROUP BY fingerprint, `k8s.statefulset.name`) 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, `k8s.statefulset.name` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `k8s.statefulset.name`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `k8s.statefulset.name`) SELECT * FROM __spatial_aggregation_cte ORDER BY `k8s.statefulset.name`, ts",
|
||||
Args: []any{"signoz_calls_total", uint64(1747936800000), uint64(1747983420000), "cumulative", false, "my-statefulset", "signoz_calls_total", uint64(1747947360000), uint64(1747983420000), 0},
|
||||
Warnings: []string{"key `k8s.statefulset.name` not found on metric signoz_calls_total"},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
@@ -30,17 +30,6 @@ 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 (
|
||||
@@ -60,16 +49,8 @@ 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 {
|
||||
@@ -168,20 +149,14 @@ 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, SamplesV4BufferTableName:
|
||||
case SamplesV4TableName:
|
||||
return SamplesV4TableName, SamplesV4LocalTableName
|
||||
case SamplesV4Agg5mTableName:
|
||||
return SamplesV4Agg5mTableName, SamplesV4Agg5mLocalTableName
|
||||
@@ -213,10 +188,13 @@ func WhichSamplesTableToUse(
|
||||
}
|
||||
|
||||
func AggregationColumnForSamplesTable(
|
||||
tableName string,
|
||||
start, end uint64,
|
||||
metricType metrictypes.Type,
|
||||
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:
|
||||
@@ -224,7 +202,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, SamplesV4BufferTableName:
|
||||
case SamplesV4TableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -266,7 +244,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, SamplesV4BufferTableName:
|
||||
case SamplesV4TableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -306,7 +284,7 @@ func AggregationColumnForSamplesTable(
|
||||
}
|
||||
case metrictypes.Unspecified:
|
||||
switch tableName {
|
||||
case SamplesV4TableName, SamplesV4BufferTableName:
|
||||
case SamplesV4TableName:
|
||||
switch timeAggregation {
|
||||
case metrictypes.TimeAggregationLatest:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
@@ -354,65 +332,6 @@ 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,9 +480,7 @@ 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,omitzero"`
|
||||
|
||||
Reduced bool `json:"-"`
|
||||
ReduceTo ReduceTo `json:"reduceTo,omitempty"`
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of MetricAggregation.
|
||||
|
||||
117
pkg/types/spantypes/spanmappersimulator.go
Normal file
117
pkg/types/spantypes/spanmappersimulator.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// resourceSourcePrefix marks a source that reads from resource attributes:
|
||||
// buildAttributeRule prefixes those keys with "resource." (e.g. "resource.service.name").
|
||||
var resourceSourcePrefix = FieldContextResource.StringValue() + "."
|
||||
|
||||
type SpanMappingPreviewGroup struct {
|
||||
Group PostableSpanMapperGroup `json:"group" required:"true"`
|
||||
Mappers []PostableSpanMapper `json:"mappers" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
type SpanMappingPreviewRequest struct {
|
||||
Span map[string]any `json:"span" nullable:"true"`
|
||||
Groups []SpanMappingPreviewGroup `json:"groups" nullable:"true"`
|
||||
GroupID *string `json:"groupId" nullable:"true"`
|
||||
}
|
||||
|
||||
type SpanMappingPreviewResponse struct {
|
||||
Span map[string]any `json:"span" nullable:"true"`
|
||||
}
|
||||
|
||||
func SimulateMappingForAttributes(groups []*SpanMapperGroupWithMappers, resourceAttrs, spanAttrs map[string]any) (outResource, outSpan map[string]any) {
|
||||
cfg := buildProcessorConfig(filterEnabledGroupsWithMappers(groups))
|
||||
|
||||
outResource = cloneAttrs(resourceAttrs)
|
||||
outSpan = cloneAttrs(spanAttrs)
|
||||
|
||||
applyEnabledGroups(cfg, outSpan, outResource)
|
||||
return outResource, outSpan
|
||||
}
|
||||
|
||||
func filterEnabledGroupsWithMappers(groups []*SpanMapperGroupWithMappers) []*SpanMapperGroupWithMappers {
|
||||
out := make([]*SpanMapperGroupWithMappers, 0, len(groups))
|
||||
for _, gm := range groups {
|
||||
if gm == nil || gm.Group == nil || !gm.Group.Enabled {
|
||||
continue
|
||||
}
|
||||
enabled := make([]*SpanMapper, 0, len(gm.Mappers))
|
||||
for _, m := range gm.Mappers {
|
||||
if m != nil && m.Enabled {
|
||||
enabled = append(enabled, m)
|
||||
}
|
||||
}
|
||||
if len(enabled) > 0 {
|
||||
out = append(out, &SpanMapperGroupWithMappers{Group: gm.Group, Mappers: enabled})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneAttrs(in map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(in))
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The functions below are copied from signoz-otel-collector (processor/signozspanmappingprocessor, PR #796):
|
||||
// TODO(spanmapper-preview): delete them and call the real processor once that PR merges and the dependency is bumped.
|
||||
func applyEnabledGroups(cfg *spanMapperProcessorConfig, spanAttrs, resourceAttrs map[string]any) {
|
||||
for i := range cfg.Groups {
|
||||
g := &cfg.Groups[i]
|
||||
if !spanMapperConditionMet(g.ExistsAny, spanAttrs, resourceAttrs) {
|
||||
continue
|
||||
}
|
||||
for j := range g.Attributes {
|
||||
applySpanMapperRule(&g.Attributes[j], spanAttrs, resourceAttrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func spanMapperConditionMet(cond spanMapperProcessorExistsAny, spanAttrs, resourceAttrs map[string]any) bool {
|
||||
return anyKeyContains(spanAttrs, cond.Attributes) || anyKeyContains(resourceAttrs, cond.Resource)
|
||||
}
|
||||
|
||||
func anyKeyContains(attrs map[string]any, patterns []string) bool {
|
||||
for k := range attrs {
|
||||
for _, p := range patterns {
|
||||
if strings.Contains(k, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func applySpanMapperRule(rule *spanMapperProcessorAttribute, spanAttrs, resourceAttrs map[string]any) {
|
||||
dst := spanAttrs
|
||||
if rule.Context == FieldContextResource.StringValue() {
|
||||
dst = resourceAttrs
|
||||
}
|
||||
|
||||
for i := range rule.Sources {
|
||||
src := &rule.Sources[i]
|
||||
sourceKey, isResource := strings.CutPrefix(src.Key, resourceSourcePrefix)
|
||||
|
||||
from := spanAttrs
|
||||
if isResource {
|
||||
from = resourceAttrs
|
||||
}
|
||||
val, ok := from[sourceKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
dst[rule.Target] = val
|
||||
if src.Action == SpanMapperOperationMove.StringValue() {
|
||||
delete(from, sourceKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
159
pkg/types/spantypes/spanmappersimulator_test.go
Normal file
159
pkg/types/spantypes/spanmappersimulator_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func simGroup(name string, attrCond, resCond []string, mappers ...*SpanMapper) *SpanMapperGroupWithMappers {
|
||||
return &SpanMapperGroupWithMappers{
|
||||
Group: &SpanMapperGroup{
|
||||
Name: name,
|
||||
Condition: SpanMapperGroupCondition{Attributes: attrCond, Resource: resCond},
|
||||
Enabled: true,
|
||||
},
|
||||
Mappers: mappers,
|
||||
}
|
||||
}
|
||||
|
||||
func simMapper(target string, ctx FieldContext, sources ...SpanMapperSource) *SpanMapper {
|
||||
return &SpanMapper{
|
||||
Name: target,
|
||||
FieldContext: ctx,
|
||||
Config: SpanMapperConfig{Sources: sources},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func simAttrSrc(key string, op SpanMapperOperation, priority int) SpanMapperSource {
|
||||
return SpanMapperSource{Key: key, Context: FieldContextSpanAttribute, Operation: op, Priority: priority}
|
||||
}
|
||||
|
||||
func simResSrc(key string, op SpanMapperOperation, priority int) SpanMapperSource {
|
||||
return SpanMapperSource{Key: key, Context: FieldContextResource, Operation: op, Priority: priority}
|
||||
}
|
||||
|
||||
func TestSimulate_MatchInSpanAttrs(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("llm", []string{"model"}, nil,
|
||||
simMapper("gen_ai.request.model", FieldContextSpanAttribute,
|
||||
simAttrSrc("llm.model", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"llm.model": "gpt-4", "gen_ai.llm.model": "gpt-40"})
|
||||
|
||||
assert.Equal(t, "gpt-4", outSpan["gen_ai.request.model"])
|
||||
}
|
||||
|
||||
func TestSimulate_MatchInResourceAttrs(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("llm", nil, []string{"service.name"},
|
||||
simMapper("gen_ai.request.model", FieldContextSpanAttribute,
|
||||
simResSrc("service.name", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, map[string]any{"service.name": "my-llm-service"}, nil)
|
||||
|
||||
assert.Equal(t, "my-llm-service", outSpan["gen_ai.request.model"])
|
||||
}
|
||||
|
||||
func TestSimulate_NoMatchSkipsGroup(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("llm", []string{"model"}, nil,
|
||||
simMapper("gen_ai.request.model", FieldContextSpanAttribute,
|
||||
simAttrSrc("llm.model", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"some.other.key": "value"})
|
||||
|
||||
_, ok := outSpan["gen_ai.request.model"]
|
||||
assert.False(t, ok, "target must not be set when condition is not met")
|
||||
}
|
||||
|
||||
func TestSimulate_SourceFirstMatchWins(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("tokens", []string{"llm"}, nil,
|
||||
simMapper("gen_ai.request.tokens", FieldContextSpanAttribute,
|
||||
simAttrSrc("gen_ai.request_tokens", SpanMapperOperationCopy, 2),
|
||||
simAttrSrc("llm.tokens", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"gen_ai.request_tokens": "100", "llm.tokens": "200"})
|
||||
|
||||
assert.Equal(t, "100", outSpan["gen_ai.request.tokens"])
|
||||
}
|
||||
|
||||
func TestSimulate_SourceFallsBackToSecond(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("tokens", []string{"llm"}, nil,
|
||||
simMapper("gen_ai.request.tokens", FieldContextSpanAttribute,
|
||||
simAttrSrc("gen_ai.request_tokens", SpanMapperOperationCopy, 2),
|
||||
simAttrSrc("llm.tokens", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"llm.tokens": "200"})
|
||||
|
||||
assert.Equal(t, "200", outSpan["gen_ai.request.tokens"])
|
||||
}
|
||||
|
||||
func TestSimulate_ActionMove(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("input", []string{"gen_ai"}, nil,
|
||||
simMapper("gen_ai.request.input", FieldContextSpanAttribute,
|
||||
simAttrSrc("gen_ai.input", SpanMapperOperationMove, 1)),
|
||||
),
|
||||
}
|
||||
_, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"gen_ai.input": "hello"})
|
||||
|
||||
assert.Equal(t, "hello", outSpan["gen_ai.request.input"])
|
||||
_, srcPresent := outSpan["gen_ai.input"]
|
||||
assert.False(t, srcPresent, "source key must be deleted when action=move")
|
||||
}
|
||||
|
||||
func TestSimulate_WriteToResourceContext(t *testing.T) {
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("llm", []string{"llm"}, nil,
|
||||
simMapper("gen_ai.request.model", FieldContextResource,
|
||||
simAttrSrc("llm.model", SpanMapperOperationCopy, 1)),
|
||||
),
|
||||
}
|
||||
outResource, outSpan := SimulateMappingForAttributes(groups, nil, map[string]any{"llm.model": "gpt-4"})
|
||||
|
||||
assert.Equal(t, "gpt-4", outResource["gen_ai.request.model"], "target must be written to resource attributes")
|
||||
_, inSpan := outSpan["gen_ai.request.model"]
|
||||
assert.False(t, inSpan)
|
||||
}
|
||||
|
||||
func TestSimulate_DisabledGroupsAndMappersSkipped(t *testing.T) {
|
||||
disabledGroup := simGroup("g1", []string{"llm"}, nil,
|
||||
simMapper("gen_ai.request.model", FieldContextSpanAttribute,
|
||||
simAttrSrc("llm.model", SpanMapperOperationCopy, 1)))
|
||||
disabledGroup.Group.Enabled = false
|
||||
|
||||
_, outSpan := SimulateMappingForAttributes([]*SpanMapperGroupWithMappers{disabledGroup}, nil, map[string]any{"llm.model": "gpt-4"})
|
||||
|
||||
_, ok := outSpan["gen_ai.request.model"]
|
||||
assert.False(t, ok, "disabled groups must not be evaluated")
|
||||
}
|
||||
|
||||
func TestSimulate_NoMappingsReturnsInputUnchanged(t *testing.T) {
|
||||
outResource, outSpan := SimulateMappingForAttributes(nil, map[string]any{"host.name": "h1"}, map[string]any{"model": "gpt-5"})
|
||||
|
||||
assert.Equal(t, map[string]any{"host.name": "h1"}, outResource, "resource attributes returned unchanged")
|
||||
assert.Equal(t, map[string]any{"model": "gpt-5"}, outSpan, "span attributes returned unchanged")
|
||||
}
|
||||
|
||||
func TestSimulate_DoesNotMutateInput(t *testing.T) {
|
||||
input := map[string]any{"gen_ai.input": "hi"}
|
||||
groups := []*SpanMapperGroupWithMappers{
|
||||
simGroup("input", []string{"gen_ai"}, nil,
|
||||
simMapper("gen_ai.request.input", FieldContextSpanAttribute,
|
||||
simAttrSrc("gen_ai.input", SpanMapperOperationMove, 1))),
|
||||
}
|
||||
_, _ = SimulateMappingForAttributes(groups, nil, input)
|
||||
|
||||
// Original input map must be untouched (move would have deleted the key).
|
||||
_, ok := input["gen_ai.input"]
|
||||
assert.True(t, ok, "input map must not be mutated by the preview")
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MetadataStore is the interface for the telemetry metadata store.
|
||||
@@ -27,12 +26,12 @@ type MetadataStore interface {
|
||||
GetAllValues(ctx context.Context, fieldValueSelector *FieldValueSelector) (*TelemetryFieldValues, bool, error)
|
||||
|
||||
// FetchTemporality fetches the temporality for metric
|
||||
FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error)
|
||||
FetchTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error)
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics
|
||||
FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error)
|
||||
FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, 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)
|
||||
FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error)
|
||||
|
||||
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
|
||||
ListLogsJSONIndexes(ctx context.Context, filters ...string) ([]TelemetryFieldKeySkipIndex, error)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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.
|
||||
@@ -17,7 +16,6 @@ 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
|
||||
@@ -308,7 +306,7 @@ func (m *MockMetadataStore) SetAllValues(lookupKey string, values *telemetrytype
|
||||
}
|
||||
|
||||
// FetchTemporality fetches the temporality for a metric.
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricName string) (metrictypes.Temporality, error) {
|
||||
if temporality, exists := m.TemporalityMap[metricName]; exists {
|
||||
return temporality, nil
|
||||
}
|
||||
@@ -316,7 +314,7 @@ func (m *MockMetadataStore) FetchTemporality(ctx context.Context, orgID valuer.U
|
||||
}
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics.
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
@@ -331,10 +329,9 @@ func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, orgID val
|
||||
}
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics.
|
||||
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) {
|
||||
func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, 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 {
|
||||
@@ -347,12 +344,9 @@ func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, or
|
||||
} else {
|
||||
types[metricName] = metrictypes.UnspecifiedType
|
||||
}
|
||||
if m.ReducedMap[metricName] {
|
||||
reduced[metricName] = true
|
||||
}
|
||||
}
|
||||
|
||||
return temporalities, types, reduced, nil
|
||||
return temporalities, types, nil
|
||||
}
|
||||
|
||||
// SetTemporality sets the temporality for a metric in the mock store.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.3, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g1-p1-uid", "k8s.pod.name": "dup-ns-g1-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.5, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 300000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 300000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 300000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g2-p1-uid", "k8s.pod.name": "dup-ns-g2-p1", "k8s.namespace.name": "dup-ns", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.cpu.usage", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 200000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 200000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.memory.working_set", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 200000000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.pod.phase", "labels": {"k8s.pod.uid": "dup-ns-g3-p1-uid", "k8s.pod.name": "dup-ns-g3-p1", "k8s.namespace.name": "dup-ns"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
@@ -1,60 +0,0 @@
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 60.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 60.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 60.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 600.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 600.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 600.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 400.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 400.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g1-p1-uid", "k8s.pod.name": "dup-pvc-g1-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 400.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 100.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 5000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 5000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 5000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 1000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 4000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 4000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g2-p1-uid", "k8s.pod.name": "dup-pvc-g2-p1", "k8s.namespace.name": "ns-y", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-a"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 4000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 50.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 50.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 50.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 300.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 300.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 300.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 3000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 3000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 3000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g3-p1-uid", "k8s.pod.name": "dup-pvc-g3-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup", "k8s.cluster.name": "cluster-b"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2500.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.available", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 200.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 200.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.capacity", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 200.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.free", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 0.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:00:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:02:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
{"metric_name": "k8s.volume.inodes.used", "labels": {"k8s.persistentvolumeclaim.name": "dup-pvc", "k8s.pod.uid": "dup-pvc-g4-p1-uid", "k8s.pod.name": "dup-pvc-g4-p1", "k8s.namespace.name": "ns-x", "k8s.node.name": "node-x", "k8s.statefulset.name": "ss-dup"}, "timestamp": "2025-01-10T10:04:00+00:00", "value": 2000.0, "temporality": "Unspecified", "type_": "Gauge", "is_monotonic": false}
|
||||
@@ -317,94 +317,23 @@ def test_namespaces_pod_phase_aggregation(
|
||||
}
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
"namespaceCPU",
|
||||
"namespaceMemory",
|
||||
}
|
||||
|
||||
|
||||
def _phase(pending=0, running=0, succeeded=0, failed=0, unknown=0) -> dict:
|
||||
return {"pending": pending, "running": running, "succeeded": succeeded, "failed": failed, "unknown": unknown}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scenario",
|
||||
"group_key,expected_running",
|
||||
[
|
||||
# Explicit groupBy=[k8s.namespace.name]: one record per namespace,
|
||||
# namespaceName populated (namespaces.go:27-30), response grouped_list.
|
||||
# Each namespace has 1 running pod.
|
||||
# groupBy=[k8s.namespace.name]: one record per namespace, namespaceName
|
||||
# populated (namespaces.go:27-30). Each namespace has 1 running pod.
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "namespaces_groupby.jsonl",
|
||||
"group_by": "k8s.namespace.name",
|
||||
"filter": None,
|
||||
"group_meta_keys": ["k8s.namespace.name"],
|
||||
"expected_type": "grouped_list",
|
||||
"groups": {
|
||||
"gb-ns-1": {"namespaceName": "gb-ns-1", "podCountsByPhase": _phase(running=1)},
|
||||
"gb-ns-2": {"namespaceName": "gb-ns-2", "podCountsByPhase": _phase(running=1)},
|
||||
"gb-ns-3": {"namespaceName": "gb-ns-3", "podCountsByPhase": _phase(running=1)},
|
||||
"gb-ns-4": {"namespaceName": "gb-ns-4", "podCountsByPhase": _phase(running=1)},
|
||||
},
|
||||
},
|
||||
"k8s.namespace.name",
|
||||
{"gb-ns-1": 1, "gb-ns-2": 1, "gb-ns-3": 1, "gb-ns-4": 1},
|
||||
id="namespace_name",
|
||||
),
|
||||
# Explicit groupBy=[k8s.cluster.name]: aggregated across each cluster's 2
|
||||
# namespaces, namespaceName empty, response grouped_list. 2 running each.
|
||||
# groupBy=[k8s.cluster.name]: aggregated across each cluster's 2
|
||||
# namespaces, namespaceName empty. Each cluster has 2 x 1 = 2 running pods.
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "namespaces_groupby.jsonl",
|
||||
"group_by": "k8s.cluster.name",
|
||||
"filter": None,
|
||||
"group_meta_keys": ["k8s.cluster.name"],
|
||||
"expected_type": "grouped_list",
|
||||
"groups": {
|
||||
"gb-cluster-a": {"namespaceName": "", "podCountsByPhase": _phase(running=2)},
|
||||
"gb-cluster-b": {"namespaceName": "", "podCountsByPhase": _phase(running=2)},
|
||||
},
|
||||
},
|
||||
"k8s.cluster.name",
|
||||
{"gb-cluster-a": 2, "gb-cluster-b": 2},
|
||||
id="cluster",
|
||||
),
|
||||
# Default groupBy (no groupBy in request) => [k8s.namespace.name,
|
||||
# k8s.cluster.name] (module.go ListNamespaces), response list. Namespaces
|
||||
# are cluster-scoped, so a same-named namespace must NOT collapse across
|
||||
# clusters; the empty-cluster group (k8s.cluster.name label absent on the
|
||||
# source pods) must appear as its own row with real metrics, not be dropped.
|
||||
# Single pod per group => SpaceAggregationSum == seeded value.
|
||||
# Fails on the pre-cluster default (name only) — the three groups would
|
||||
# collapse into one summed row.
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "namespaces_same_name_across_clusters.jsonl",
|
||||
"group_by": None,
|
||||
"filter": "k8s.namespace.name = 'dup-ns'",
|
||||
"group_meta_keys": ["k8s.namespace.name", "k8s.cluster.name"],
|
||||
"expected_type": "list",
|
||||
"groups": {
|
||||
("dup-ns", "cluster-a"): {
|
||||
"namespaceName": "dup-ns",
|
||||
"namespaceCPU": 0.3,
|
||||
"namespaceMemory": 100000000.0,
|
||||
"podCountsByPhase": _phase(running=1),
|
||||
},
|
||||
("dup-ns", "cluster-b"): {
|
||||
"namespaceName": "dup-ns",
|
||||
"namespaceCPU": 0.5,
|
||||
"namespaceMemory": 300000000.0,
|
||||
"podCountsByPhase": _phase(failed=1),
|
||||
},
|
||||
# empty-cluster group: k8s.cluster.name label absent on the source pods.
|
||||
("dup-ns", ""): {
|
||||
"namespaceName": "dup-ns",
|
||||
"namespaceCPU": 0.1,
|
||||
"namespaceMemory": 200000000.0,
|
||||
"podCountsByPhase": _phase(pending=1),
|
||||
},
|
||||
},
|
||||
},
|
||||
id="default_disambiguates_cluster",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_namespaces_groupby(
|
||||
@@ -412,64 +341,55 @@ def test_namespaces_groupby(
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
scenario: dict,
|
||||
group_key: str,
|
||||
expected_running: dict,
|
||||
) -> None:
|
||||
"""groupBy determines row identity. Explicit groupBy returns one grouped_list
|
||||
record per distinct group (namespaceName populated only when grouping by
|
||||
k8s.namespace.name; namespaces.go:27-30). With no groupBy the default is
|
||||
[k8s.namespace.name, k8s.cluster.name] (module.go ListNamespaces), so
|
||||
same-named namespaces across clusters stay as separate, un-collapsed list rows
|
||||
(incl. an absent-cluster group keyed by ""). meta always surfaces the grouping
|
||||
key(s)."""
|
||||
"""groupBy returns one record per distinct group with aggregated pod-phase
|
||||
counts. namespaceName is populated only when grouping by k8s.namespace.name
|
||||
(namespaces.go:27-30 list-vs-grouped branch); meta surfaces the groupBy key."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path(f"inframonitoring/{scenario['fixture']}"),
|
||||
get_testdata_file_path("inframonitoring/namespaces_groupby.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
|
||||
body: dict = {
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
}
|
||||
if scenario["group_by"] is not None:
|
||||
body["groupBy"] = [{"name": scenario["group_by"], "fieldDataType": "string", "fieldContext": "resource"}]
|
||||
if scenario["filter"] is not None:
|
||||
body["filter"] = {"expression": scenario["filter"]}
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json=body,
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [
|
||||
{
|
||||
"name": group_key,
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
}
|
||||
],
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
data = response.json()["data"]
|
||||
assert data["total"] == len(expected_running)
|
||||
|
||||
groups = scenario["groups"]
|
||||
meta_keys = scenario["group_meta_keys"]
|
||||
assert data["type"] == scenario["expected_type"]
|
||||
assert data["total"] == len(groups)
|
||||
group_of = lambda r: r["namespaceName"] if group_key == "k8s.namespace.name" else r["meta"][group_key] # noqa: E731 # pylint: disable=unnecessary-lambda-assignment
|
||||
by_group = {group_of(r): r for r in data["records"]}
|
||||
assert set(by_group.keys()) == set(expected_running.keys())
|
||||
|
||||
def _gid(rec: dict):
|
||||
vals = [rec["meta"][k] for k in meta_keys]
|
||||
return vals[0] if len(vals) == 1 else tuple(vals)
|
||||
|
||||
by_group = {_gid(r): r for r in data["records"]}
|
||||
assert set(by_group.keys()) == set(groups.keys())
|
||||
|
||||
for gid, exp in groups.items():
|
||||
rec = by_group[gid]
|
||||
for k in meta_keys:
|
||||
assert k in rec["meta"], rec["meta"]
|
||||
for field, val in exp.items():
|
||||
if field in _GROUPBY_FLOAT_FIELDS:
|
||||
assert compare_values(rec[field], val, 1e-6), f"{gid}.{field}: got {rec[field]}, expected {val}"
|
||||
else:
|
||||
assert rec[field] == val, f"{gid}.{field}: got {rec[field]}, expected {val}"
|
||||
for group, running in expected_running.items():
|
||||
rec = by_group[group]
|
||||
# namespaceName populated per namespace when grouping by k8s.namespace.name,
|
||||
# empty otherwise.
|
||||
assert rec["namespaceName"] == (group if group_key == "k8s.namespace.name" else "")
|
||||
assert rec["podCountsByPhase"]["running"] == running
|
||||
for other in ("pending", "succeeded", "failed", "unknown"):
|
||||
assert rec["podCountsByPhase"][other] == 0
|
||||
assert group_key in rec["meta"], rec["meta"]
|
||||
|
||||
|
||||
def test_namespaces_pagination(
|
||||
|
||||
@@ -376,111 +376,23 @@ def test_volumes_non_pvc_volume_filtered(
|
||||
assert rec["persistentVolumeClaimName"] == "np-real-pvc"
|
||||
|
||||
|
||||
# Float record fields compared with tolerance; everything else compared with ==.
|
||||
_GROUPBY_FLOAT_FIELDS = {
|
||||
"volumeAvailable",
|
||||
"volumeCapacity",
|
||||
"volumeUsage",
|
||||
"volumeInodes",
|
||||
"volumeInodesFree",
|
||||
"volumeInodesUsed",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scenario",
|
||||
"group_key,expected_groups",
|
||||
[
|
||||
# Explicit groupBy=[k8s.persistentvolumeclaim.name]: one record per PVC,
|
||||
# persistentVolumeClaimName populated (volumes.go:26-29), response grouped_list.
|
||||
# groupBy=[k8s.persistentvolumeclaim.name]: one record per PVC,
|
||||
# persistentVolumeClaimName populated (volumes.go:26-29).
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "volumes_groupby.jsonl",
|
||||
"group_by": "k8s.persistentvolumeclaim.name",
|
||||
"filter": None,
|
||||
"group_meta_keys": ["k8s.persistentvolumeclaim.name"],
|
||||
"expected_type": "grouped_list",
|
||||
"groups": {
|
||||
"gb-pvc-a1": {"persistentVolumeClaimName": "gb-pvc-a1"},
|
||||
"gb-pvc-a2": {"persistentVolumeClaimName": "gb-pvc-a2"},
|
||||
"gb-pvc-b1": {"persistentVolumeClaimName": "gb-pvc-b1"},
|
||||
"gb-pvc-b2": {"persistentVolumeClaimName": "gb-pvc-b2"},
|
||||
},
|
||||
},
|
||||
"k8s.persistentvolumeclaim.name",
|
||||
{"gb-pvc-a1", "gb-pvc-a2", "gb-pvc-b1", "gb-pvc-b2"},
|
||||
id="pvc_name",
|
||||
),
|
||||
# Explicit groupBy=[k8s.namespace.name]: aggregated per namespace,
|
||||
# persistentVolumeClaimName cleared, response grouped_list.
|
||||
# groupBy=[k8s.namespace.name]: aggregated per namespace,
|
||||
# persistentVolumeClaimName cleared (custom-groupBy branch).
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "volumes_groupby.jsonl",
|
||||
"group_by": "k8s.namespace.name",
|
||||
"filter": None,
|
||||
"group_meta_keys": ["k8s.namespace.name"],
|
||||
"expected_type": "grouped_list",
|
||||
"groups": {
|
||||
"gb-ns-a": {"persistentVolumeClaimName": ""},
|
||||
"gb-ns-b": {"persistentVolumeClaimName": ""},
|
||||
},
|
||||
},
|
||||
"k8s.namespace.name",
|
||||
{"gb-ns-a", "gb-ns-b"},
|
||||
id="namespace",
|
||||
),
|
||||
# Default groupBy (no groupBy in request) => [k8s.persistentvolumeclaim.name,
|
||||
# k8s.namespace.name, k8s.cluster.name] (module.go ListVolumes), response list.
|
||||
# Same PVC name must NOT collapse across namespaces OR clusters; the
|
||||
# empty-cluster group (k8s.cluster.name label absent on the source series)
|
||||
# must appear as its own row with real metrics, not be dropped.
|
||||
# Single series per group => SpaceAggregationSum == seeded value.
|
||||
# Fails on the pre-cluster default (name+ns) — the three ns-x groups would
|
||||
# collapse into one summed row.
|
||||
pytest.param(
|
||||
{
|
||||
"fixture": "volumes_same_name_across_ns_and_clusters.jsonl",
|
||||
"group_by": None,
|
||||
"filter": "k8s.persistentvolumeclaim.name = 'dup-pvc'",
|
||||
"group_meta_keys": ["k8s.persistentvolumeclaim.name", "k8s.namespace.name", "k8s.cluster.name"],
|
||||
"expected_type": "list",
|
||||
"groups": {
|
||||
("dup-pvc", "ns-x", "cluster-a"): {
|
||||
"persistentVolumeClaimName": "dup-pvc",
|
||||
"volumeCapacity": 100.0,
|
||||
"volumeAvailable": 60.0,
|
||||
"volumeUsage": 40.0,
|
||||
"volumeInodes": 1000.0,
|
||||
"volumeInodesFree": 600.0,
|
||||
"volumeInodesUsed": 400.0,
|
||||
},
|
||||
("dup-pvc", "ns-y", "cluster-a"): {
|
||||
"persistentVolumeClaimName": "dup-pvc",
|
||||
"volumeCapacity": 500.0,
|
||||
"volumeAvailable": 100.0,
|
||||
"volumeUsage": 400.0,
|
||||
"volumeInodes": 5000.0,
|
||||
"volumeInodesFree": 1000.0,
|
||||
"volumeInodesUsed": 4000.0,
|
||||
},
|
||||
("dup-pvc", "ns-x", "cluster-b"): {
|
||||
"persistentVolumeClaimName": "dup-pvc",
|
||||
"volumeCapacity": 300.0,
|
||||
"volumeAvailable": 50.0,
|
||||
"volumeUsage": 250.0,
|
||||
"volumeInodes": 3000.0,
|
||||
"volumeInodesFree": 500.0,
|
||||
"volumeInodesUsed": 2500.0,
|
||||
},
|
||||
# empty-cluster group: k8s.cluster.name label absent on the source series.
|
||||
("dup-pvc", "ns-x", ""): {
|
||||
"persistentVolumeClaimName": "dup-pvc",
|
||||
"volumeCapacity": 200.0,
|
||||
"volumeAvailable": 0.0,
|
||||
"volumeUsage": 200.0,
|
||||
"volumeInodes": 2000.0,
|
||||
"volumeInodesFree": 0.0,
|
||||
"volumeInodesUsed": 2000.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
id="default_disambiguates_ns_and_cluster",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_volumes_groupby(
|
||||
@@ -488,64 +400,51 @@ def test_volumes_groupby(
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token,
|
||||
insert_metrics,
|
||||
scenario: dict,
|
||||
group_key: str,
|
||||
expected_groups: set,
|
||||
) -> None:
|
||||
"""groupBy determines row identity. Explicit groupBy returns one grouped_list
|
||||
record per distinct group (persistentVolumeClaimName populated only when
|
||||
grouping by k8s.persistentvolumeclaim.name; volumes.go:26-29). With no groupBy
|
||||
the default is [k8s.persistentvolumeclaim.name, k8s.namespace.name,
|
||||
k8s.cluster.name] (module.go ListVolumes), so same-named PVCs across
|
||||
namespaces/clusters stay as separate, un-collapsed list rows (incl. an
|
||||
absent-cluster group keyed by ""). meta always surfaces the grouping key(s)."""
|
||||
"""groupBy returns one record per distinct group. persistentVolumeClaimName
|
||||
is populated only when grouping by k8s.persistentvolumeclaim.name
|
||||
(volumes.go:26-29 list-vs-grouped branch); meta surfaces the groupBy key."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
insert_metrics(
|
||||
Metrics.load_from_file(
|
||||
get_testdata_file_path(f"inframonitoring/{scenario['fixture']}"),
|
||||
get_testdata_file_path("inframonitoring/volumes_groupby.jsonl"),
|
||||
base_time=now - timedelta(minutes=4),
|
||||
)
|
||||
)
|
||||
|
||||
body: dict = {
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
}
|
||||
if scenario["group_by"] is not None:
|
||||
body["groupBy"] = [{"name": scenario["group_by"], "fieldDataType": "string", "fieldContext": "resource"}]
|
||||
if scenario["filter"] is not None:
|
||||
body["filter"] = {"expression": scenario["filter"]}
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(ENDPOINT),
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json=body,
|
||||
json={
|
||||
"start": int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
"end": int(now.timestamp() * 1000),
|
||||
"limit": 50,
|
||||
"groupBy": [
|
||||
{
|
||||
"name": group_key,
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
}
|
||||
],
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
data = response.json()["data"]
|
||||
assert data["total"] == len(expected_groups)
|
||||
|
||||
groups = scenario["groups"]
|
||||
meta_keys = scenario["group_meta_keys"]
|
||||
assert data["type"] == scenario["expected_type"]
|
||||
assert data["total"] == len(groups)
|
||||
is_pvc_group = group_key == "k8s.persistentvolumeclaim.name"
|
||||
group_of = lambda r: r["persistentVolumeClaimName"] if is_pvc_group else r["meta"][group_key] # noqa: E731 # pylint: disable=unnecessary-lambda-assignment
|
||||
by_group = {group_of(r): r for r in data["records"]}
|
||||
assert set(by_group.keys()) == expected_groups
|
||||
|
||||
def _gid(rec: dict):
|
||||
vals = [rec["meta"][k] for k in meta_keys]
|
||||
return vals[0] if len(vals) == 1 else tuple(vals)
|
||||
|
||||
by_group = {_gid(r): r for r in data["records"]}
|
||||
assert set(by_group.keys()) == set(groups.keys())
|
||||
|
||||
for gid, exp in groups.items():
|
||||
rec = by_group[gid]
|
||||
for k in meta_keys:
|
||||
assert k in rec["meta"], rec["meta"]
|
||||
for field, val in exp.items():
|
||||
if field in _GROUPBY_FLOAT_FIELDS:
|
||||
assert compare_values(rec[field], val, 1e-6), f"{gid}.{field}: got {rec[field]}, expected {val}"
|
||||
else:
|
||||
assert rec[field] == val, f"{gid}.{field}: got {rec[field]}, expected {val}"
|
||||
for group, rec in by_group.items():
|
||||
# persistentVolumeClaimName populated per PVC when grouping by it, empty otherwise.
|
||||
assert rec["persistentVolumeClaimName"] == (group if is_pvc_group else "")
|
||||
assert group_key in rec["meta"], rec["meta"]
|
||||
|
||||
|
||||
def test_volumes_pagination(
|
||||
|
||||
@@ -614,7 +614,7 @@ def test_histogram_p90_returns_warning_outside_data_window(
|
||||
assert warnings[0]["message"].startswith(f"no data found for the metric {metric_name}")
|
||||
|
||||
|
||||
def test_non_existent_metrics_returns_warning(
|
||||
def test_non_existent_metrics_returns_404(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
@@ -635,11 +635,9 @@ def test_non_existent_metrics_returns_warning(
|
||||
|
||||
start_2h = int((now - timedelta(hours=2)).timestamp() * 1000)
|
||||
response = make_query_request(signoz, token, start_2h, end_ms, [query])
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND
|
||||
|
||||
data = response.json()
|
||||
warnings = get_all_warnings(data)
|
||||
assert any("whatevergoennnsgoeshere" in w["message"] and "has never been received" in w["message"] for w in warnings), f"expected never-seen metric warning, got: {warnings}"
|
||||
assert get_error_message(response.json()) == "could not find the metric whatevergoennnsgoeshere"
|
||||
|
||||
|
||||
def test_non_existent_internal_metrics_returns_no_warning(
|
||||
|
||||
Reference in New Issue
Block a user