Compare commits

..

51 Commits

Author SHA1 Message Date
Srikanth Chekuri
0193b4f9bf Merge branch 'main' into feat/services_use_span_metrics 2026-01-23 04:17:43 +05:30
nikhilmantri0902
ce05187ee5 chore: added spanMetricsSingleBucketStep correction 2026-01-19 17:59:54 +05:30
nikhilmantri0902
b4e3b7f641 chore: added selection criteria for MetricScopeFieldDefinitions 2026-01-16 18:27:02 +05:30
nikhilmantri0902
6a6809fef3 chore: remove average latency 2026-01-16 16:35:59 +05:30
nikhilmantri0902
1ffbc3bd4b chore: removed the format table option from one query 2026-01-16 16:21:15 +05:30
nikhilmantri0902
5af8a02fd8 chore: removed unnecessary comment and added no cache true for span metrics 2026-01-16 15:18:45 +05:30
Nikhil Mantri
034599bbeb Merge branch 'main' into feat/services_use_span_metrics 2026-01-16 13:09:25 +05:30
Srikanth Chekuri
0e731df10a Merge branch 'main' into feat/services_use_span_metrics 2026-01-15 00:49:09 +05:30
Nikhil Mantri
43e5a3d96c Merge branch 'main' into feat/services_use_span_metrics 2026-01-13 15:23:46 +05:30
Srikanth Chekuri
17ff4a1841 Merge branch 'main' into feat/services_use_span_metrics 2026-01-09 23:59:01 +05:30
nikhilmantri0902
5c61aa41f6 chore: openapi fix 2026-01-08 14:56:05 +05:30
Srikanth Chekuri
b514ce5b31 Merge branch 'main' into feat/services_use_span_metrics 2026-01-08 02:04:30 +05:30
nikhilmantri0902
5ffce5de13 chore: small corrections 2026-01-07 17:17:54 +05:30
nikhilmantri0902
cd3838dfb7 chore: periodSeconds calculations correction 2026-01-07 17:15:24 +05:30
nikhilmantri0902
01071cd3e7 chore: small corrections 2026-01-07 17:11:04 +05:30
nikhilmantri0902
1feee4a49f chore: removed unused method 2026-01-07 17:04:27 +05:30
nikhilmantri0902
5d067ed413 chore: added flagger depdendency to services module 2026-01-07 16:55:18 +05:30
nikhilmantri0902
4fc00d2a72 chore: resolved conflicts 2026-01-07 16:24:21 +05:30
nikhilmantri0902
7d1af84a06 chore: removed unnecessary comment 2025-12-31 15:25:23 +05:30
Nikhil Mantri
683d2eebd4 Merge branch 'main' into feat/services_use_span_metrics 2025-12-31 15:15:04 +05:30
nikhilmantri0902
82419a64f8 chore: added large step interval for pxx computation 2025-12-31 13:32:03 +05:30
nikhilmantri0902
3aa5c53ca4 chore: services rate calculation logic change 2025-12-30 16:44:38 +05:30
nikhilmantri0902
608e5d64fe chore: todo added for is averaging the best way to calculate p99 2025-12-30 14:39:00 +05:30
nikhilmantri0902
629b93cc14 chore: service metrics powering homepage component 2025-12-30 12:53:29 +05:30
nikhilmantri0902
3e97c41799 chore: removed unused func 2025-12-29 21:23:47 +05:30
nikhilmantri0902
f775a03d2c chore: removed fmt.Prints 2025-12-29 21:09:32 +05:30
nikhilmantri0902
892ef538bc chore: frontend latency graph unit fix 2025-12-29 20:33:21 +05:30
nikhilmantri0902
5c08458f48 chore: fix for calls and error in key operations table 2025-12-29 20:07:36 +05:30
nikhilmantri0902
e671e9d647 chore: methods rearrangement 2025-12-29 19:18:13 +05:30
nikhilmantri0902
1013cb727c chore: logic change 2025-12-29 17:29:15 +05:30
nikhilmantri0902
932a0c4a83 chore: env variable initialization in the module itself 2025-12-29 16:25:44 +05:30
nikhilmantri0902
426964d489 chore: replace rate -> increase for span metrics num errors and num calls 2025-12-29 15:56:31 +05:30
nikhilmantri0902
c66ad5738e chore: use span metrics 2025-12-28 10:56:20 +05:30
nikhilmantri0902
634e8923c7 chore: add call rate fix 2025-12-27 13:39:02 +05:30
nikhilmantri0902
69855a1136 chore: rate->increase in top ops and entry point ops 2025-12-26 16:09:05 +05:30
nikhilmantri0902
cc1513c7e3 chore: top Ops and entry point calcula
tion fix
2025-12-26 15:33:33 +05:30
nikhilmantri0902
ba60d9ee50 chore: removed method 2025-12-26 12:38:04 +05:30
nikhilmantri0902
261622616f chore: removed log 2025-12-26 12:36:26 +05:30
nikhilmantri0902
592f5a7e11 chore: errorFilterExpr fix 2025-12-26 12:20:51 +05:30
nikhilmantri0902
dc390c813d chore: rate fix 2025-12-26 11:58:03 +05:30
nikhilmantri0902
ba6690cddb chore: lint fix 2025-12-26 11:20:02 +05:30
nikhilmantri0902
3a45ae12d1 chore: frontend change 2025-12-25 19:03:46 +05:30
nikhilmantri0902
5aef65fc11 chore: backend code modification for top_operations and entrypoint_operations 2025-12-25 18:53:41 +05:30
nikhilmantri0902
efd2d961d0 chore: redesign 2025-12-25 17:52:14 +05:30
nikhilmantri0902
ec618a00ce chore: redesign 2025-12-25 17:51:46 +05:30
nikhilmantri0902
c896753d3a chore: latency to nano seconds 2025-12-25 17:43:10 +05:30
nikhilmantri0902
cc616602eb chore: fix 2025-12-25 11:25:03 +05:30
nikhilmantri0902
255847ac61 chore: added mapping functions to handle span metrics 2025-12-24 19:33:09 +05:30
nikhilmantri0902
a1e4461865 chore: isMetricScopeField method + buildMetricScopeCondition method 2025-12-24 14:48:20 +05:30
nikhilmantri0902
146fd9892b chore: isMetricScopeField method + buildMetricScopeCondition method 2025-12-24 14:43:59 +05:30
nikhilmantri0902
b9ecdcf210 chore: added selector for isTopLevelOperation 2025-12-24 14:26:49 +05:30
76 changed files with 1268 additions and 2512 deletions

View File

@@ -12,7 +12,6 @@ linters:
- misspell
- nilnil
- sloglint
- wastedassign
- unparam
- unused
settings:

View File

@@ -101,6 +101,7 @@ go-test: ## Runs go unit tests
.PHONY: go-run-community
go-run-community: ## Runs the community go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_FLAGGER_CONFIG_BOOLEAN_USE__SPAN__METRICS=true \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \

View File

@@ -38,7 +38,7 @@ module.exports = {
'import', // Import/export linting
'sonarjs', // Code quality/complexity
// TODO: Uncomment after running: yarn add -D eslint-plugin-spellcheck
// 'spellcheck', // Correct spellings
// 'spellcheck',
],
settings: {
react: {
@@ -60,18 +60,12 @@ module.exports = {
'no-debugger': 'error', // Disallows debugger statements in production code
curly: 'error', // Requires curly braces for all control statements
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TODO: Enable after fixing ~15 console.log statements
// 'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TypeScript rules
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
'@typescript-eslint/no-unused-vars': [
// Disallows unused variables/args
'error',
{
argsIgnorePattern: '^_', // Allows unused args prefixed with _ (e.g., _unusedParam)
varsIgnorePattern: '^_', // Allows unused vars prefixed with _ (e.g., _unusedVar)
},
],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn', // Warns when using 'any' type (consider upgrading to error)
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies

View File

@@ -41,7 +41,7 @@ export const getConsumerLagDetails = async (
> => {
const { detailType, ...restProps } = props;
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${detailType}`,
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{
...restProps,
},

View File

@@ -43,17 +43,16 @@ export const omitIdFromQuery = (query: Query | null): any => ({
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id: _aggregateAttributeId, ...rest } =
queryData.aggregateAttribute || {};
const { id, ...rest } = queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
const { id, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters?.items?.map((item) => {
const { id: _itemId, ...newItem } = item;
const { id, ...newItem } = item;
if (item.key) {
const { id: _keyId, ...rest } = item.key;
const { id, ...rest } = item.key;
return {
...newItem,
key: rest,

View File

@@ -45,6 +45,7 @@ function Pre({
}
function Code({
node,
inline,
className = 'blog-code',
children,

View File

@@ -29,6 +29,7 @@ import {
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
@@ -207,6 +208,8 @@ function QuerySearch({
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
const { handleRunQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const dynamicVariables = useMemo(

View File

@@ -87,7 +87,7 @@ function TraceOperatorEditor({
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery } = useQueryBuilder();
const { currentQuery, handleRunQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>

View File

@@ -5,6 +5,7 @@ import { EditorView } from '@uiw/react-codemirror';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -120,8 +121,13 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
// Note: We're NOT mocking CodeMirror here - using the real component
// This provides integration testing with the actual CodeMirror editor
const handleRunQueryMock = ((UseQBModule as unknown) as {
handleRunQuery: jest.MockedFunction<() => void>;
}).handleRunQuery;
const SAMPLE_KEY_TYPING = 'http.';
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
describe('QuerySearch (Integration with Real CodeMirror)', () => {

View File

@@ -796,12 +796,12 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
});
const {
aggregateAttribute: _aggregateAttribute,
aggregateOperator: _aggregateOperator,
timeAggregation: _timeAggregation,
spaceAggregation: _spaceAggregation,
reduceTo: _reduceTo,
filters: _filters,
aggregateAttribute,
aggregateOperator,
timeAggregation,
spaceAggregation,
reduceTo,
filters,
...retainedQuery
} = query;

View File

@@ -1,5 +1,14 @@
import './Slider.styles.scss';
export default function Slider(): JSX.Element {
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
interface ISliderProps {
filter: IQuickFiltersConfig;
}
// not needed for now build when required
export default function Slider(props: ISliderProps): JSX.Element {
const { filter } = props;
console.log(filter);
return <div>Slider</div>;
}

View File

@@ -303,9 +303,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
);
case FiltersType.DURATION:
return <Duration filter={filter} onFilterChange={onFilterChange} />;
return (
<Duration
filter={filter}
onFilterChange={onFilterChange}
source={source}
/>
);
case FiltersType.SLIDER:
return <Slider />;
return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';

View File

@@ -73,7 +73,7 @@ export function sanitizeDashboardData(
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue: _selectedValue, ...rest } = value;
const { selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},

View File

@@ -9,11 +9,10 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { Atom, Terminal } from 'lucide-react';
import { Atom, Play, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -166,8 +165,9 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<RunQueryBtn
onStageRunQuery={(): void => {
<Button
type="primary"
onClick={(): void => {
runQuery();
logEvent('Alert: Stage and run query', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
@@ -176,7 +176,11 @@ function QuerySection({
queryType: queryCategory,
});
}}
/>
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={tabs}
@@ -195,7 +199,14 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<RunQueryBtn onStageRunQuery={runQuery} />
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={items}

View File

@@ -1,13 +1,7 @@
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import {
getQueryRangeRequestData,
getServiceListFromQuery,
} from 'container/ServiceApplication/utils';
import { useGetQueriesRange } from 'hooks/queryBuilder/useGetQueriesRange';
import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -17,7 +11,6 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { AppState } from 'store/reducers';
@@ -30,7 +23,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -166,32 +158,6 @@ function ServiceMetrics({
[queries],
);
const [isError, setIsError] = useState(false);
const queryKey: QueryKey = useMemo(
() => [
timeRange.startTime,
timeRange.endTime,
selectedTags,
globalSelectedInterval,
],
[
timeRange.startTime,
timeRange.endTime,
selectedTags,
globalSelectedInterval,
],
);
const {
data,
isLoading: isLoadingTopLevelOperations,
isError: isErrorTopLevelOperations,
} = useGetTopLevelOperations(queryKey, {
start: timeRange.startTime * 1e6,
end: timeRange.endTime * 1e6,
});
const handleTimeIntervalChange = useCallback((value: number): void => {
const timeInterval = TIME_PICKER_OPTIONS.find(
(option) => option.value === value,
@@ -209,66 +175,29 @@ function ServiceMetrics({
});
}, []);
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}),
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
);
const dataQueries = useGetQueriesRange(
queryRangeRequestData,
ENTITY_VERSION_V4,
{
queryKey: useMemo(
() => [
`GetMetricsQueryRange-home-${globalSelectedInterval}`,
timeRange.endTime,
timeRange.startTime,
globalSelectedInterval,
],
[globalSelectedInterval, timeRange.endTime, timeRange.startTime],
),
// Fetch services data from /api/v2/services
const { data: servicesData, isLoading, isError } = useQueryService({
minTime: timeRange.startTime * 1e6, // Convert ms to nanoseconds
maxTime: timeRange.endTime * 1e6, // Convert ms to nanoseconds
selectedTime: globalSelectedInterval,
selectedTags,
options: {
keepPreviousData: true,
enabled: true,
refetchOnMount: false,
onError: () => {
setIsError(true);
},
},
);
});
const isLoading = useMemo(() => dataQueries.some((query) => query.isLoading), [
dataQueries,
const services: ServicesList[] = useMemo(() => servicesData || [], [
servicesData,
]);
const services: ServicesList[] = useMemo(
() =>
getServiceListFromQuery({
queries: dataQueries,
topLevelOperations,
isLoading,
}),
[dataQueries, topLevelOperations, isLoading],
);
const sortedServices = useMemo(
() =>
services?.sort((a, b) => {
const aUpdateAt = new Date(a.p99).getTime();
const bUpdateAt = new Date(b.p99).getTime();
return bUpdateAt - aUpdateAt;
}) || [],
services?.sort(
(a, b) =>
// p99 is already a number (nanoseconds), sort descending
b.p99 - a.p99,
) || [],
[services],
);
@@ -293,7 +222,7 @@ function ServiceMetrics({
[safeNavigate],
);
if (isLoadingTopLevelOperations || isLoading) {
if (isLoading) {
return (
<Card className="services-list-card home-data-card loading-card">
<Card.Content>
@@ -303,7 +232,7 @@ function ServiceMetrics({
);
}
if (isErrorTopLevelOperations || isError) {
if (isError) {
return (
<Card className="services-list-card home-data-card error-card">
<Card.Content>

View File

@@ -1,6 +1,14 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { Button, Flex, Input, Typography } from 'antd';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Tag,
Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';

View File

@@ -1,155 +0,0 @@
import { OPERATORS } from 'constants/queryBuilder';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import {
GraphTitle,
KeyOperationTableHeader,
MetricsType,
WidgetKeys,
} from '../constant';
import { TopOperationQueryFactoryProps } from '../Tabs/types';
import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
export const topOperationQueries = ({
servicename,
dotMetricsEnabled,
}: TopOperationQueryFactoryProps): QueryBuilderData => {
const latencyAutoCompleteData: BaseAutocompleteData = {
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
const errorRateAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozCallsTotal,
dataType: DataTypes.Float64,
type: '',
};
const numOfCallAutoCompleteData: BaseAutocompleteData = {
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
};
const latencyAndNumberOfCallAdditionalItems: TagFilterItem[] = [
{
id: '',
key: {
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
value: [servicename],
op: OPERATORS.IN,
},
];
const errorRateAdditionalItemsA: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataTypes.String,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [servicename],
},
{
id: '',
key: {
dataType: DataTypes.Int64,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: ['STATUS_CODE_ERROR'],
},
];
const errorRateAdditionalItemsB = latencyAndNumberOfCallAdditionalItems;
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataTypes.String,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
];
const autocompleteData = [
latencyAutoCompleteData,
latencyAutoCompleteData,
latencyAutoCompleteData,
errorRateAutoCompleteData,
errorRateAutoCompleteData,
numOfCallAutoCompleteData,
];
const additionalItems = [
latencyAndNumberOfCallAdditionalItems,
latencyAndNumberOfCallAdditionalItems,
latencyAndNumberOfCallAdditionalItems,
errorRateAdditionalItemsA,
errorRateAdditionalItemsB,
latencyAndNumberOfCallAdditionalItems,
];
const disabled = [false, false, false, true, true, false];
const legends = [
KeyOperationTableHeader.P50,
KeyOperationTableHeader.P90,
KeyOperationTableHeader.P99,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.NUM_OF_CALLS,
];
const timeAggregateOperators = [
MetricAggregateOperator.EMPTY,
MetricAggregateOperator.EMPTY,
MetricAggregateOperator.EMPTY,
MetricAggregateOperator.RATE,
MetricAggregateOperator.RATE,
MetricAggregateOperator.RATE,
];
const spaceAggregateOperators = [
MetricAggregateOperator.P50,
MetricAggregateOperator.P90,
MetricAggregateOperator.P99,
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
];
const expressions = ['D*100/E'];
const legendFormulas = [GraphTitle.ERROR_PERCENTAGE];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
disabled,
legends,
timeAggregateOperators,
spaceAggregateOperators,
expressions,
legendFormulas,
dataSource,
groupBy,
});
};

View File

@@ -46,7 +46,6 @@ import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel
import ServiceOverview from './Overview/ServiceOverview';
import TopLevelOperation from './Overview/TopLevelOperations';
import TopOperation from './Overview/TopOperation';
import TopOperationMetrics from './Overview/TopOperationMetrics';
import { Button, Card } from './styles';
import { IServiceName } from './types';
import {
@@ -72,10 +71,6 @@ function Application(): JSX.Element {
const urlQuery = useUrlQuery();
const { featureFlags } = useAppContext();
const isSpanMetricEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
?.active || false;
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
@@ -396,7 +391,7 @@ function Application(): JSX.Element {
<Col span={12}>
<Card>
{isSpanMetricEnabled ? <TopOperationMetrics /> : <TopOperation />}{' '}
<TopOperation />
</Card>
</Col>
</Row>

View File

@@ -78,7 +78,7 @@ function ServiceOverview({
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
yAxisUnit: isSpanMetricEnable ? 'ms' : 'ns',
id: SERVICE_CHART_ID.latency,
}),
[

View File

@@ -1,130 +0,0 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { topOperationMetricsDownloadOptions } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQueries/TopOperationQueries';
import { QueryTable } from 'container/QueryTable';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { IServiceName } from '../types';
import { title } from './config';
import ColumnWithLink from './TableRenderer/ColumnWithLink';
import { getTableColumnRenderer } from './TableRenderer/TableColumnRenderer';
function TopOperationMetrics(): JSX.Element {
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { notifications } = useNotifications();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queries } = useResourceAttribute();
const selectedTraceTags = JSON.stringify(
convertRawQueriesToTraceSelectedTags(queries) || [],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const keyOperationWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: topOperationQueries({
servicename,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
},
panelTypes: PANEL_TYPES.TABLE,
}),
[servicename, dotMetricsEnabled],
);
const updatedQuery = updateStepInterval(keyOperationWidget.query);
const isEmptyWidget = keyOperationWidget.id === PANEL_TYPES.EMPTY_WIDGET;
const { data, isLoading } = useGetQueryRange(
{
selectedTime: keyOperationWidget?.timePreferance,
graphType: keyOperationWidget?.panelTypes,
query: updatedQuery,
globalSelectedInterval,
variables: {},
},
ENTITY_VERSION_V4,
{
queryKey: [
`GetMetricsQueryRange-${keyOperationWidget?.timePreferance}-${globalSelectedInterval}-${keyOperationWidget?.id}`,
keyOperationWidget,
maxTime,
minTime,
globalSelectedInterval,
],
keepPreviousData: true,
enabled: !isEmptyWidget,
refetchOnMount: false,
onError: (error) => {
notifications.error({ message: error.message });
},
},
);
const queryTableData = data?.payload?.data?.newResult?.data?.result || [];
const renderColumnCell = useMemo(
() =>
getTableColumnRenderer({
columnName: 'operation',
renderFunction: (record: RowData): ReactNode => (
<ColumnWithLink
servicename={servicename}
minTime={minTime}
maxTime={maxTime}
selectedTraceTags={selectedTraceTags}
record={record}
/>
),
}),
[servicename, minTime, maxTime, selectedTraceTags],
);
return (
<QueryTable
title={title}
query={updatedQuery}
queryTableData={queryTableData}
loading={isLoading}
renderColumnCell={renderColumnCell}
downloadOption={topOperationMetricsDownloadOptions}
sticky
/>
);
}
export default TopOperationMetrics;

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { DownloadOptions } from 'container/Download/Download.types';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
import {
MetricAggregateOperator,
@@ -107,11 +106,6 @@ export enum WidgetKeys {
Db_system_norm = 'db_system',
}
export const topOperationMetricsDownloadOptions: DownloadOptions = {
isDownloadEnabled: true,
fileName: 'top-operation',
} as const;
export const SERVICE_CHART_ID = {
latency: 'SERVICE_OVERVIEW_LATENCY',
error: 'SERVICE_OVERVIEW_ERROR',

View File

@@ -91,7 +91,7 @@ function Summary(): JSX.Element {
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...queryFilters,
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
items: queryFilters.items.map(({ id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [queryFilters]);

View File

@@ -12,7 +12,6 @@ import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -21,7 +20,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Terminal } from 'lucide-react';
import { Atom, Play, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
@@ -29,14 +28,20 @@ import {
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
function QuerySection({
selectedGraph,
queryResponse,
}: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
@@ -237,7 +242,15 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
<Button
loading={queryResponse.isFetching}
type="primary"
onClick={handleRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={items}
@@ -248,6 +261,10 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
interface QueryProps {
selectedGraph: PANEL_TYPES;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
}
export default QuerySection;

View File

@@ -64,7 +64,7 @@ function LeftContainer({
enableDrillDown={enableDrillDown}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} />
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer
selectedLogFields={selectedLogFields}

View File

@@ -166,7 +166,7 @@ function UpdateContextLinks({
onSave(newContextLink);
} catch (error) {
// Form validation failed, don't call onSave
console.error('Form validation failed:', error);
console.log('Form validation failed:', error);
}
};

View File

@@ -1,37 +0,0 @@
.run-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
.ant-btn-icon {
margin: 0 !important;
}
}
.cancel-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
}
.cmd-hint {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 4px;
border-radius: 4px;
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(35, 38, 46, 0.6);
line-height: 1;
font-size: 10px;
}
.lightMode {
.cmd-hint {
color: var(--bg-ink-200);
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(231, 232, 236, 0.8);
}
}

View File

@@ -1,53 +0,0 @@
import './RunQueryBtn.scss';
import { Button } from 'antd';
import {
ChevronUp,
Command,
CornerDownLeft,
Loader2,
Play,
} from 'lucide-react';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
interface RunQueryBtnProps {
label?: string;
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
}
function RunQueryBtn({
label,
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
return isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries || !onStageRunQuery}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
{label || 'Run Query'}
<div className="cmd-hint">
{isMac ? <Command size={12} /> : <ChevronUp size={12} />}
<CornerDownLeft size={12} />
</div>
</Button>
);
}
export default RunQueryBtn;

View File

@@ -1,82 +0,0 @@
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import RunQueryBtn from '../RunQueryBtn';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
UserOperatingSystem: { MACOS: 'mac', WINDOWS: 'win', LINUX: 'linux' },
}));
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
describe('RunQueryBtn', () => {
test('renders run state and triggers on click', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
expect(btn).toBeEnabled();
fireEvent.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
});
test('disabled when onStageRunQuery is undefined', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancel = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancel);
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('shows Command + CornerDownLeft on mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('shows ChevronUp + CornerDownLeft on non-mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.WINDOWS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('renders custom label when provided', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(
screen.getByRole('button', { name: /stage & run query/i }),
).toBeInTheDocument();
});
});

View File

@@ -1,12 +1,12 @@
import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Loader2, Play } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
import RunQueryBtn from '../RunQueryBtn/RunQueryBtn';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
@@ -42,7 +42,14 @@ export default function RightToolbarActions({
if (showLiveLogs) {
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn />
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled
icon={<Play size={14} />}
>
Run Query
</Button>
</div>
);
}
@@ -58,11 +65,26 @@ export default function RightToolbarActions({
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onStageRunQuery={onStageRunQuery}
/>
{isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Run Query
</Button>
)}
</div>
);
}

View File

@@ -136,6 +136,7 @@ const useAggregateDrilldown = ({
query,
// panelType,
aggregateData: aggregateDataWithTimeRange,
widgetId,
onClose,
});

View File

@@ -129,6 +129,7 @@ const useBaseAggregateOptions = ({
const handleBaseDrilldown = useCallback(
(key: string): void => {
console.log('Base drilldown:', { key, aggregateData });
const route = getRoute(key);
const timeRange = aggregateData?.timeRange;
const filtersToAdd = aggregateData?.filters || [];

View File

@@ -17,6 +17,7 @@ interface UseBaseAggregateOptionsProps {
query?: Query;
// panelType?: PANEL_TYPES;
aggregateData?: AggregateData | null;
widgetId?: string;
onClose: () => void;
}
@@ -26,6 +27,7 @@ const useDashboardVarConfig = ({
query,
// panelType,
aggregateData,
widgetId,
onClose,
}: UseBaseAggregateOptionsProps): {
dashbaordVariablesConfig: {
@@ -81,6 +83,11 @@ const useDashboardVarConfig = ({
dashboardVar: [string, IDashboardVariable],
fieldValue: any,
) => {
console.log('Setting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
fieldValue,
});
onValueUpdate(fieldName, dashboardVar[1]?.id, fieldValue, false);
onClose();
},
@@ -89,6 +96,10 @@ const useDashboardVarConfig = ({
const handleUnsetVariable = useCallback(
(fieldName: string, dashboardVar: [string, IDashboardVariable]) => {
console.log('Unsetting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
});
onValueUpdate(fieldName, dashboardVar[0], null, false);
onClose();
},
@@ -98,6 +109,12 @@ const useDashboardVarConfig = ({
const handleCreateVariable = useCallback(
(fieldName: string, fieldValue: string | number | boolean) => {
const source = getSourceFromQuery();
console.log('Creating variable from drilldown:', {
fieldName,
fieldValue,
source,
widgetId,
});
createVariable(
fieldName,
fieldValue,
@@ -108,7 +125,7 @@ const useDashboardVarConfig = ({
);
onClose();
},
[createVariable, getSourceFromQuery, onClose],
[createVariable, getSourceFromQuery, widgetId, onClose],
);
const contextItems = useMemo(

View File

@@ -1,22 +1,14 @@
import * as Sentry from '@sentry/react';
import { FeatureKeys } from 'constants/features';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useAppContext } from 'providers/App/App';
import ServiceMetrics from './ServiceMetrics';
import ServiceTraces from './ServiceTraces';
import { Container } from './styles';
function Services(): JSX.Element {
const { featureFlags } = useAppContext();
const isSpanMetricEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
?.active || false;
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Container style={{ marginTop: 0 }}>
{isSpanMetricEnabled ? <ServiceMetrics /> : <ServiceTraces />}
<ServiceTraces />
</Container>
</Sentry.ErrorBoundary>
);

View File

@@ -19,6 +19,20 @@
display: flex;
align-items: center;
gap: 8px;
.cancel-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
.run-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
}
}

View File

@@ -31,8 +31,8 @@ export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
...step.filters,
items: step.filters.items.map((item) => {
const {
id: _unusedId,
isIndexed: _isIndexed,
id: unusedId,
isIndexed,
...keyObj
} = item.key as BaseAutocompleteData;
return {

View File

@@ -143,7 +143,7 @@ export const useValidateFunnelSteps = ({
selectedTime,
steps.map((step) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { latency_type: _latency_type, ...rest } = step;
const { latency_type, ...rest } = step;
return rest;
}),
],

View File

@@ -55,7 +55,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
const parsedDraggedColumns = await JSON.parse(localStorageColumns);
nextDraggedColumns = parsedDraggedColumns;
} catch (e) {
console.error('error while parsing json: ', e);
console.log('error while parsing json');
} finally {
redirectWithDraggedColumns(nextDraggedColumns);
}

View File

@@ -4,6 +4,11 @@ import parser from 'lib/logql/parser';
describe('lib/logql/parser', () => {
test('parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
parser(queryObject.query);
} catch (e) {
console.log(e);
}
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
});
});

View File

@@ -4,7 +4,11 @@ import { reverseParser } from 'lib/logql/reverseParser';
describe('lib/logql/reverseParser', () => {
test('reverse parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
try {
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
} catch (e) {
console.log(e);
}
});
});
});

View File

@@ -11,8 +11,8 @@ describe('getYAxisScale', () => {
keyIndex: 1,
thresholdValue: 10,
thresholdUnit: 'percentunit',
moveThreshold(): void {
// no-op
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},
@@ -21,8 +21,8 @@ describe('getYAxisScale', () => {
keyIndex: 2,
thresholdValue: 20,
thresholdUnit: 'percentunit',
moveThreshold(): void {
// no-op
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},

View File

@@ -9,6 +9,7 @@ import {
MessagingQueuesPayloadProps,
} from 'api/messagingQueues/getConsumerLagDetails';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';

View File

@@ -275,7 +275,7 @@ export function setConfigDetail(
},
): void {
// remove "key" and its value from the paramsToSet object
const { key: _key, ...restParamsToSet } = paramsToSet || {};
const { key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {

View File

@@ -145,7 +145,7 @@ export const removeFilter = (
const updatedValues = prevValue.filter((item: any) => item !== value);
if (updatedValues.length === 0) {
const { [filterType]: _item, ...remainingFilters } = prevFilters;
const { [filterType]: item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as FilterType)
: undefined;
@@ -175,7 +175,7 @@ export const removeAllFilters = (
return prevFilters;
}
const { [filterType]: _item, ...remainingFilters } = prevFilters;
const { [filterType]: item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as Record<

View File

@@ -20,7 +20,8 @@ export const parseQueryIntoSpanKind = (
current = parsedValue;
}
} catch (error) {
console.error('error while parsing json: ', error);
console.log(error);
console.log('error while parsing json');
}
}

View File

@@ -19,7 +19,7 @@ export const parseQueryIntoCurrent = (
current = parseInt(parsedValue, 10);
}
} catch (error) {
console.error('error while parsing json: ', error);
console.log('error while parsing json');
}
}

View File

@@ -20,7 +20,8 @@ export const parseQueryIntoOrder = (
current = parsedValue;
}
} catch (error) {
console.error('error while parsing json: ', error);
console.log(error);
console.log('error while parsing json');
}
}

View File

@@ -20,7 +20,8 @@ export const parseAggregateOrderParams = (
current = parsedValue;
}
} catch (error) {
console.error('error while parsing json: ', error);
console.log(error);
console.log('error while parsing json');
}
}

View File

@@ -3,12 +3,26 @@ package implservices
import (
"fmt"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/servicetypes/servicetypesv1"
)
// spanMetricsSingleBucketStep keeps pxx queries in a single bucket
// while staying within the requested time range.
func spanMetricsSingleBucketStep(startMs, endMs uint64) qbtypes.Step {
if endMs <= startMs {
return qbtypes.Step{Duration: time.Second}
}
duration := time.Duration(endMs-startMs) * time.Millisecond
if duration < time.Second {
duration = time.Second
}
return qbtypes.Step{Duration: duration}
}
// validateTagFilterItems validates the tag filter items. This should be used before using
// buildFilterExpression or any other function that uses tag filter items.
func validateTagFilterItems(tags []servicetypesv1.TagFilterItem) error {

View File

@@ -3,16 +3,20 @@ package implservices
import (
"context"
"fmt"
"time"
"math"
"sort"
"strconv"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/services"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/servicetypes/servicetypesv1"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -22,13 +26,15 @@ import (
type module struct {
Querier querier.Querier
TelemetryStore telemetrystore.TelemetryStore
Flagger flagger.Flagger
}
// NewModule constructs the services module with the provided querier dependency.
func NewModule(q querier.Querier, ts telemetrystore.TelemetryStore) services.Module {
func NewModule(q querier.Querier, ts telemetrystore.TelemetryStore, flagger flagger.Flagger) services.Module {
return &module{
Querier: q,
TelemetryStore: ts,
Flagger: flagger,
}
}
@@ -74,10 +80,25 @@ func (m *module) Get(ctx context.Context, orgUUID valuer.UUID, req *servicetypes
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
// Prepare phase
queryRangeReq, startMs, endMs, err := m.buildQueryRangeRequest(req)
if err != nil {
return nil, err
var (
startMs uint64
endMs uint64
err error
queryRangeReq *qbtypes.QueryRangeRequest
)
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgUUID)
useSpanMetrics := m.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseSpanMetrics, evalCtx)
if useSpanMetrics {
queryRangeReq, startMs, endMs, err = m.buildSpanMetricsQueryRangeRequest(req)
if err != nil {
return nil, err
}
} else {
queryRangeReq, startMs, endMs, err = m.buildQueryRangeRequest(req)
if err != nil {
return nil, err
}
}
// Fetch phase
@@ -87,7 +108,14 @@ func (m *module) Get(ctx context.Context, orgUUID valuer.UUID, req *servicetypes
}
// Process phase
items, serviceNames := m.mapQueryRangeRespToServices(resp, startMs, endMs)
var items []*servicetypesv1.ResponseItem
var serviceNames []string
if useSpanMetrics {
items, serviceNames = m.mapSpanMetricsRespToServices(resp, startMs, endMs)
} else {
items, serviceNames = m.mapQueryRangeRespToServices(resp, startMs, endMs)
}
if len(items) == 0 {
return []*servicetypesv1.ResponseItem{}, nil
}
@@ -108,9 +136,23 @@ func (m *module) GetTopOperations(ctx context.Context, orgUUID valuer.UUID, req
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
qr, err := m.buildTopOpsQueryRangeRequest(req)
if err != nil {
return nil, err
var (
qr *qbtypes.QueryRangeRequest
err error
)
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgUUID)
useSpanMetrics := m.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseSpanMetrics, evalCtx)
if useSpanMetrics {
qr, err = m.buildSpanMetricsTopOpsQueryRangeRequest(req)
if err != nil {
return nil, err
}
} else {
qr, err = m.buildTopOpsQueryRangeRequest(req)
if err != nil {
return nil, err
}
}
resp, err := m.executeQuery(ctx, orgUUID, qr)
@@ -118,7 +160,17 @@ func (m *module) GetTopOperations(ctx context.Context, orgUUID valuer.UUID, req
return nil, err
}
items := m.mapTopOpsQueryRangeResp(resp)
var items []servicetypesv1.OperationItem
if useSpanMetrics {
items = m.mapSpanMetricsTopOpsResp(resp)
// Apply limit after merging multiple queries
if req.Limit > 0 && len(items) > req.Limit {
items = items[:req.Limit]
}
} else {
items = m.mapTopOpsQueryRangeResp(resp)
}
return items, nil
}
@@ -128,9 +180,23 @@ func (m *module) GetEntryPointOperations(ctx context.Context, orgUUID valuer.UUI
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
qr, err := m.buildEntryPointOpsQueryRangeRequest(req)
if err != nil {
return nil, err
var (
qr *qbtypes.QueryRangeRequest
err error
)
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgUUID)
useSpanMetrics := m.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseSpanMetrics, evalCtx)
if useSpanMetrics {
qr, err = m.buildSpanMetricsEntryPointOpsQueryRangeRequest(req)
if err != nil {
return nil, err
}
} else {
qr, err = m.buildEntryPointOpsQueryRangeRequest(req)
if err != nil {
return nil, err
}
}
resp, err := m.executeQuery(ctx, orgUUID, qr)
@@ -138,10 +204,36 @@ func (m *module) GetEntryPointOperations(ctx context.Context, orgUUID valuer.UUI
return nil, err
}
items := m.mapEntryPointOpsQueryRangeResp(resp)
var items []servicetypesv1.OperationItem
if useSpanMetrics {
items = m.mapSpanMetricsEntryPointOpsResp(resp)
// Apply limit after merging multiple queries
if req.Limit > 0 && len(items) > req.Limit {
items = items[:req.Limit]
}
} else {
items = m.mapEntryPointOpsQueryRangeResp(resp)
}
return items, nil
}
// executeQuery calls the underlying Querier with the provided request.
func (m *module) executeQuery(ctx context.Context, orgUUID valuer.UUID, qr *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
return m.Querier.QueryRange(ctx, orgUUID, qr)
}
// attachTopLevelOps fetches top-level ops from TelemetryStore and attaches them to items.
func (m *module) attachTopLevelOps(ctx context.Context, serviceNames []string, startMs uint64, items []*servicetypesv1.ResponseItem) error {
startTime := time.UnixMilli(int64(startMs)).UTC()
opsMap, err := m.FetchTopLevelOperations(ctx, startTime, serviceNames)
if err != nil {
return err
}
applyOpsToItems(items, opsMap)
return nil
}
// buildQueryRangeRequest constructs the QBv5 QueryRangeRequest and computes the time window.
func (m *module) buildQueryRangeRequest(req *servicetypesv1.Request) (*qbtypes.QueryRangeRequest, uint64, uint64, error) {
// Parse start/end (nanoseconds) from strings and convert to milliseconds for QBv5
@@ -211,11 +303,6 @@ func (m *module) buildQueryRangeRequest(req *servicetypesv1.Request) (*qbtypes.Q
return &reqV5, startMs, endMs, nil
}
// executeQuery calls the underlying Querier with the provided request.
func (m *module) executeQuery(ctx context.Context, orgUUID valuer.UUID, qr *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
return m.Querier.QueryRange(ctx, orgUUID, qr)
}
// mapQueryRangeRespToServices converts the raw query response into service items and collected service names.
func (m *module) mapQueryRangeRespToServices(resp *qbtypes.QueryRangeResponse, startMs, endMs uint64) ([]*servicetypesv1.ResponseItem, []string) {
if resp == nil || len(resp.Data.Results) == 0 { // no rows
@@ -241,7 +328,7 @@ func (m *module) mapQueryRangeRespToServices(resp *qbtypes.QueryRangeResponse, s
}
}
periodSeconds := float64((endMs - startMs) / 1000)
periodSeconds := float64(endMs-startMs) / 1000.0
out := make([]*servicetypesv1.ResponseItem, 0, len(sd.Data))
serviceNames := make([]string, 0, len(sd.Data))
@@ -285,15 +372,211 @@ func (m *module) mapQueryRangeRespToServices(resp *qbtypes.QueryRangeResponse, s
return out, serviceNames
}
// attachTopLevelOps fetches top-level ops from TelemetryStore and attaches them to items.
func (m *module) attachTopLevelOps(ctx context.Context, serviceNames []string, startMs uint64, items []*servicetypesv1.ResponseItem) error {
startTime := time.UnixMilli(int64(startMs)).UTC()
opsMap, err := m.FetchTopLevelOperations(ctx, startTime, serviceNames)
// buildSpanMetricsQueryRangeRequest constructs span-metrics queries for services.
func (m *module) buildSpanMetricsQueryRangeRequest(req *servicetypesv1.Request) (*qbtypes.QueryRangeRequest, uint64, uint64, error) {
// base filters from request
// Parse start/end (nanoseconds) from strings and convert to milliseconds for QBv5
startNs, err := strconv.ParseUint(req.Start, 10, 64)
if err != nil {
return err
return nil, 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid start time: %v", err)
}
applyOpsToItems(items, opsMap)
return nil
endNs, err := strconv.ParseUint(req.End, 10, 64)
if err != nil {
return nil, 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid end time: %v", err)
}
if startNs >= endNs {
return nil, 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be before end")
}
if err := validateTagFilterItems(req.Tags); err != nil {
return nil, 0, 0, err
}
startMs := startNs / 1_000_000
endMs := endNs / 1_000_000
filterExpr, variables := buildFilterExpression(req.Tags)
// enforce top-level scope via synthetic field
scopeExpr := "isTopLevelOperation = 'true'"
if filterExpr != "" {
filterExpr = "(" + filterExpr + ") AND (" + scopeExpr + ")"
} else {
filterExpr = scopeExpr
}
// Build error filter for num_errors query
var errorFilterExpr string
if filterExpr != "" {
errorFilterExpr = "(" + filterExpr + ") AND (status.code = 'STATUS_CODE_ERROR')"
} else {
errorFilterExpr = "status.code = 'STATUS_CODE_ERROR'"
}
// common groupBy on service.name
groupByService := []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextAttribute, // aligns with working payload
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
}},
}
queries := []qbtypes.QueryEnvelope{
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p99_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByService,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile99,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_calls",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByService,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_errors",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: errorFilterExpr},
GroupBy: groupByService,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
}
reqV5 := qbtypes.QueryRangeRequest{
Start: startMs,
End: endMs,
RequestType: qbtypes.RequestTypeScalar,
Variables: variables,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
NoCache: true,
}
return &reqV5, startMs, endMs, nil
}
// mapSpanMetricsRespToServices merges span-metrics scalar results keyed by service.name using queryName for aggregation mapping.
func (m *module) mapSpanMetricsRespToServices(resp *qbtypes.QueryRangeResponse, startMs, endMs uint64) ([]*servicetypesv1.ResponseItem, []string) {
if resp == nil || len(resp.Data.Results) == 0 {
return []*servicetypesv1.ResponseItem{}, []string{}
}
periodSeconds := float64(endMs-startMs) / 1000.0
type agg struct {
p99Latency float64
numCalls float64
numErrors float64
}
perSvc := make(map[string]*agg)
for _, result := range resp.Data.Results {
sd, ok := result.(*qbtypes.ScalarData)
if !ok || sd == nil || len(sd.Columns) == 0 {
continue
}
// locate service.name column and aggregation column for this query
serviceNameRespIndex := -1
aggIdx := -1
for i, c := range sd.Columns {
switch c.Type {
case qbtypes.ColumnTypeGroup:
if c.Name == "service.name" {
serviceNameRespIndex = i
}
case qbtypes.ColumnTypeAggregation:
if aggIdx == -1 {
aggIdx = i
}
}
}
if serviceNameRespIndex == -1 || aggIdx == -1 {
continue
}
for _, row := range sd.Data {
svcName := fmt.Sprintf("%v", row[serviceNameRespIndex])
a := perSvc[svcName]
if a == nil {
a = &agg{}
perSvc[svcName] = a
}
val := toFloat(row, aggIdx)
switch sd.QueryName {
case "p99_latency":
a.p99Latency = val * math.Pow(10, 6) // convert to nanoseconds because frontend expects this
case "num_calls":
a.numCalls = val
case "num_errors":
a.numErrors = val
}
}
}
out := make([]*servicetypesv1.ResponseItem, 0, len(perSvc))
serviceNames := make([]string, 0, len(perSvc))
for svcName, a := range perSvc {
errorRate := 0.0
if a.numCalls > 0 {
errorRate = a.numErrors * 100 / a.numCalls
}
callRate := 0.0
if a.numCalls > 0 {
callRate = a.numCalls / periodSeconds
}
out = append(out, &servicetypesv1.ResponseItem{
ServiceName: svcName,
Percentile99: a.p99Latency,
CallRate: callRate,
ErrorRate: errorRate,
DataWarning: servicetypesv1.DataWarning{TopLevelOps: []string{}},
})
serviceNames = append(serviceNames, svcName)
}
return out, serviceNames
}
func (m *module) buildTopOpsQueryRangeRequest(req *servicetypesv1.OperationsRequest) (*qbtypes.QueryRangeRequest, error) {
@@ -404,6 +687,531 @@ func (m *module) mapTopOpsQueryRangeResp(resp *qbtypes.QueryRangeResponse) []ser
return out
}
// buildSpanMetricsTopOpsQueryRangeRequest constructs span-metrics queries for top operations.
func (m *module) buildSpanMetricsTopOpsQueryRangeRequest(req *servicetypesv1.OperationsRequest) (*qbtypes.QueryRangeRequest, error) {
if req.Service == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "service is required")
}
startNs, err := strconv.ParseUint(req.Start, 10, 64)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid start time: %v", err)
}
endNs, err := strconv.ParseUint(req.End, 10, 64)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid end time: %v", err)
}
if startNs >= endNs {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be before end")
}
if req.Limit < 1 || req.Limit > 5000 {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be between 1 and 5000")
}
if err := validateTagFilterItems(req.Tags); err != nil {
return nil, err
}
startMs := startNs / 1_000_000
endMs := endNs / 1_000_000
// Build service filter
serviceTag := servicetypesv1.TagFilterItem{
Key: "service.name",
Operator: "in",
StringValues: []string{req.Service},
}
tags := append([]servicetypesv1.TagFilterItem{serviceTag}, req.Tags...)
filterExpr, variables := buildFilterExpression(tags)
// Build error filter for num_errors query
var errorFilterExpr string
if filterExpr != "" {
errorFilterExpr = "(" + filterExpr + ") AND (status.code = 'STATUS_CODE_ERROR')"
} else {
errorFilterExpr = "status.code = 'STATUS_CODE_ERROR'"
}
// Common groupBy on operation
groupByOperation := []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "operation",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
}},
}
queries := []qbtypes.QueryEnvelope{
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p50_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile50,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p95_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p99_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile99,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_calls",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_errors",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: errorFilterExpr},
GroupBy: groupByOperation,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
}
reqV5 := qbtypes.QueryRangeRequest{
Start: startMs,
End: endMs,
RequestType: qbtypes.RequestTypeScalar,
Variables: variables,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
NoCache: true,
}
return &reqV5, nil
}
// mapSpanMetricsTopOpsResp maps span-metrics scalar results to OperationItem array using queryName for aggregation mapping.
func (m *module) mapSpanMetricsTopOpsResp(resp *qbtypes.QueryRangeResponse) []servicetypesv1.OperationItem {
if resp == nil || len(resp.Data.Results) == 0 {
return []servicetypesv1.OperationItem{}
}
// Group data by operation name and merge aggregations from all results
type agg struct {
p50 float64
p95 float64
p99 float64
numCalls uint64
numErrors uint64
}
perOp := make(map[string]*agg)
// Iterate through all results (each query returns a separate ScalarData)
for _, result := range resp.Data.Results {
sd, ok := result.(*qbtypes.ScalarData)
if !ok || sd == nil {
continue
}
// Skip empty results
if len(sd.Columns) == 0 || len(sd.Data) == 0 {
continue
}
// Find operation and aggregation column indices
operationIdx := -1
aggIdx := -1
for i, c := range sd.Columns {
switch c.Type {
case qbtypes.ColumnTypeGroup:
if c.Name == "operation" {
operationIdx = i
}
case qbtypes.ColumnTypeAggregation:
if aggIdx == -1 {
aggIdx = i
}
}
}
if operationIdx == -1 || aggIdx == -1 {
continue
}
// Process each row in this result and merge by operation name
for _, row := range sd.Data {
if len(row) <= operationIdx || len(row) <= aggIdx {
continue
}
opName := fmt.Sprintf("%v", row[operationIdx])
a := perOp[opName]
if a == nil {
a = &agg{}
perOp[opName] = a
}
// Map values based on queryName
switch sd.QueryName {
case "p50_latency":
a.p50 = toFloat(row, aggIdx) * math.Pow(10, 6) // convert milliseconds to nanoseconds
case "p95_latency":
a.p95 = toFloat(row, aggIdx) * math.Pow(10, 6)
case "p99_latency":
a.p99 = toFloat(row, aggIdx) * math.Pow(10, 6)
case "num_calls":
a.numCalls = uint64(toFloat(row, aggIdx))
case "num_errors":
a.numErrors = uint64(toFloat(row, aggIdx))
}
}
}
if len(perOp) == 0 {
return []servicetypesv1.OperationItem{}
}
out := make([]servicetypesv1.OperationItem, 0, len(perOp))
for opName, a := range perOp {
out = append(out, servicetypesv1.OperationItem{
Name: opName,
P50: a.p50,
P95: a.p95,
P99: a.p99,
NumCalls: a.numCalls,
ErrorCount: a.numErrors,
})
}
sort.Slice(out, func(i, j int) bool {
return out[i].P99 > out[j].P99
})
return out
}
// buildSpanMetricsEntryPointOpsQueryRangeRequest constructs span-metrics queries for entry point operations.
// Similar to buildSpanMetricsTopOpsQueryRangeRequest but includes isTopLevelOperation filter.
func (m *module) buildSpanMetricsEntryPointOpsQueryRangeRequest(req *servicetypesv1.OperationsRequest) (*qbtypes.QueryRangeRequest, error) {
if req.Service == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "service is required")
}
startNs, err := strconv.ParseUint(req.Start, 10, 64)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid start time: %v", err)
}
endNs, err := strconv.ParseUint(req.End, 10, 64)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid end time: %v", err)
}
if startNs >= endNs {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be before end")
}
if req.Limit < 1 || req.Limit > 5000 {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be between 1 and 5000")
}
if err := validateTagFilterItems(req.Tags); err != nil {
return nil, err
}
startMs := startNs / 1_000_000
endMs := endNs / 1_000_000
// Build service filter
serviceTag := servicetypesv1.TagFilterItem{
Key: "service.name",
Operator: "in",
StringValues: []string{req.Service},
}
tags := append([]servicetypesv1.TagFilterItem{serviceTag}, req.Tags...)
filterExpr, variables := buildFilterExpression(tags)
// Enforce top-level scope via synthetic field (entry point operations only)
scopeExpr := "isTopLevelOperation = 'true'"
if filterExpr != "" {
filterExpr = "(" + filterExpr + ") AND (" + scopeExpr + ")"
} else {
filterExpr = scopeExpr
}
// Build error filter for num_errors query
var errorFilterExpr string
if filterExpr != "" {
errorFilterExpr = "(" + filterExpr + ") AND (status.code = 'STATUS_CODE_ERROR')"
} else {
errorFilterExpr = "status.code = 'STATUS_CODE_ERROR'"
}
// Common groupBy on operation
groupByOperation := []qbtypes.GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "operation",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
}},
}
queries := []qbtypes.QueryEnvelope{
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p50_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile50,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p95_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "p99_latency",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
StepInterval: spanMetricsSingleBucketStep(startMs, endMs),
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency.bucket",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile99,
ReduceTo: qbtypes.ReduceToAvg,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_calls",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: filterExpr},
GroupBy: groupByOperation,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
{Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "num_errors",
Signal: telemetrytypes.SignalMetrics,
Filter: &qbtypes.Filter{Expression: errorFilterExpr},
GroupBy: groupByOperation,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationIncrease,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToSum,
},
},
},
},
}
reqV5 := qbtypes.QueryRangeRequest{
Start: startMs,
End: endMs,
RequestType: qbtypes.RequestTypeScalar,
Variables: variables,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
NoCache: true,
}
return &reqV5, nil
}
// mapSpanMetricsEntryPointOpsResp maps span-metrics scalar results to OperationItem array for entry point operations.
// Uses queryName for aggregation mapping.
func (m *module) mapSpanMetricsEntryPointOpsResp(resp *qbtypes.QueryRangeResponse) []servicetypesv1.OperationItem {
if resp == nil || len(resp.Data.Results) == 0 {
return []servicetypesv1.OperationItem{}
}
// Group data by operation name and merge aggregations from all results
type agg struct {
p50 float64
p95 float64
p99 float64
numCalls uint64
numErrors uint64
}
perOp := make(map[string]*agg)
// Iterate through all results (each query returns a separate ScalarData)
for _, result := range resp.Data.Results {
sd, ok := result.(*qbtypes.ScalarData)
if !ok || sd == nil {
continue
}
// Skip empty results
if len(sd.Columns) == 0 || len(sd.Data) == 0 {
continue
}
// Find operation and aggregation column indices
operationIdx := -1
aggIdx := -1
for i, c := range sd.Columns {
switch c.Type {
case qbtypes.ColumnTypeGroup:
if c.Name == "operation" {
operationIdx = i
}
case qbtypes.ColumnTypeAggregation:
if aggIdx == -1 {
aggIdx = i
}
}
}
if operationIdx == -1 || aggIdx == -1 {
continue
}
for _, row := range sd.Data {
if len(row) <= operationIdx || len(row) <= aggIdx {
continue
}
opName := fmt.Sprintf("%v", row[operationIdx])
a := perOp[opName]
if a == nil {
a = &agg{}
perOp[opName] = a
}
// Map values based on queryName
switch sd.QueryName {
case "p50_latency":
a.p50 = toFloat(row, aggIdx) * math.Pow(10, 6) // convert seconds to nanoseconds
case "p95_latency":
a.p95 = toFloat(row, aggIdx) * math.Pow(10, 6)
case "p99_latency":
a.p99 = toFloat(row, aggIdx) * math.Pow(10, 6)
case "num_calls":
a.numCalls = uint64(toFloat(row, aggIdx))
case "num_errors":
a.numErrors = uint64(toFloat(row, aggIdx))
}
}
}
if len(perOp) == 0 {
return []servicetypesv1.OperationItem{}
}
out := make([]servicetypesv1.OperationItem, 0, len(perOp))
for opName, a := range perOp {
out = append(out, servicetypesv1.OperationItem{
Name: opName,
P50: a.p50,
P95: a.p95,
P99: a.p99,
NumCalls: a.numCalls,
ErrorCount: a.numErrors,
})
}
sort.Slice(out, func(i, j int) bool {
return out[i].P99 > out[j].P99
})
return out
}
func (m *module) buildEntryPointOpsQueryRangeRequest(req *servicetypesv1.OperationsRequest) (*qbtypes.QueryRangeRequest, error) {
if req.Service == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "service is required")

View File

@@ -81,10 +81,8 @@ func FilterIntermediateColumns(result *qbtypes.QueryRangeResponse) *qbtypes.Quer
// Filter out columns for intermediate queries used only in formulas
filteredColumns := make([]*qbtypes.ColumnDescriptor, 0)
intermediateQueryNames := map[string]bool{
"error": true,
"total_span": true,
"endpoints_current": true,
"endpoints_legacy": true,
"error": true,
"total_span": true,
}
columnIndices := make([]int, 0)
@@ -298,15 +296,15 @@ func BuildDomainList(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.Que
return nil, err
}
queries := buildEndpointsQueries(req)
queries = append(queries,
queries := []qbtypes.QueryEnvelope{
buildEndpointsQuery(req),
buildLastSeenQuery(req),
buildRpsQuery(req),
buildErrorQuery(req),
buildTotalSpanQuery(req),
buildP99Query(req),
buildErrorRateFormula(),
)
}
return &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
@@ -348,58 +346,20 @@ func BuildDomainInfo(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.Que
}, nil
}
// buildEndpointsQueries returns queries for counting distinct URLs with semconv fallback.
// It uses two queries with mutually exclusive filters:
// - endpoints_current: count_distinct(url.full) WHERE url.full EXISTS
// - endpoints_legacy: count_distinct(http.url) WHERE url.full NOT EXISTS
// And a formula to combine them: endpoints_current + endpoints_legacy
func buildEndpointsQueries(req *thirdpartyapitypes.ThirdPartyApiRequest) []qbtypes.QueryEnvelope {
// Query for current semconv (url.full)
currentFilter := buildBaseFilter(req.Filter)
currentFilter.Expression = fmt.Sprintf("(%s) AND %s EXISTS", currentFilter.Expression, urlPathKey)
endpointsCurrent := qbtypes.QueryEnvelope{
func buildEndpointsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "endpoints_current",
Name: "endpoints",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: fmt.Sprintf("count_distinct(%s)", urlPathKey)},
{Expression: "count_distinct(http.url)"},
},
Filter: currentFilter,
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
// Query for legacy semconv (http.url) - only when url.full doesn't exist
legacyFilter := buildBaseFilter(req.Filter)
legacyFilter.Expression = fmt.Sprintf("(%s) AND %s NOT EXISTS", legacyFilter.Expression, urlPathKey)
endpointsLegacy := qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "endpoints_legacy",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: fmt.Sprintf("count_distinct(%s)", urlPathKeyLegacy)},
},
Filter: legacyFilter,
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
// Formula to combine both counts
endpointsFormula := qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeFormula,
Spec: qbtypes.QueryBuilderFormula{
Name: "endpoints",
Expression: "endpoints_current + endpoints_legacy",
},
}
return []qbtypes.QueryEnvelope{endpointsCurrent, endpointsLegacy, endpointsFormula}
}
func buildLastSeenQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {

View File

@@ -36,6 +36,29 @@ func NewBucketCache(settings factory.ProviderSettings, cache cache.Cache, cacheT
}
}
// cachedBucket represents a cached time bucket
type cachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type qbtypes.RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats qbtypes.ExecStats `json:"stats"`
}
// cachedData represents the full cached data for a query
type cachedData struct {
Buckets []*cachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *cachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *cachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
// GetMissRanges returns cached data and missing time ranges
func (bc *bucketCache) GetMissRanges(
ctx context.Context,
@@ -55,7 +78,7 @@ func (bc *bucketCache) GetMissRanges(
bc.logger.DebugContext(ctx, "cache key", "cache_key", cacheKey)
// Try to get cached data
var data qbtypes.CachedData
var data cachedData
err := bc.cache.Get(ctx, orgID, cacheKey, &data)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
@@ -124,9 +147,9 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
cacheKey := bc.generateCacheKey(q)
// Get existing cached data
var existingData qbtypes.CachedData
var existingData cachedData
if err := bc.cache.Get(ctx, orgID, cacheKey, &existingData); err != nil {
existingData = qbtypes.CachedData{}
existingData = cachedData{}
}
// Trim the result to exclude data within flux interval
@@ -180,7 +203,7 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
uniqueWarnings := bc.deduplicateWarnings(allWarnings)
// Create updated cached data
updatedData := qbtypes.CachedData{
updatedData := cachedData{
Buckets: mergedBuckets,
Warnings: uniqueWarnings,
}
@@ -199,7 +222,7 @@ func (bc *bucketCache) generateCacheKey(q qbtypes.Query) string {
}
// findMissingRangesWithStep identifies time ranges not covered by cached buckets with step alignment
func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
// When step is 0 or window is too small to be cached, use simple algorithm
if stepMs == 0 || (startMs+stepMs) > endMs {
return bc.findMissingRangesBasic(buckets, startMs, endMs)
@@ -242,7 +265,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket
}
if needsSort {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -316,7 +339,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket
}
// findMissingRangesBasic is the simple algorithm without step alignment
func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
// Check if already sorted before sorting
needsSort := false
for i := 1; i < len(buckets); i++ {
@@ -327,7 +350,7 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, s
}
if needsSort {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -398,9 +421,9 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, s
}
// filterRelevantBuckets returns buckets that overlap with the requested time range
func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.CachedBucket {
func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, endMs uint64) []*cachedBucket {
// Pre-allocate with estimated capacity
relevant := make([]*qbtypes.CachedBucket, 0, len(buckets))
relevant := make([]*cachedBucket, 0, len(buckets))
for _, bucket := range buckets {
// Check if bucket overlaps with requested range
@@ -410,7 +433,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, st
}
// Sort by start time
slices.SortFunc(relevant, func(a, b *qbtypes.CachedBucket) int {
slices.SortFunc(relevant, func(a, b *cachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -424,7 +447,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, st
}
// mergeBuckets combines multiple cached buckets into a single result
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.CachedBucket, warnings []string) *qbtypes.Result {
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket, warnings []string) *qbtypes.Result {
if len(buckets) == 0 {
return &qbtypes.Result{}
}
@@ -457,7 +480,7 @@ func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.Cach
}
// mergeTimeSeriesValues merges time series data from multiple buckets
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*qbtypes.CachedBucket) *qbtypes.TimeSeriesData {
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cachedBucket) *qbtypes.TimeSeriesData {
// Estimate capacity based on bucket count
estimatedSeries := len(buckets) * 10
@@ -608,7 +631,7 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
}
// resultToBuckets converts a query result into time-based buckets
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*qbtypes.CachedBucket {
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*cachedBucket {
// Check if result is empty
isEmpty, isFiltered := bc.isEmptyResult(result)
@@ -629,7 +652,7 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
// Always create a bucket, even for empty filtered results
// This ensures we don't re-query for data that doesn't exist
return []*qbtypes.CachedBucket{
return []*cachedBucket{
{
StartMs: startMs,
EndMs: endMs,
@@ -641,9 +664,9 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
}
// mergeAndDeduplicateBuckets combines and deduplicates bucket lists
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.CachedBucket) []*qbtypes.CachedBucket {
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucket) []*cachedBucket {
// Create a map to deduplicate by time range
bucketMap := make(map[string]*qbtypes.CachedBucket)
bucketMap := make(map[string]*cachedBucket)
// Add existing buckets
for _, bucket := range existing {
@@ -658,13 +681,13 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.Cac
}
// Convert back to slice with pre-allocated capacity
result := make([]*qbtypes.CachedBucket, 0, len(bucketMap))
result := make([]*cachedBucket, 0, len(bucketMap))
for _, bucket := range bucketMap {
result = append(result, bucket)
}
// Sort by start time
slices.SortFunc(result, func(a, b *qbtypes.CachedBucket) int {
slices.SortFunc(result, func(a, b *cachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}

View File

@@ -147,14 +147,14 @@ func BenchmarkBucketCache_MergeTimeSeriesValues(b *testing.B) {
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
// Create test buckets
buckets := make([]*qbtypes.CachedBucket, tc.numBuckets)
buckets := make([]*cachedBucket, tc.numBuckets)
for i := 0; i < tc.numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
result := createBenchmarkResultWithSeries(startMs, endMs, 1000, tc.numSeries, tc.numValues)
valueBytes, _ := json.Marshal(result.Value)
buckets[i] = &qbtypes.CachedBucket{
buckets[i] = &cachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,
@@ -417,8 +417,8 @@ func createBenchmarkResultWithSeries(startMs, endMs uint64, _ uint64, numSeries,
}
// Helper function to create buckets with specific gap patterns
func createBucketsWithPattern(numBuckets int, pattern string) []*qbtypes.CachedBucket {
buckets := make([]*qbtypes.CachedBucket, 0, numBuckets)
func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
buckets := make([]*cachedBucket, 0, numBuckets)
for i := 0; i < numBuckets; i++ {
// Skip some buckets based on pattern
@@ -432,7 +432,7 @@ func createBucketsWithPattern(numBuckets int, pattern string) []*qbtypes.CachedB
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
buckets = append(buckets, &qbtypes.CachedBucket{
buckets = append(buckets, &cachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,

View File

@@ -521,7 +521,7 @@ func TestBucketCache_FindMissingRanges_EdgeCases(t *testing.T) {
bc := NewBucketCache(instrumentationtest.New().ToProviderSettings(), memCache, cacheTTL, defaultFluxInterval).(*bucketCache)
// Test with buckets that have gaps and overlaps
buckets := []*qbtypes.CachedBucket{
buckets := []*cachedBucket{
{StartMs: 1000, EndMs: 2000},
{StartMs: 2500, EndMs: 3500},
{StartMs: 3000, EndMs: 4000}, // Overlaps with previous
@@ -1097,7 +1097,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
tests := []struct {
name string
buckets []*qbtypes.CachedBucket
buckets []*cachedBucket
startMs uint64
endMs uint64
stepMs uint64
@@ -1106,7 +1106,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
}{
{
name: "start_not_aligned_to_step",
buckets: []*qbtypes.CachedBucket{},
buckets: []*cachedBucket{},
startMs: 1500, // Not aligned to 1000ms step
endMs: 5000,
stepMs: 1000,
@@ -1118,7 +1118,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "end_not_aligned_to_step",
buckets: []*qbtypes.CachedBucket{},
buckets: []*cachedBucket{},
startMs: 1000,
endMs: 4500, // Not aligned to 1000ms step
stepMs: 1000,
@@ -1129,7 +1129,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "bucket_boundaries_not_aligned",
buckets: []*qbtypes.CachedBucket{
buckets: []*cachedBucket{
{StartMs: 1500, EndMs: 2500}, // Not aligned
},
startMs: 1000,
@@ -1143,7 +1143,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "small_window_less_than_step",
buckets: []*qbtypes.CachedBucket{},
buckets: []*cachedBucket{},
startMs: 1000,
endMs: 1500, // Less than one step
stepMs: 1000,
@@ -1154,7 +1154,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "zero_step_uses_basic_algorithm",
buckets: []*qbtypes.CachedBucket{},
buckets: []*cachedBucket{},
startMs: 1000,
endMs: 5000,
stepMs: 0,

View File

@@ -40,7 +40,7 @@ func TestNewHandlers(t *testing.T) {
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, nil)
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil)

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
@@ -85,6 +86,7 @@ func NewModules(
queryParser queryparser.QueryParser,
config Config,
dashboard dashboard.Module,
flagger flagger.Flagger,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
@@ -107,7 +109,7 @@ func NewModules(
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
Services: implservices.NewModule(querier, telemetryStore, flagger),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
}

View File

@@ -40,7 +40,7 @@ func TestNewModules(t *testing.T) {
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, nil)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -388,7 +388,7 @@ func New(
// Initialize all modules
roleModule := implrole.NewModule(implrole.NewStore(sqlstore), authz, nil)
dashboardModule := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleModule, queryParser, querier, licensing)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboardModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboardModule, flagger)
// Initialize all handlers for the modules
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway)

View File

@@ -698,6 +698,25 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
// hit the limit?
complete := rowCount <= limit
for _, selector := range fieldKeySelectors {
for _, key := range telemetrymetrics.MetricScopeFieldDefinitions {
if selector.Signal != telemetrytypes.SignalUnspecified && selector.Signal != telemetrytypes.SignalMetrics {
continue
}
if selector.FieldContext != telemetrytypes.FieldContextUnspecified && selector.FieldContext != key.FieldContext {
continue
}
if selector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified && selector.FieldDataType != key.FieldDataType {
continue
}
if matchesSelectorName(selector.Name, key.Name, selector.SelectorMatchType) {
keys = append(keys, &key)
break
}
}
}
return keys, complete, nil
}

View File

@@ -4,9 +4,11 @@ import (
"context"
"fmt"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -139,9 +141,13 @@ func (c *conditionBuilder) ConditionFor(
operator qbtypes.FilterOperator,
value any,
sb *sqlbuilder.SelectBuilder,
_ uint64,
start uint64,
_ uint64,
) (string, error) {
if c.isMetricScopeField(key.Name) {
return c.buildMetricScopeCondition(operator, value, start)
}
condition, err := c.conditionFor(ctx, key, operator, value, sb)
if err != nil {
return "", err
@@ -149,3 +155,37 @@ func (c *conditionBuilder) ConditionFor(
return condition, nil
}
func (c *conditionBuilder) isMetricScopeField(keyName string) bool {
return keyName == MetricScopeFieldIsTopLevelOperation
}
// buildMetricScopeCondition handles synthetic field isTopLevelOperation for metrics signal.
func (c *conditionBuilder) buildMetricScopeCondition(operator qbtypes.FilterOperator, value any, start uint64) (string, error) {
if operator != qbtypes.FilterOperatorEqual {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "%s only supports '=' operator", MetricScopeFieldIsTopLevelOperation)
}
// Accept true in bool or string form; anything else is invalid
isTrue := false
switch v := value.(type) {
case bool:
isTrue = v
case string:
isTrue = strings.ToLower(v) == "true"
default:
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "%s expects boolean value, got %T", MetricScopeFieldIsTopLevelOperation, value)
}
if !isTrue {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "%s can only be filtered with value 'true'", MetricScopeFieldIsTopLevelOperation)
}
startSec := int64(start / 1000)
return fmt.Sprintf(
"((JSONExtractString(labels, 'operation'), JSONExtractString(labels, 'service.name')) GLOBAL IN (SELECT DISTINCT name, serviceName FROM %s.%s WHERE time >= toDateTime(%d)))",
telemetrytraces.DBName,
// telemetrytraces.LocalTopLevelOperationsTableName,
telemetrytraces.TopLevelOperationsTableName,
startSec,
), nil
}

View File

@@ -36,3 +36,14 @@ var IntrinsicMetricFieldDefinitions = map[string]telemetrytypes.TelemetryFieldKe
// FieldDataType: telemetrytypes.FieldDataTypeBool,
// },
}
const MetricScopeFieldIsTopLevelOperation = "isTopLevelOperation"
var MetricScopeFieldDefinitions = map[string]telemetrytypes.TelemetryFieldKey{
MetricScopeFieldIsTopLevelOperation: {
Name: MetricScopeFieldIsTopLevelOperation,
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextMetric,
FieldDataType: telemetrytypes.FieldDataTypeBool,
},
}

View File

@@ -279,7 +279,7 @@ func (c *conditionBuilder) buildSpanScopeCondition(key *telemetrytypes.Telemetry
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s only supports '=' operator", key.Name)
}
var isTrue bool
isTrue := false
switch v := value.(type) {
case bool:
isTrue = v

View File

@@ -1,12 +1,13 @@
package telemetrytraces
const (
DBName = "signoz_traces"
SpanIndexV3TableName = "distributed_signoz_index_v3"
SpanIndexV3LocalTableName = "signoz_index_v3"
TagAttributesV2TableName = "distributed_tag_attributes_v2"
TagAttributesV2LocalTableName = "tag_attributes_v2"
TopLevelOperationsTableName = "distributed_top_level_operations"
TraceSummaryTableName = "distributed_trace_summary"
SpanAttributesKeysTblName = "distributed_span_attributes_keys"
DBName = "signoz_traces"
SpanIndexV3TableName = "distributed_signoz_index_v3"
SpanIndexV3LocalTableName = "signoz_index_v3"
TagAttributesV2TableName = "distributed_tag_attributes_v2"
TagAttributesV2LocalTableName = "tag_attributes_v2"
TopLevelOperationsTableName = "distributed_top_level_operations"
LocalTopLevelOperationsTableName = "top_level_operations"
TraceSummaryTableName = "distributed_trace_summary"
SpanAttributesKeysTblName = "distributed_span_attributes_keys"
)

View File

@@ -1,8 +1,6 @@
package querybuildertypesv5
import (
"fmt"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -133,44 +131,12 @@ func (q *QueryBuilderQuery[T]) UnmarshalJSON(data []byte) error {
var temp Alias
// Use UnmarshalJSONWithContext for better error messages
if err := UnmarshalJSONWithContext(data, &temp, fmt.Sprintf("query spec for %T", q)); err != nil {
if err := UnmarshalJSONWithContext(data, &temp, "query spec"); err != nil {
return err
}
// Copy the decoded values back to the original struct
*q = QueryBuilderQuery[T](temp)
// Nomarlize the query after unmarshaling
q.Normalize()
return nil
}
// Normalize normalizes all the field keys in the query
func (q *QueryBuilderQuery[T]) Normalize() {
// normalize select fields
for idx := range q.SelectFields {
q.SelectFields[idx].Normalize()
}
// normalize group by fields
for idx := range q.GroupBy {
q.GroupBy[idx].Normalize()
}
// normalize order by fields
for idx := range q.Order {
q.Order[idx].Key.Normalize()
}
// normalize secondary aggregations
for idx := range q.SecondaryAggregations {
for jdx := range q.SecondaryAggregations[idx].Order {
q.SecondaryAggregations[idx].Order[jdx].Key.Normalize()
}
for jdx := range q.SecondaryAggregations[idx].GroupBy {
q.SecondaryAggregations[idx].GroupBy[jdx].Normalize()
}
}
}

View File

@@ -1,653 +0,0 @@
package querybuildertypesv5
import (
"encoding/json"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryBuilderQuery_Copy(t *testing.T) {
t.Run("copy with all fields populated", func(t *testing.T) {
original := QueryBuilderQuery[TraceAggregation]{
Name: "A",
StepInterval: Step{Duration: 60 * time.Second},
Signal: telemetrytypes.SignalTraces,
Source: telemetrytypes.SourceUnspecified,
Aggregations: []TraceAggregation{
{
Expression: "count()",
Alias: "trace_count",
},
},
Disabled: false,
Filter: &Filter{
Expression: "service.name = 'frontend'",
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "timestamp",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionDesc,
},
},
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "trace_id",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Limit: 100,
Offset: 0,
Cursor: "cursor123",
LimitBy: &LimitBy{
Value: "10",
Keys: []string{
"service.name",
},
},
Having: &Having{
Expression: "count() > 100",
},
SecondaryAggregations: []SecondaryAggregation{
{
Limit: 10,
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "value",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionAsc,
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "region",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
},
},
Functions: []Function{
{
Name: FunctionNameTimeShift,
Args: []FunctionArg{
{
Name: "shift",
Value: "1h",
},
},
},
},
Legend: "{{service.name}}",
ShiftBy: 3600000,
}
// Create a copy
copied := original.Copy()
// Assert that values are equal
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.StepInterval, copied.StepInterval)
assert.Equal(t, original.Signal, copied.Signal)
assert.Equal(t, original.Source, copied.Source)
assert.Equal(t, original.Disabled, copied.Disabled)
assert.Equal(t, original.Limit, copied.Limit)
assert.Equal(t, original.Offset, copied.Offset)
assert.Equal(t, original.Cursor, copied.Cursor)
assert.Equal(t, original.Legend, copied.Legend)
assert.Equal(t, original.ShiftBy, copied.ShiftBy)
// Assert deep copies for slices and pointers
require.NotNil(t, copied.Aggregations)
assert.Equal(t, len(original.Aggregations), len(copied.Aggregations))
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
require.NotNil(t, copied.Filter)
assert.Equal(t, original.Filter.Expression, copied.Filter.Expression)
require.NotNil(t, copied.GroupBy)
assert.Equal(t, len(original.GroupBy), len(copied.GroupBy))
assert.Equal(t, original.GroupBy[0].Name, copied.GroupBy[0].Name)
require.NotNil(t, copied.Order)
assert.Equal(t, len(original.Order), len(copied.Order))
assert.Equal(t, original.Order[0].Key.Name, copied.Order[0].Key.Name)
require.NotNil(t, copied.SelectFields)
assert.Equal(t, len(original.SelectFields), len(copied.SelectFields))
assert.Equal(t, original.SelectFields[0].Name, copied.SelectFields[0].Name)
require.NotNil(t, copied.LimitBy)
assert.Equal(t, original.LimitBy.Value, copied.LimitBy.Value)
assert.Equal(t, len(original.LimitBy.Keys), len(copied.LimitBy.Keys))
require.NotNil(t, copied.Having)
assert.Equal(t, original.Having.Expression, copied.Having.Expression)
require.NotNil(t, copied.SecondaryAggregations)
assert.Equal(t, len(original.SecondaryAggregations), len(copied.SecondaryAggregations))
assert.Equal(t, original.SecondaryAggregations[0].Limit, copied.SecondaryAggregations[0].Limit)
require.NotNil(t, copied.Functions)
assert.Equal(t, len(original.Functions), len(copied.Functions))
assert.Equal(t, original.Functions[0].Name, copied.Functions[0].Name)
// Verify independence - modify copied and ensure original is unchanged
copied.Name = "B"
assert.Equal(t, "A", original.Name)
copied.Aggregations[0].Expression = "sum()"
assert.Equal(t, "count()", original.Aggregations[0].Expression)
copied.Filter.Expression = "modified"
assert.Equal(t, "service.name = 'frontend'", original.Filter.Expression)
copied.GroupBy[0].Name = "modified"
assert.Equal(t, "service.name", original.GroupBy[0].Name)
copied.Order[0].Key.Name = "modified"
assert.Equal(t, "timestamp", original.Order[0].Key.Name)
copied.SelectFields[0].Name = "modified"
assert.Equal(t, "trace_id", original.SelectFields[0].Name)
copied.LimitBy.Value = "999"
assert.Equal(t, "10", original.LimitBy.Value)
copied.Having.Expression = "modified"
assert.Equal(t, "count() > 100", original.Having.Expression)
copied.SecondaryAggregations[0].Limit = 999
assert.Equal(t, 10, original.SecondaryAggregations[0].Limit)
copied.Functions[0].Name = FunctionNameAbsolute
assert.Equal(t, FunctionNameTimeShift, original.Functions[0].Name)
})
t.Run("copy with nil fields", func(t *testing.T) {
original := QueryBuilderQuery[TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
assert.Nil(t, copied.Aggregations)
assert.Nil(t, copied.Filter)
assert.Nil(t, copied.GroupBy)
assert.Nil(t, copied.Order)
assert.Nil(t, copied.SelectFields)
assert.Nil(t, copied.LimitBy)
assert.Nil(t, copied.Having)
assert.Nil(t, copied.SecondaryAggregations)
assert.Nil(t, copied.Functions)
})
t.Run("copy metric aggregation query", func(t *testing.T) {
original := QueryBuilderQuery[MetricAggregation]{
Name: "M",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []MetricAggregation{
{
MetricName: "cpu_usage",
SpaceAggregation: metrictypes.SpaceAggregationAvg,
TimeAggregation: metrictypes.TimeAggregationAvg,
},
},
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
require.NotNil(t, copied.Aggregations)
assert.Equal(t, original.Aggregations[0].MetricName, copied.Aggregations[0].MetricName)
assert.Equal(t, original.Aggregations[0].SpaceAggregation, copied.Aggregations[0].SpaceAggregation)
// Verify independence
copied.Aggregations[0].MetricName = "modified"
assert.Equal(t, "cpu_usage", original.Aggregations[0].MetricName)
})
t.Run("copy log aggregation query", func(t *testing.T) {
original := QueryBuilderQuery[LogAggregation]{
Name: "L",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{
Expression: "count()",
Alias: "log_count",
},
},
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
require.NotNil(t, copied.Aggregations)
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
// Verify independence
copied.Aggregations[0].Expression = "sum()"
assert.Equal(t, "count()", original.Aggregations[0].Expression)
})
}
func TestQueryBuilderQuery_Normalize(t *testing.T) {
t.Run("normalize select fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
{
Name: "span.name",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
}
query.Normalize()
// Normalize only changes FieldContext, not the Name
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
assert.Equal(t, "span.name", query.SelectFields[1].Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SelectFields[1].FieldContext)
})
t.Run("normalize group by fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
}
query.Normalize()
assert.Equal(t, "service.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
})
t.Run("normalize order by fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "timestamp",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionDesc,
},
},
}
query.Normalize()
assert.Equal(t, "timestamp", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
})
t.Run("normalize secondary aggregations", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SecondaryAggregations: []SecondaryAggregation{
{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "value",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionAsc,
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "region",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
},
},
}
query.Normalize()
assert.Equal(t, "value", query.SecondaryAggregations[0].Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SecondaryAggregations[0].Order[0].Key.FieldContext)
assert.Equal(t, "region", query.SecondaryAggregations[0].GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SecondaryAggregations[0].GroupBy[0].FieldContext)
})
t.Run("normalize with nil fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
}
// Should not panic
query.Normalize()
assert.Equal(t, "A", query.Name)
})
t.Run("normalize all fields together", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SelectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "host.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "duration",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
SecondaryAggregations: []SecondaryAggregation{
{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "count",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "status.code",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
},
}
query.Normalize()
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, "host.name", query.GroupBy[0].Name)
assert.Equal(t, "duration", query.Order[0].Key.Name)
assert.Equal(t, "count", query.SecondaryAggregations[0].Order[0].Key.Name)
assert.Equal(t, "status.code", query.SecondaryAggregations[0].GroupBy[0].Name)
})
}
func TestQueryBuilderQuery_UnmarshalJSON(t *testing.T) {
t.Run("valid trace query", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"stepInterval": 60,
"aggregations": [{
"expression": "count()",
"alias": "trace_count"
}],
"filter": {
"expression": "service.name = 'frontend'"
},
"groupBy": [{
"name": "service.name",
"fieldContext": "resource"
}],
"order": [{
"key": {
"name": "service.name",
"fieldContext": "resource"
},
"direction": "desc"
}],
"limit": 100
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "A", query.Name)
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "count()", query.Aggregations[0].Expression)
assert.Equal(t, "trace_count", query.Aggregations[0].Alias)
require.NotNil(t, query.Filter)
assert.Equal(t, "service.name = 'frontend'", query.Filter.Expression)
assert.Equal(t, 1, len(query.GroupBy))
assert.Equal(t, "service.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
assert.Equal(t, 1, len(query.Order))
assert.Equal(t, "service.name", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.Order[0].Key.FieldContext)
assert.Equal(t, OrderDirectionDesc, query.Order[0].Direction)
assert.Equal(t, 100, query.Limit)
})
t.Run("valid metric query", func(t *testing.T) {
jsonData := `{
"name": "M",
"signal": "metrics",
"stepInterval": "1m",
"aggregations": [{
"metricName": "cpu_usage",
"spaceAggregation": "avg",
"timeAggregation": "avg"
}]
}`
var query QueryBuilderQuery[MetricAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "M", query.Name)
assert.Equal(t, telemetrytypes.SignalMetrics, query.Signal)
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "cpu_usage", query.Aggregations[0].MetricName)
})
t.Run("valid log query", func(t *testing.T) {
jsonData := `{
"name": "L",
"signal": "logs",
"aggregations": [{
"expression": "count()",
"alias": "log_count"
}]
}`
var query QueryBuilderQuery[LogAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "L", query.Name)
assert.Equal(t, telemetrytypes.SignalLogs, query.Signal)
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "count()", query.Aggregations[0].Expression)
})
t.Run("unknown field error", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"unknownField": "value"
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown field")
})
t.Run("query with all optional fields", func(t *testing.T) {
// NOTE: This json payload is not realistic, just for testing all fields
jsonData := `{
"name": "A",
"signal": "traces",
"stepInterval": "5m",
"source": "traces",
"aggregations": [{
"expression": "count()",
"alias": "span.count"
}],
"disabled": true,
"filter": {
"expression": "service.name = 'api'"
},
"groupBy": [{
"name": "service.name",
"fieldContext": "resource"
}],
"order": [{
"key": {
"name": "timestamp",
"fieldContext": "span"
},
"direction": "asc"
}],
"selectFields": [{
"name": "trace_id",
"fieldContext": "span"
}],
"limit": 50,
"offset": 10,
"cursor": "cursor123",
"limitBy": {
"value": "5",
"keys": ["service.name"]
},
"having": {
"expression": "count() > 10"
},
"secondaryAggregations": [{
"limit": 20,
"order": [{
"key": {
"name": "value",
"fieldContext": "span"
},
"direction": "desc"
}],
"groupBy": [{
"name": "region",
"fieldContext": "resource"
}]
}],
"functions": [{
"name": "timeShift",
"args": [{
"name": "shift",
"value": "1h"
}]
}],
"legend": "{{service.name}}"
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "A", query.Name)
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
assert.Equal(t, int64(300000), query.StepInterval.Milliseconds())
// Source is set in the JSON, so it should be "traces", not SourceUnspecified
assert.Equal(t, "traces", query.Source.String.StringValue())
assert.True(t, query.Disabled)
assert.Equal(t, query.Aggregations[0].Expression, "count()")
assert.Equal(t, query.Aggregations[0].Alias, "span.count")
assert.NotNil(t, query.Filter)
assert.NotNil(t, query.GroupBy)
assert.NotNil(t, query.Order)
assert.NotNil(t, query.SelectFields)
assert.Equal(t, 50, query.Limit)
assert.Equal(t, 10, query.Offset)
assert.Equal(t, "cursor123", query.Cursor)
assert.NotNil(t, query.LimitBy)
assert.NotNil(t, query.Having)
assert.NotNil(t, query.SecondaryAggregations)
assert.NotNil(t, query.Functions)
assert.Equal(t, "{{service.name}}", query.Legend)
})
t.Run("normalization happens during unmarshaling", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"selectFields": [{
"name": "resource.service.name"
}],
"groupBy": [{
"name": "resource.host.name"
}],
"order": [{
"key": {
"name": "span.duration"
},
"direction": "desc"
}]
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
// FieldContext should be normalized, Name should remain as-is
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
assert.Equal(t, "host.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
assert.Equal(t, "duration", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
})
}

View File

@@ -1,61 +0,0 @@
package querybuildertypesv5
import (
"bytes"
"encoding/json"
"maps"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
)
var _ cachetypes.Cacheable = (*CachedData)(nil)
type CachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats ExecStats `json:"stats"`
}
func (c *CachedBucket) Clone() *CachedBucket {
return &CachedBucket{
StartMs: c.StartMs,
EndMs: c.EndMs,
Type: c.Type,
Value: bytes.Clone(c.Value),
Stats: ExecStats{
RowsScanned: c.Stats.RowsScanned,
BytesScanned: c.Stats.BytesScanned,
DurationMS: c.Stats.DurationMS,
StepIntervals: maps.Clone(c.Stats.StepIntervals),
},
}
}
// CachedData represents the full cached data for a query
type CachedData struct {
Buckets []*CachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *CachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *CachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
func (c *CachedData) Clone() cachetypes.Cacheable {
clonedCachedData := new(CachedData)
clonedCachedData.Buckets = make([]*CachedBucket, len(c.Buckets))
for i := range c.Buckets {
clonedCachedData.Buckets[i] = c.Buckets[i].Clone()
}
clonedCachedData.Warnings = make([]string, len(c.Warnings))
copy(clonedCachedData.Warnings, c.Warnings)
return clonedCachedData
}

View File

@@ -1,87 +0,0 @@
package querybuildertypesv5
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func createBuckets_TimeSeries(numBuckets int) []*CachedBucket {
buckets := make([]*CachedBucket, numBuckets)
for i := 0; i < numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
timeSeriesData := &TimeSeriesData{
QueryName: "A",
Aggregations: []*AggregationBucket{
{
Index: 0,
Series: []*TimeSeries{
{
Labels: []*Label{
{Key: telemetrytypes.TelemetryFieldKey{Name: "service"}, Value: "test"},
},
Values: []*TimeSeriesValue{
{Timestamp: 1672563720000, Value: 1, Partial: true}, // 12:02
{Timestamp: 1672563900000, Value: 2}, // 12:05
{Timestamp: 1672564200000, Value: 2.5}, // 12:10
{Timestamp: 1672564500000, Value: 2.6}, // 12:15
{Timestamp: 1672566600000, Value: 2.9}, // 12:50
{Timestamp: 1672566900000, Value: 3}, // 12:55
{Timestamp: 1672567080000, Value: 4, Partial: true}, // 12:58
},
},
},
},
},
}
value, err := json.Marshal(timeSeriesData)
if err != nil {
panic(err)
}
buckets[i] = &CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: RequestTypeTimeSeries,
Value: json.RawMessage(value),
Stats: ExecStats{
RowsScanned: uint64(i * 500),
BytesScanned: uint64(i * 10000),
DurationMS: uint64(i * 1000),
},
}
}
return buckets
}
func BenchmarkCachedData_JSONMarshal_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(data)
assert.NoError(b, err)
}
}
func BenchmarkCachedData_Clone_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = data.Clone()
}
}

View File

@@ -47,19 +47,19 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
switch header.Signal {
case telemetrytypes.SignalTraces:
var spec QueryBuilderQuery[TraceAggregation]
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
return wrapUnmarshalError(err, "invalid trace builder query spec: %v", err)
}
q.Spec = spec
case telemetrytypes.SignalLogs:
var spec QueryBuilderQuery[LogAggregation]
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
return wrapUnmarshalError(err, "invalid log builder query spec: %v", err)
}
q.Spec = spec
case telemetrytypes.SignalMetrics:
var spec QueryBuilderQuery[MetricAggregation]
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
return wrapUnmarshalError(err, "invalid metric builder query spec: %v", err)
}
q.Spec = spec
@@ -75,7 +75,6 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
case QueryTypeFormula:
var spec QueryBuilderFormula
// TODO: use json.Unmarshal here after implementing custom unmarshaler for QueryBuilderFormula
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "formula spec"); err != nil {
return wrapUnmarshalError(err, "invalid formula spec: %v", err)
}
@@ -83,7 +82,6 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
case QueryTypeJoin:
var spec QueryBuilderJoin
// TODO: use json.Unmarshal here after implementing custom unmarshaler for QueryBuilderJoin
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "join spec"); err != nil {
return wrapUnmarshalError(err, "invalid join spec: %v", err)
}
@@ -91,14 +89,13 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
case QueryTypeTraceOperator:
var spec QueryBuilderTraceOperator
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "trace operator spec"); err != nil {
return wrapUnmarshalError(err, "invalid trace operator spec: %v", err)
}
q.Spec = spec
case QueryTypePromQL:
var spec PromQuery
// TODO: use json.Unmarshal here after implementing custom unmarshaler for PromQuery
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "PromQL spec"); err != nil {
return wrapUnmarshalError(err, "invalid PromQL spec: %v", err)
}
@@ -106,7 +103,6 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
case QueryTypeClickHouseSQL:
var spec ClickHouseQuery
// TODO: use json.Unmarshal here after implementing custom unmarshaler for ClickHouseQuery
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "ClickHouse SQL spec"); err != nil {
return wrapUnmarshalError(err, "invalid ClickHouse SQL spec: %v", err)
}

View File

@@ -366,45 +366,6 @@ func (q QueryBuilderTraceOperator) Copy() QueryBuilderTraceOperator {
return c
}
// UnmarshalJSON implements custom JSON unmarshaling to disallow unknown fields
func (q *QueryBuilderTraceOperator) UnmarshalJSON(data []byte) error {
// Define a type alias to avoid infinite recursion
type Alias QueryBuilderTraceOperator
var temp Alias
// Use UnmarshalJSONWithContext for better error messages
if err := UnmarshalJSONWithContext(data, &temp, "query spec"); err != nil {
return err
}
// Copy the decoded values back to the original struct
*q = QueryBuilderTraceOperator(temp)
// Nomarlize the query after unmarshaling
q.Normalize()
return nil
}
// Normalize normalizes all the field keys in the query
func (q *QueryBuilderTraceOperator) Normalize() {
// normalize select fields
for idx := range q.SelectFields {
q.SelectFields[idx].Normalize()
}
// normalize group by fields
for idx := range q.GroupBy {
q.GroupBy[idx].Normalize()
}
// normalize order by fields
for idx := range q.Order {
q.Order[idx].Key.Normalize()
}
}
// ValidateUniqueTraceOperator ensures only one trace operator exists in queries
func ValidateUniqueTraceOperator(queries []QueryEnvelope) error {
traceOperatorCount := 0

View File

@@ -51,118 +51,63 @@ func (f TelemetryFieldKey) String() string {
return sb.String()
}
func (f TelemetryFieldKey) Text() string {
return TelemetryFieldKeyToText(&f)
}
// Normalize parses and normalizes a TelemetryFieldKey by extracting
// the field context and data type from the field name if they are not already specified.
// This function modifies the key in place.
//
// Example:
//
// key := &TelemetryFieldKey{Name: "resource.service.name:string"}
// key.Normalize()
// // Result: Name: "service.name", FieldContext: FieldContextResource, FieldDataType: FieldDataTypeString
func (f *TelemetryFieldKey) Normalize() {
// Step 1: Parse data type from the right (after the last ":") if not already specified
if f.FieldDataType == FieldDataTypeUnspecified {
if colonIdx := strings.LastIndex(f.Name, ":"); colonIdx != -1 {
potentialDataType := f.Name[colonIdx+1:]
if dt, ok := fieldDataTypes[potentialDataType]; ok && dt != FieldDataTypeUnspecified {
f.FieldDataType = dt
f.Name = f.Name[:colonIdx]
}
}
}
// Step 2: Parse field context from the left if not already specified
if f.FieldContext == FieldContextUnspecified {
if dotIdx := strings.Index(f.Name, "."); dotIdx != -1 {
potentialContext := f.Name[:dotIdx]
if fc, ok := fieldContexts[potentialContext]; ok && fc != FieldContextUnspecified {
f.Name = f.Name[dotIdx+1:]
f.FieldContext = fc
// Step 2a: Handle special case for log.body.* fields
if f.FieldContext == FieldContextLog && strings.HasPrefix(f.Name, BodyJSONStringSearchPrefix) {
f.FieldContext = FieldContextBody
f.Name = strings.TrimPrefix(f.Name, BodyJSONStringSearchPrefix)
}
}
}
}
}
// GetFieldKeyFromKeyText returns a TelemetryFieldKey from a key text.
// The key text is expected to be in the format of `fieldContext.fieldName:fieldDataType` in the search query.
// Both fieldContext and :fieldDataType are optional.
// fieldName can contain dots and can start with a dot (e.g., ".http_code").
// Special cases:
// - When key exactly matches a field context name (e.g., "body", "attribute"), use unspecified context
// - When key starts with "body." prefix, use "body" as context with remainder as field name
func GetFieldKeyFromKeyText(key string) TelemetryFieldKey {
var explicitFieldDataType FieldDataType = FieldDataTypeUnspecified
var fieldName string
// Step 1: Parse data type from the right (after the last ":")
var keyWithoutDataType string
if colonIdx := strings.LastIndex(key, ":"); colonIdx != -1 {
potentialDataType := key[colonIdx+1:]
if dt, ok := fieldDataTypes[potentialDataType]; ok && dt != FieldDataTypeUnspecified {
explicitFieldDataType = dt
keyWithoutDataType = key[:colonIdx]
} else {
// No valid data type found, treat the entire key as the field name
keyWithoutDataType = key
keyTextParts := strings.Split(key, ".")
var explicitFieldContextProvided, explicitFieldDataTypeProvided bool
var explicitFieldContext FieldContext
var explicitFieldDataType FieldDataType
var ok bool
if len(keyTextParts) > 1 {
explicitFieldContext, ok = fieldContexts[keyTextParts[0]]
if ok && explicitFieldContext != FieldContextUnspecified {
explicitFieldContextProvided = true
}
}
if explicitFieldContextProvided {
keyTextParts = keyTextParts[1:]
}
// check if there is a field data type provided
if len(keyTextParts) >= 1 {
lastPart := keyTextParts[len(keyTextParts)-1]
lastPartParts := strings.Split(lastPart, ":")
if len(lastPartParts) > 1 {
explicitFieldDataType, ok = fieldDataTypes[lastPartParts[1]]
if ok && explicitFieldDataType != FieldDataTypeUnspecified {
explicitFieldDataTypeProvided = true
}
}
if explicitFieldDataTypeProvided {
keyTextParts[len(keyTextParts)-1] = lastPartParts[0]
}
}
realKey := strings.Join(keyTextParts, ".")
fieldKeySelector := TelemetryFieldKey{
Name: realKey,
}
if explicitFieldContextProvided {
fieldKeySelector.FieldContext = explicitFieldContext
} else {
keyWithoutDataType = key
fieldKeySelector.FieldContext = FieldContextUnspecified
}
// Step 2: Parse field context from the left
if dotIdx := strings.Index(keyWithoutDataType, "."); dotIdx != -1 {
potentialContext := keyWithoutDataType[:dotIdx]
if fc, ok := fieldContexts[potentialContext]; ok && fc != FieldContextUnspecified {
fieldName = keyWithoutDataType[dotIdx+1:]
// Step 2a: Handle special case for log.body.* fields
if fc == FieldContextLog && strings.HasPrefix(fieldName, BodyJSONStringSearchPrefix) {
fc = FieldContextBody
fieldName = strings.TrimPrefix(fieldName, BodyJSONStringSearchPrefix)
}
return TelemetryFieldKey{
Name: fieldName,
FieldContext: fc,
FieldDataType: explicitFieldDataType,
}
}
if explicitFieldDataTypeProvided {
fieldKeySelector.FieldDataType = explicitFieldDataType
} else {
fieldKeySelector.FieldDataType = FieldDataTypeUnspecified
}
// Step 3: No context found, entire key is the field name
return TelemetryFieldKey{
Name: keyWithoutDataType,
FieldContext: FieldContextUnspecified,
FieldDataType: explicitFieldDataType,
}
}
func TelemetryFieldKeyToText(key *TelemetryFieldKey) string {
var sb strings.Builder
if key.FieldContext != FieldContextUnspecified {
sb.WriteString(key.FieldContext.StringValue())
sb.WriteString(".")
}
sb.WriteString(key.Name)
if key.FieldDataType != FieldDataTypeUnspecified {
sb.WriteString(":")
sb.WriteString(key.FieldDataType.StringValue())
}
return sb.String()
return fieldKeySelector
}
func FieldKeyToMaterializedColumnName(key *TelemetryFieldKey) string {

View File

@@ -83,314 +83,12 @@ func TestGetFieldKeyFromKeyText(t *testing.T) {
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for log.body.status - should use log context with body.status as field name
{
keyText: "log.body.status",
expected: TelemetryFieldKey{
Name: "status",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for log.body. with data type
{
keyText: "log.body.status_code:int",
expected: TelemetryFieldKey{
Name: "status_code",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeNumber,
},
},
// Test case for log.body. with nested field name
{
keyText: "log.body.http.status.code",
expected: TelemetryFieldKey{
Name: "http.status.code",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for body. prefix - should use body as context
{
keyText: "body.http.status.code:int",
expected: TelemetryFieldKey{
Name: "http.status.code",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeNumber,
},
},
// Test case for body. prefix without data type - should use body as context
{
keyText: "body.status",
expected: TelemetryFieldKey{
Name: "status",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for body. prefix with array data type - should use body as context
{
keyText: "body.tags:[]string",
expected: TelemetryFieldKey{
Name: "tags",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeArrayString,
},
},
// Test case for body. prefix with array data type (int64)
{
keyText: "body.ids:[]int64",
expected: TelemetryFieldKey{
Name: "ids",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeArrayInt64,
},
},
// Test case for body. prefix with nested field and array data type
{
keyText: "body.http.headers:[]string",
expected: TelemetryFieldKey{
Name: "http.headers",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeArrayString,
},
},
// Test case for just "body" - should use unspecified context with body as field name
{
keyText: "body",
expected: TelemetryFieldKey{
Name: "body",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for just "body" with data type
{
keyText: "body:string",
expected: TelemetryFieldKey{
Name: "body",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeString,
},
},
// Test case for log.body (without trailing dot) - should keep log context
{
keyText: "log.body",
expected: TelemetryFieldKey{
Name: "body",
FieldContext: FieldContextLog,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for log.body with data type - should keep log context
{
keyText: "log.body:string",
expected: TelemetryFieldKey{
Name: "body",
FieldContext: FieldContextLog,
FieldDataType: FieldDataTypeString,
},
},
// Test case for field name with dots and no context
{
keyText: "http.status.code",
expected: TelemetryFieldKey{
Name: "http.status.code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for field name with dots and data type but no context
{
keyText: "http.status.code:int",
expected: TelemetryFieldKey{
Name: "http.status.code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeNumber,
},
},
// Test case for just field name
{
keyText: "fieldName",
expected: TelemetryFieldKey{
Name: "fieldName",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for just field name with data type
{
keyText: "fieldName:string",
expected: TelemetryFieldKey{
Name: "fieldName",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeString,
},
},
// Test case for field starting with dot
{
keyText: ".http_code",
expected: TelemetryFieldKey{
Name: ".http_code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeUnspecified,
},
},
// Test case for field starting with dot and with data type
{
keyText: ".http_code:int",
expected: TelemetryFieldKey{
Name: ".http_code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeNumber,
},
},
// Test case for field starting with dot and nested field name
{
keyText: ".http.status.code:int",
expected: TelemetryFieldKey{
Name: ".http.status.code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeNumber,
},
},
}
for _, testCase := range testCases {
result := GetFieldKeyFromKeyText(testCase.keyText)
if !reflect.DeepEqual(result, testCase.expected) {
t.Errorf("For key '%s': expected %v, got %v", testCase.keyText, testCase.expected, result)
t.Errorf("expected %v, got %v", testCase.expected, result)
}
}
}
func TestNormalize(t *testing.T) {
testCases := []struct {
name string
input TelemetryFieldKey
expected TelemetryFieldKey
}{
{
name: "Normalize key with context and data type in name",
input: TelemetryFieldKey{
Name: "resource.service.name:string",
},
expected: TelemetryFieldKey{
Name: "service.name",
FieldContext: FieldContextResource,
FieldDataType: FieldDataTypeString,
},
},
{
name: "Normalize key with existing context and data type",
input: TelemetryFieldKey{
Name: "service.name",
FieldContext: FieldContextResource,
FieldDataType: FieldDataTypeString,
},
expected: TelemetryFieldKey{
Name: "service.name",
FieldContext: FieldContextResource,
FieldDataType: FieldDataTypeString,
},
},
{
name: "Normalize body field",
input: TelemetryFieldKey{
Name: "body.status:int",
},
expected: TelemetryFieldKey{
Name: "status",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeNumber,
},
},
{
name: "Normalize log.body.* field",
input: TelemetryFieldKey{
Name: "log.body.status",
},
expected: TelemetryFieldKey{
Name: "status",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeUnspecified,
},
},
{
name: "Normalize field with no context",
input: TelemetryFieldKey{
Name: "http.status.code:int",
},
expected: TelemetryFieldKey{
Name: "http.status.code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeNumber,
},
},
{
name: "Normalize exact body keyword",
input: TelemetryFieldKey{
Name: "body",
},
expected: TelemetryFieldKey{
Name: "body",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeUnspecified,
},
},
{
name: "Normalize span field",
input: TelemetryFieldKey{
Name: "span.kind:string",
},
expected: TelemetryFieldKey{
Name: "kind",
FieldContext: FieldContextSpan,
FieldDataType: FieldDataTypeString,
},
},
{
name: "Normalize attribute field",
input: TelemetryFieldKey{
Name: "attribute.http.method",
},
expected: TelemetryFieldKey{
Name: "http.method",
FieldContext: FieldContextAttribute,
FieldDataType: FieldDataTypeUnspecified,
},
},
{
name: "Normalize field starting with dot",
input: TelemetryFieldKey{
Name: ".http_code:int",
},
expected: TelemetryFieldKey{
Name: ".http_code",
FieldContext: FieldContextUnspecified,
FieldDataType: FieldDataTypeNumber,
},
},
{
name: "Normalize array data type field",
input: TelemetryFieldKey{
Name: "body.tags:[]string",
},
expected: TelemetryFieldKey{
Name: "tags",
FieldContext: FieldContextBody,
FieldDataType: FieldDataTypeArrayString,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
key := tc.input
key.Normalize()
if !reflect.DeepEqual(key, tc.expected) {
t.Errorf("Expected %v, got %v", tc.expected, key)
}
})
}
}

View File

@@ -377,7 +377,7 @@ def idp_login(driver: webdriver.Chrome) -> Callable[[str, str], None]:
# Fill the email in username field
username_field = wait.until(EC.element_to_be_clickable((By.ID, "username")))
username_field.send_keys(email)
# Fill the password in password field
password_field = wait.until(EC.element_to_be_clickable((By.ID, "password")))
password_field.send_keys(password)

View File

@@ -66,7 +66,7 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
"SIGNOZ_PROMETHEUS_ACTIVE__QUERY__TRACKER_ENABLED": False,
"SIGNOZ_GATEWAY_URL": gateway.container_configs["8080"].base(),
"SIGNOZ_TOKENIZER_JWT_SECRET": "secret",
"SIGNOZ_GLOBAL_INGESTION__URL": "https://ingest.test.signoz.cloud",
"SIGNOZ_GLOBAL_INGESTION__URL": "https://ingest.test.signoz.cloud"
}
| sqlstore.env
| clickhouse.env

View File

@@ -2,7 +2,6 @@ from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
import pytest
import requests
from fixtures import types
@@ -12,9 +11,7 @@ from fixtures.querier import (
assert_minutely_bucket_values,
find_named_result,
index_series_by_label,
make_query_request,
)
from src.querier.util import assert_identical_query_response
def test_logs_list(
@@ -399,122 +396,6 @@ def test_logs_list(
assert "d-001" in values
@pytest.mark.parametrize(
"order_by_context,expected_order",
####
# Tests:
# 1. Query logs ordered by attribute.service.name descending
# 2. Query logs ordered by resource.service.name descending
# 3. Query logs ordered by service.name descending
###
[
pytest.param("attribute", ["log-002", "log-001", "log-004", "log-003"]),
pytest.param("resource", ["log-003", "log-004", "log-001", "log-002"]),
pytest.param("", ["log-002", "log-001", "log-003", "log-004"]),
],
)
def test_logs_list_with_order_by(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
order_by_context: str,
expected_order: List[str],
) -> None:
"""
Setup:
Insert 3 logs with service.name in attributes and resources
"""
attribute_resource_pair = [
[{"id": "log-001", "service.name": "c"}, {}],
[{"id": "log-002", "service.name": "d"}, {}],
[{"id": "log-003"}, {"service.name": "b"}],
[{"id": "log-004"}, {"service.name": "a"}],
]
insert_logs(
[
Logs(
timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=3),
attributes=attribute_resource_pair[i][0],
resources=attribute_resource_pair[i][1],
body="Log with DEBUG severity",
severity_text="DEBUG",
)
for i in range(4)
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = {
"type": "builder_query",
"spec": {
"name": "A",
"signal": "logs",
"order": [
{
"key": {
"name": "service.name",
"fieldContext": order_by_context,
},
"direction": "desc",
}
],
},
}
query_with_inline_context = {
"type": "builder_query",
"spec": {
"name": "A",
"signal": "logs",
"order": [
{
"key": {
"name": f"{order_by_context + '.' if order_by_context else ''}service.name",
},
"direction": "desc",
}
],
},
}
response = make_query_request(
signoz,
token,
start_ms=int(
(datetime.now(tz=timezone.utc) - timedelta(minutes=1)).timestamp() * 1000
),
end_ms=int(datetime.now(tz=timezone.utc).timestamp() * 1000),
request_type="raw",
queries=[query],
)
# Verify that both queries return the same results with specifying context with key name
response_with_inline_context = make_query_request(
signoz,
token,
start_ms=int(
(datetime.now(tz=timezone.utc) - timedelta(minutes=1)).timestamp() * 1000
),
end_ms=int(datetime.now(tz=timezone.utc).timestamp() * 1000),
request_type="raw",
queries=[query_with_inline_context],
)
assert_identical_query_response(response, response_with_inline_context)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
results = response.json()["data"]["data"]["results"]
rows = results[0]["rows"]
ids = [row["data"]["attributes_string"].get("id", "") for row in rows]
assert ids == expected_order
def test_logs_time_series_count(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
@@ -944,44 +825,6 @@ def test_logs_time_series_count(
},
)
response_with_inline_context = make_query_request(
signoz,
token,
start_ms=int(
(
datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
- timedelta(minutes=5)
).timestamp()
* 1000
),
end_ms=int(
datetime.now(tz=timezone.utc).replace(second=0, microsecond=0).timestamp()
* 1000
),
request_type="time_series",
queries=[
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "logs",
"stepInterval": 60,
"disabled": False,
"groupBy": [
{
"name": "resource.host.name:string",
}
],
"order": [{"key": {"name": "host.name"}, "direction": "desc"}],
"having": {"expression": ""},
"aggregations": [{"expression": "count()"}],
},
}
],
)
assert_identical_query_response(response, response_with_inline_context)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"

View File

@@ -2,7 +2,6 @@ from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, Dict, List
import pytest
import requests
from fixtures import types
@@ -11,10 +10,8 @@ from fixtures.querier import (
assert_minutely_bucket_values,
find_named_result,
index_series_by_label,
make_query_request,
)
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
from src.querier.util import assert_identical_query_response
def test_traces_list(
@@ -218,59 +215,6 @@ def test_traces_list(
},
)
# Query results with context appended to key names
response_with_inline_context = make_query_request(
signoz,
token,
start_ms=int(
(datetime.now(tz=timezone.utc) - timedelta(minutes=5)).timestamp() * 1000
),
end_ms=int(datetime.now(tz=timezone.utc).timestamp() * 1000),
request_type="raw",
queries=[
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"disabled": False,
"limit": 10,
"offset": 0,
"order": [
{"key": {"name": "timestamp"}, "direction": "desc"},
],
"selectFields": [
{
"name": "resource.service.name",
"fieldDataType": "string",
"signal": "traces",
},
{
"name": "span.name:string",
"signal": "traces",
},
{
"name": "span.duration_nano",
"signal": "traces",
},
{
"name": "span.http_method",
"signal": "traces",
},
{
"name": "span.response_status_code",
"signal": "traces",
},
],
"having": {"expression": ""},
"aggregations": [{"expression": "count()"}],
},
}
],
)
assert_identical_query_response(response, response_with_inline_context)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
@@ -476,236 +420,6 @@ def test_traces_list(
assert set(values) == set(["topic-service", "http-service"])
@pytest.mark.parametrize(
"order_by,aggregation_alias,expected_status",
[
# Case 1a: count by count()
pytest.param({"name": "count()"}, "count_", HTTPStatus.OK),
# Case 1b: count by count() with alias span.count_
pytest.param({"name": "count()"}, "span.count_", HTTPStatus.OK),
# Case 2a: count by count() with context specified in the key
pytest.param(
{"name": "count()", "fieldContext": "span"}, "count_", HTTPStatus.OK
),
# Case 2b: count by count() with context specified in the key with alias span.count_
pytest.param(
{"name": "count()", "fieldContext": "span"}, "span.count_", HTTPStatus.OK
),
# Case 3a: count by span.count() and context specified in the key [BAD REQUEST]
pytest.param(
{"name": "span.count()", "fieldContext": "span"},
"count_",
HTTPStatus.BAD_REQUEST,
),
# Case 3b: count by span.count() and context specified in the key with alias span.count_ [BAD REQUEST]
pytest.param(
{"name": "span.count()", "fieldContext": "span"},
"span.count_",
HTTPStatus.BAD_REQUEST,
),
# Case 4a: count by span.count() and context specified in the key
pytest.param(
{"name": "span.count()", "fieldContext": ""}, "count_", HTTPStatus.OK
),
# Case 4b: count by span.count() and context specified in the key with alias span.count_
pytest.param(
{"name": "span.count()", "fieldContext": ""}, "span.count_", HTTPStatus.OK
),
# Case 5a: count by count_
pytest.param({"name": "count_"}, "count_", HTTPStatus.OK),
# Case 5b: count by count_ with alias span.count_
pytest.param({"name": "count_"}, "count_", HTTPStatus.OK),
# Case 6a: count by span.count_
pytest.param({"name": "span.count_"}, "count_", HTTPStatus.OK),
# Case 6b: count by span.count_ with alias span.count_
pytest.param(
{"name": "span.count_"}, "span.count_", HTTPStatus.BAD_REQUEST
), # THIS SHOULD BE OK BUT FAILS DUE TO LIMITATION IN CURRENT IMPLEMENTATION
# Case 7a: count by span.count_ and context specified in the key [BAD REQUEST]
pytest.param(
{"name": "span.count_", "fieldContext": "span"},
"count_",
HTTPStatus.BAD_REQUEST,
),
# Case 7b: count by span.count_ and context specified in the key with alias span.count_
pytest.param(
{"name": "span.count_", "fieldContext": "span"},
"span.count_",
HTTPStatus.OK,
),
],
)
def test_traces_aggergate_order_by_count(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
order_by: Dict[str, str],
aggregation_alias: str,
expected_status: HTTPStatus,
) -> None:
"""
Setup:
Insert 4 traces with different attributes.
http-service: POST /integration -> SELECT, HTTP PATCH
topic-service: topic publish
Tests:
1. Query traces count for spans grouped by service.name and host.name
"""
http_service_trace_id = TraceIdGenerator.trace_id()
http_service_span_id = TraceIdGenerator.span_id()
http_service_db_span_id = TraceIdGenerator.span_id()
http_service_patch_span_id = TraceIdGenerator.span_id()
topic_service_trace_id = TraceIdGenerator.trace_id()
topic_service_span_id = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=3),
trace_id=http_service_trace_id,
span_id=http_service_span_id,
parent_span_id="",
name="POST /integration",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "http-service",
"os.type": "linux",
"host.name": "linux-000",
"cloud.provider": "integration",
"cloud.account.id": "000",
},
attributes={
"net.transport": "IP.TCP",
"http.scheme": "http",
"http.user_agent": "Integration Test",
"http.request.method": "POST",
"http.response.status_code": "200",
},
),
Traces(
timestamp=now - timedelta(seconds=3.5),
duration=timedelta(seconds=0.5),
trace_id=http_service_trace_id,
span_id=http_service_db_span_id,
parent_span_id=http_service_span_id,
name="SELECT",
kind=TracesKind.SPAN_KIND_CLIENT,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "http-service",
"os.type": "linux",
"host.name": "linux-000",
"cloud.provider": "integration",
"cloud.account.id": "000",
},
attributes={
"db.name": "integration",
"db.operation": "SELECT",
"db.statement": "SELECT * FROM integration",
},
),
Traces(
timestamp=now - timedelta(seconds=3),
duration=timedelta(seconds=1),
trace_id=http_service_trace_id,
span_id=http_service_patch_span_id,
parent_span_id=http_service_span_id,
name="HTTP PATCH",
kind=TracesKind.SPAN_KIND_CLIENT,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "http-service",
"os.type": "linux",
"host.name": "linux-000",
"cloud.provider": "integration",
"cloud.account.id": "000",
},
attributes={
"http.request.method": "PATCH",
"http.status_code": "404",
},
),
Traces(
timestamp=now - timedelta(seconds=1),
duration=timedelta(seconds=4),
trace_id=topic_service_trace_id,
span_id=topic_service_span_id,
parent_span_id="",
name="topic publish",
kind=TracesKind.SPAN_KIND_PRODUCER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "topic-service",
"os.type": "linux",
"host.name": "linux-001",
"cloud.provider": "integration",
"cloud.account.id": "001",
},
attributes={
"message.type": "SENT",
"messaging.operation": "publish",
"messaging.message.id": "001",
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = {
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"disabled": False,
"order": [{"key": {"name": "count()"}, "direction": "desc"}],
"aggregations": [{"expression": "count()", "alias": "count_"}],
},
}
# Query traces count for spans
query["spec"]["order"][0]["key"] = order_by
query["spec"]["aggregations"][0]["alias"] = aggregation_alias
response = make_query_request(
signoz,
token,
start_ms=int(
(datetime.now(tz=timezone.utc) - timedelta(minutes=5)).timestamp() * 1000
),
end_ms=int(datetime.now(tz=timezone.utc).timestamp() * 1000),
request_type="time_series",
queries=[query],
)
assert response.status_code == expected_status
if expected_status != HTTPStatus.OK:
return
assert response.json()["status"] == "success"
results = response.json()["data"]["data"]["results"]
assert len(results) == 1
aggregations = results[0]["aggregations"]
assert len(aggregations) == 1
series = aggregations[0]["series"]
assert len(series) == 1
assert series[0]["values"][0]["value"] == 4
def test_traces_fill_gaps(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument

View File

@@ -1,20 +0,0 @@
from http import HTTPStatus
import requests
def assert_identical_query_response(
response1: requests.Response, response2: requests.Response
) -> None:
"""
Assert that two query responses are identical in status and data.
"""
assert response1.status_code == response2.status_code, "Status codes do not match"
if response1.status_code == HTTPStatus.OK:
assert (
response1.json()["status"] == response2.json()["status"]
), "Response statuses do not match"
assert (
response1.json()["data"]["data"]["results"]
== response2.json()["data"]["data"]["results"]
), "Response data do not match"