Compare commits

..

2 Commits

Author SHA1 Message Date
Ashwin Bhatkal
012eed1576 fix: run query button props 2026-04-09 16:00:10 +05:30
Ashwin Bhatkal
5a7660069a fix: cancel not working in metrics summary page 2026-04-09 15:59:48 +05:30
50 changed files with 700 additions and 612 deletions

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
@@ -94,9 +93,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return signoz.NewAuditorProviderFactories()
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},

View File

@@ -8,7 +8,6 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
@@ -25,7 +24,6 @@ import (
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
@@ -135,13 +133,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)
},
func(licensing licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
factories := signoz.NewAuditorProviderFactories()
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler)

View File

@@ -364,34 +364,3 @@ serviceaccount:
analytics:
# toggle service account analytics
enabled: true
##################### Auditor #####################
auditor:
# Specifies the auditor provider to use.
# noop: discards all audit events (community default).
# otlphttp: exports audit events via OTLP HTTP (enterprise).
provider: noop
# The async channel capacity for audit events. Events are dropped when full (fail-open).
buffer_size: 1000
# The maximum number of events per export batch.
batch_size: 100
# The maximum time between export flushes.
flush_interval: 1s
otlphttp:
# The target scheme://host:port/path of the OTLP HTTP endpoint.
endpoint: http://localhost:4318/v1/logs
# Whether to use HTTP instead of HTTPS.
insecure: false
# The maximum duration for an export attempt.
timeout: 10s
# Additional HTTP headers sent with every export request.
headers: {}
retry:
# Whether to retry on transient failures.
enabled: true
# The initial wait time before the first retry.
initial_interval: 5s
# The upper bound on backoff interval.
max_interval: 30s
# The total maximum time spent retrying.
max_elapsed_time: 60s

View File

@@ -3309,7 +3309,7 @@ paths:
schema:
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
responses:
"201":
"200":
content:
application/json:
schema:
@@ -3322,7 +3322,7 @@ paths:
- status
- data
type: object
description: Created
description: OK
"401":
content:
application/json:
@@ -3683,11 +3683,6 @@ paths:
provider
operationId: ListServicesMetadata
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -3740,11 +3735,6 @@ paths:
description: This endpoint gets a service for the specified cloud provider
operationId: GetService
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true

View File

@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)

View File

@@ -28,7 +28,7 @@ import type {
CloudintegrationtypesPostableAgentCheckInDTO,
CloudintegrationtypesUpdatableAccountDTO,
CloudintegrationtypesUpdatableServiceDTO,
CreateAccount201,
CreateAccount200,
CreateAccountPathParameters,
DisconnectAccountPathParameters,
GetAccount200,
@@ -36,12 +36,10 @@ import type {
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
ListServicesMetadataParams,
ListServicesMetadataPathParameters,
RenderErrorResponseDTO,
UpdateAccountPathParameters,
@@ -262,7 +260,7 @@ export const createAccount = (
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAccount201>({
return GeneratedAPIInstance<CreateAccount200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -942,25 +940,19 @@ export const invalidateGetConnectionCredentials = async (
*/
export const listServicesMetadata = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
method: 'GET',
params,
signal,
});
};
export const getListServicesMetadataQueryKey = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services`,
...(params ? [params] : []),
] as const;
export const getListServicesMetadataQueryKey = ({
cloudProvider,
}: ListServicesMetadataPathParameters) => {
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
};
export const getListServicesMetadataQueryOptions = <
@@ -968,7 +960,6 @@ export const getListServicesMetadataQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -980,12 +971,11 @@ export const getListServicesMetadataQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListServicesMetadataQueryKey({ cloudProvider }, params);
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServicesMetadata>>
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
return {
queryKey,
@@ -1013,7 +1003,6 @@ export function useListServicesMetadata<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1024,7 +1013,6 @@ export function useListServicesMetadata<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServicesMetadataQueryOptions(
{ cloudProvider },
params,
options,
);
@@ -1043,11 +1031,10 @@ export function useListServicesMetadata<
export const invalidateListServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
options,
);
@@ -1060,24 +1047,21 @@ export const invalidateListServicesMetadata = async (
*/
export const getService = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
method: 'GET',
params,
signal,
});
};
export const getGetServiceQueryKey = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
) => {
export const getGetServiceQueryKey = ({
cloudProvider,
serviceId,
}: GetServicePathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
...(params ? [params] : []),
] as const;
};
@@ -1086,7 +1070,6 @@ export const getGetServiceQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1098,12 +1081,11 @@ export const getGetServiceQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
signal,
}) => getService({ cloudProvider, serviceId }, params, signal);
}) => getService({ cloudProvider, serviceId }, signal);
return {
queryKey,
@@ -1129,7 +1111,6 @@ export function useGetService<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1140,7 +1121,6 @@ export function useGetService<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceQueryOptions(
{ cloudProvider, serviceId },
params,
options,
);
@@ -1159,11 +1139,10 @@ export function useGetService<
export const invalidateGetService = async (
queryClient: QueryClient,
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
options,
);

View File

@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount201 = {
export type CreateAccount200 = {
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
/**
* @type string
@@ -3647,14 +3647,6 @@ export type GetConnectionCredentials200 = {
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadataParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
@@ -3667,14 +3659,6 @@ export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetServiceParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**

View File

@@ -25,11 +25,12 @@ export const REACT_QUERY_KEY = {
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
GET_ALL_ALERTS: 'GET_ALL_ALERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
ALERT_RULES_CHART_PREVIEW: 'ALERT_RULES_CHART_PREVIEW',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',

View File

@@ -1,9 +1,11 @@
import { useCallback, useMemo } from 'react';
import { useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import classNames from 'classnames';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricNameFromQueryData } from 'hooks/useGetYAxisUnit';
@@ -62,6 +64,13 @@ function QuerySection(): JSX.Element {
return currentQueryKey !== stagedQueryKey;
}, [currentQuery, alertType, thresholdState, stagedQuery]);
const queryClient = useQueryClient();
const isLoadingQueries =
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
}, [queryClient]);
const runQueryHandler = useCallback(() => {
// Reset the source param when the query is changed
// Then manually run the query
@@ -130,6 +139,8 @@ function QuerySection(): JSX.Element {
setQueryCategory={onQueryCategoryChange}
alertType={alertType}
runQuery={runQueryHandler}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
alertDef={alertDef}
panelType={PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -10,6 +10,7 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
import { Threshold } from 'container/CreateAlertV2/context/types';
@@ -185,7 +186,7 @@ function ChartPreview({
ENTITY_VERSION_V5,
{
queryKey: [
'chartPreview',
REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW,
userQueryKey || JSON.stringify(query),
selectedInterval,
minTime,

View File

@@ -29,6 +29,8 @@ function QuerySection({
setQueryCategory,
alertType,
runQuery,
isLoadingQueries,
handleCancelQuery,
alertDef,
panelType,
ruleId,
@@ -176,6 +178,8 @@ function QuerySection({
queryType: queryCategory,
});
}}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
</span>
}
@@ -195,7 +199,11 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<RunQueryBtn onStageRunQuery={runQuery} />
<RunQueryBtn
onStageRunQuery={runQuery}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
</span>
}
items={items}
@@ -237,6 +245,8 @@ interface QuerySectionProps {
setQueryCategory: (n: EQueryType) => void;
alertType: AlertTypes;
runQuery: VoidFunction;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
alertDef: AlertDef;
panelType: PANEL_TYPES;
ruleId: string;

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useIsFetching, useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -127,6 +127,11 @@ function FormAlertRules({
// use query client
const ruleCache = useQueryClient();
const isLoadingAlertQuery =
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
const handleCancelAlertQuery = useCallback(() => {
ruleCache.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
}, [ruleCache]);
const isNewRule = !ruleId || isEmpty(ruleId);
@@ -914,6 +919,8 @@ function FormAlertRules({
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={(): void => handleRunQuery()}
isLoadingQueries={isLoadingAlertQuery}
handleCancelQuery={handleCancelAlertQuery}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -1,43 +0,0 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
return (
<div className="query-section">
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
version="v4"
actions={
<ButtonWrapper>
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>
}
/>
</div>
);
}
export default QuerySection;

View File

@@ -1,7 +1,10 @@
import { Button } from 'antd';
import { useCallback } from 'react';
import { useIsFetching, useQueryClient } from 'react-query';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { QueryBuilder } from 'container/QueryBuilder';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -11,9 +14,16 @@ import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const queryClient = useQueryClient();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
const isLoadingQueries = useIsFetching([REACT_QUERY_KEY.GET_QUERY_RANGE]) > 0;
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries([REACT_QUERY_KEY.GET_QUERY_RANGE]);
}, [queryClient]);
return (
<div className="query-section">
<QueryBuilder
@@ -22,17 +32,16 @@ function QuerySection(): JSX.Element {
version="v4"
actions={
<ButtonWrapper>
<Button
onClick={(): void => {
<RunQueryBtn
onStageRunQuery={(): void => {
handleRunQuery();
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
/>
</ButtonWrapper>
}
/>

View File

@@ -1,9 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricMetadata } from 'api/generated/services/metrics';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -109,6 +111,11 @@ function Inspect({
reset,
} = useInspectMetrics(appliedMetricName);
const queryClient = useQueryClient();
const handleCancelInspectQuery = useCallback(() => {
queryClient.cancelQueries(REACT_QUERY_KEY.GET_INSPECT_METRICS_DETAILS);
}, [queryClient]);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
@@ -234,6 +241,8 @@ function Inspect({
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
currentQuery={currentQueryData}
setCurrentQuery={setCurrentQueryData}
isLoadingQueries={isInspectMetricsLoading}
handleCancelQuery={handleCancelInspectQuery}
/>
</div>
<div className="inspect-metrics-content-second-col">

View File

@@ -20,6 +20,8 @@ function QueryBuilder({
inspectMetricsTimeSeries,
currentQuery,
setCurrentQuery,
isLoadingQueries,
handleCancelQuery,
}: QueryBuilderProps): JSX.Element {
const applyInspectionOptions = useCallback(() => {
setAppliedMetricName(currentMetricName ?? '');
@@ -39,7 +41,11 @@ function QueryBuilder({
>
Query Builder
</Button>
<RunQueryBtn onStageRunQuery={applyInspectionOptions} />
<RunQueryBtn
onStageRunQuery={applyInspectionOptions}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
</div>
<Card className="inspect-metrics-query-builder-content">
<MetricNameSearch

View File

@@ -103,6 +103,8 @@ describe('QueryBuilder', () => {
filterExpression: '',
} as any,
setCurrentQuery: jest.fn(),
isLoadingQueries: false,
handleCancelQuery: jest.fn(),
};
beforeEach(() => {

View File

@@ -65,6 +65,8 @@ export interface QueryBuilderProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
}
export interface MetricNameSearchProps {

View File

@@ -12,6 +12,8 @@ function MetricsSearch({
currentQueryFilterExpression,
setCurrentQueryFilterExpression,
isLoading,
handleCancelQuery,
onRunQuery,
}: MetricsSearchProps): JSX.Element {
const handleOnChange = useCallback(
(expression: string): void => {
@@ -22,7 +24,8 @@ function MetricsSearch({
const handleStageAndRunQuery = useCallback(() => {
onChange(currentQueryFilterExpression);
}, [currentQueryFilterExpression, onChange]);
onRunQuery?.();
}, [currentQueryFilterExpression, onChange, onRunQuery]);
const handleRunQuery = useCallback(
(expression: string): void => {
@@ -53,6 +56,7 @@ function MetricsSearch({
<RunQueryBtn
onStageRunQuery={handleStageAndRunQuery}
isLoadingQueries={isLoading}
handleCancelQuery={handleCancelQuery}
/>
<div className="metrics-search-options">
<DateTimeSelectionV2

View File

@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
@@ -104,6 +105,8 @@ function Summary(): JSX.Element {
setCurrentQueryFilterExpression,
] = useState<string>(appliedFilterExpression);
const [isCancelled, setIsCancelled] = useState<boolean>(false);
useEffect(() => {
setCurrentQueryFilterExpression(appliedFilterExpression);
}, [appliedFilterExpression]);
@@ -164,6 +167,7 @@ function Summary(): JSX.Element {
isLoading: isGetMetricsStatsLoading,
isError: isGetMetricsStatsError,
error: metricsStatsError,
reset: resetMetricsStats,
} = useGetMetricsStats();
const {
@@ -172,6 +176,7 @@ function Summary(): JSX.Element {
isLoading: isGetMetricsTreemapLoading,
isError: isGetMetricsTreemapError,
error: metricsTreemapError,
reset: resetMetricsTreemap,
} = useGetMetricsTreemap();
const metricsStatsApiError = useMemo(
@@ -196,6 +201,40 @@ function Summary(): JSX.Element {
});
}, [metricsTreemapQuery, getMetricsTreemap]);
const handleCancelQuery = useCallback(() => {
resetMetricsStats();
resetMetricsTreemap();
setCurrentQueryFilterExpression(appliedFilterExpression);
setIsCancelled(true);
}, [
resetMetricsStats,
resetMetricsTreemap,
setCurrentQueryFilterExpression,
appliedFilterExpression,
]);
const handleRunQuery = useCallback(() => {
setIsCancelled(false);
getMetricsStats({
data: {
...metricsListQuery,
filter: { expression: currentQueryFilterExpression },
},
});
getMetricsTreemap({
data: {
...metricsTreemapQuery,
filter: { expression: currentQueryFilterExpression },
},
});
}, [
getMetricsStats,
getMetricsTreemap,
metricsListQuery,
metricsTreemapQuery,
currentQueryFilterExpression,
]);
const handleFilterChange = useCallback(
(expression: string) => {
const newFilters: TagFilter = {
@@ -330,11 +369,19 @@ function Summary(): JSX.Element {
!isGetMetricsTreemapLoading &&
!isGetMetricsTreemapError;
const isLoadingQueries =
isGetMetricsStatsLoading || isGetMetricsTreemapLoading;
const showFullScreenLoading =
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
isLoadingQueries &&
formattedMetricsData.length === 0 &&
!treeMapData?.data[heatmapView]?.length;
const showNoMetrics =
isMetricsListDataEmpty &&
isMetricsTreeMapDataEmpty &&
!appliedFilterExpression;
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
@@ -343,13 +390,30 @@ function Summary(): JSX.Element {
onChange={handleFilterChange}
currentQueryFilterExpression={currentQueryFilterExpression}
setCurrentQueryFilterExpression={setCurrentQueryFilterExpression}
isLoading={isGetMetricsStatsLoading || isGetMetricsTreemapLoading}
isLoading={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onRunQuery={handleRunQuery}
/>
{showFullScreenLoading ? (
<MetricsLoading />
) : isMetricsListDataEmpty &&
isMetricsTreeMapDataEmpty &&
!appliedFilterExpression ? (
) : isCancelled ? (
<div className="no-logs-container">
<div className="no-logs-container-content">
<img
className="eyes-emoji"
src="/Images/eyesEmoji.svg"
alt="eyes emoji"
/>
<Typography className="no-logs-text">
Query cancelled.
<span className="sub-text">
{' '}
Click &quot;Run Query&quot; to load metrics.
</span>
</Typography>
</div>
</div>
) : showNoMetrics ? (
<NoLogs dataSource={DataSource.METRICS} />
) : (
<>

View File

@@ -33,6 +33,8 @@ export interface MetricsSearchProps {
currentQueryFilterExpression: string;
setCurrentQueryFilterExpression: (expression: string) => void;
isLoading: boolean;
handleCancelQuery: () => void;
onRunQuery: () => void;
}
export interface MetricsTreemapProps {

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import { QueryKey } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -25,8 +24,8 @@ import PromQLQueryContainer from './QueryBuilder/promQL';
import './QuerySection.styles.scss';
function QuerySection({
selectedGraph,
queryRangeKey,
isLoadingQueries,
handleCancelQuery,
selectedWidget,
dashboardVersion,
dashboardId,
@@ -179,7 +178,7 @@ function QuerySection({
label="Stage & Run Query"
onStageRunQuery={handleRunQuery}
isLoadingQueries={isLoadingQueries}
queryRangeKey={queryRangeKey}
handleCancelQuery={handleCancelQuery}
/>
</span>
}
@@ -191,8 +190,8 @@ function QuerySection({
interface QueryProps {
selectedGraph: PANEL_TYPES;
queryRangeKey?: QueryKey;
isLoadingQueries?: boolean;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
selectedWidget: Widgets;
dashboardVersion?: string;
dashboardId?: string;

View File

@@ -1,5 +1,5 @@
import { memo, useEffect } from 'react';
import { useMemo } from 'react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V5 } from 'constants/app';
@@ -34,6 +34,7 @@ function LeftContainer({
isNewPanel = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const queryClient = useQueryClient();
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
AppState,
@@ -49,6 +50,10 @@ function LeftContainer({
],
[globalSelectedInterval, requestData, minTime, maxTime],
);
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries(queryRangeKey);
}, [queryClient, queryRangeKey]);
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
enabled: !!stagedQuery,
queryKey: queryRangeKey,
@@ -75,8 +80,8 @@ function LeftContainer({
<QueryContainer className="query-section-left-container">
<QuerySection
selectedGraph={selectedGraph}
queryRangeKey={queryRangeKey}
isLoadingQueries={queryResponse.isFetching}
handleCancelQuery={handleCancelQuery}
selectedWidget={selectedWidget}
dashboardVersion={ENTITY_VERSION_V5}
dashboardId={selectedDashboard?.id}

View File

@@ -677,18 +677,6 @@ function NewWidget({
queryType: currentQuery.queryType,
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
...(currentQuery.queryType === EQueryType.CLICKHOUSE && {
clickhouseQueryCount: currentQuery.clickhouse_sql.length,
clickhouseQueries: currentQuery.clickhouse_sql.map((q) => ({
name: q.name,
query: (q.query ?? '')
.replace(/--[^\n]*/g, '') // strip line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
.replace(/'(?:[^'\\]|\\.|'')*'/g, "'?'") // replace single-quoted strings (handles \' and '' escapes)
.replace(/\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '?'), // replace numeric literals (int, float, scientific)
disabled: q.disabled,
})),
}),
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -1,5 +1,3 @@
import { useCallback } from 'react';
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import cx from 'classnames';
import {
@@ -12,14 +10,23 @@ import {
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import './RunQueryBtn.scss';
interface RunQueryBtnProps {
type RunQueryBtnProps = {
className?: string;
label?: string;
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
queryRangeKey?: QueryKey;
}
disabled?: boolean;
} & (
| {
onStageRunQuery: () => void;
handleCancelQuery: () => void;
isLoadingQueries: boolean;
}
| {
onStageRunQuery?: never;
handleCancelQuery?: never;
isLoadingQueries?: never;
}
);
function RunQueryBtn({
className,
@@ -27,33 +34,17 @@ function RunQueryBtn({
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
queryRangeKey,
disabled,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
const queryClient = useQueryClient();
const isKeyFetchingCount = useIsFetching(
queryRangeKey as QueryKey | undefined,
);
const isLoading =
typeof isLoadingQueries === 'boolean'
? isLoadingQueries
: isKeyFetchingCount > 0;
const onCancel = useCallback(() => {
if (handleCancelQuery) {
return handleCancelQuery();
}
if (queryRangeKey) {
queryClient.cancelQueries(queryRangeKey);
}
}, [handleCancelQuery, queryClient, queryRangeKey]);
const isLoading = isLoadingQueries ?? false;
return isLoading ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className={cx('cancel-query-btn periscope-btn danger', className)}
onClick={onCancel}
onClick={handleCancelQuery}
>
Cancel
</Button>
@@ -61,7 +52,7 @@ function RunQueryBtn({
<Button
type="primary"
className={cx('run-query-btn periscope-btn primary', className)}
disabled={isLoading || !onStageRunQuery}
disabled={disabled}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>

View File

@@ -1,18 +1,9 @@
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import RunQueryBtn from '../RunQueryBtn';
jest.mock('react-query', () => {
const actual = jest.requireActual('react-query');
return {
...actual,
useIsFetching: jest.fn(),
useQueryClient: jest.fn(),
};
});
import { useIsFetching, useQueryClient } from 'react-query';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
@@ -26,79 +17,76 @@ describe('RunQueryBtn', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
(useIsFetching as jest.Mock).mockReturnValue(0);
(useQueryClient as jest.Mock).mockReturnValue({
cancelQueries: jest.fn(),
});
});
test('uses isLoadingQueries prop over useIsFetching', () => {
// Simulate fetching but prop forces not loading
(useIsFetching as jest.Mock).mockReturnValue(1);
test('shows run button and hides cancel when isLoadingQueries is false', () => {
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
expect(screen.getByRole('button', { name: /run query/i })).toBeEnabled();
expect(
screen.queryByRole('button', { name: /cancel/i }),
).not.toBeInTheDocument();
});
test('shows cancel button and hides run button when isLoadingQueries is true', () => {
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries
/>,
);
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /run query/i }),
).not.toBeInTheDocument();
});
test('calls onStageRunQuery when run button is clicked', async () => {
const user = userEvent.setup();
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} isLoadingQueries={false} />);
// Should show "Run Query" (not cancel)
const runBtn = screen.getByRole('button', { name: /run query/i });
expect(runBtn).toBeInTheDocument();
expect(runBtn).toBeEnabled();
});
test('fallback cancel: uses handleCancelQuery when no key provided', () => {
(useIsFetching as jest.Mock).mockReturnValue(0);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelBtn);
expect(onCancel).toHaveBeenCalledTimes(1);
expect(cancelQueries).not.toHaveBeenCalled();
});
test('renders run state and triggers on click', () => {
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
expect(btn).toBeEnabled();
fireEvent.click(btn);
render(
<RunQueryBtn
onStageRunQuery={onRun}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
await user.click(screen.getByRole('button', { name: /run query/i }));
expect(onRun).toHaveBeenCalledTimes(1);
});
test('disabled when onStageRunQuery is undefined', () => {
test('calls handleCancelQuery when cancel button is clicked', async () => {
const user = userEvent.setup();
const onCancel = jest.fn();
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
isLoadingQueries
handleCancelQuery={onCancel}
/>,
);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('is disabled when no props provided', () => {
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
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('derives loading from queryKey via useIsFetching and cancels via queryClient', () => {
(useIsFetching as jest.Mock).mockReturnValue(1);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const queryKey = ['GET_QUERY_RANGE', '1h', { some: 'req' }, 1, 2];
render(<RunQueryBtn queryRangeKey={queryKey} />);
// Button switches to cancel state
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
expect(cancelBtn).toBeInTheDocument();
// Clicking cancel calls cancelQueries with the key
fireEvent.click(cancelBtn);
expect(cancelQueries).toHaveBeenCalledWith(queryKey);
});
test('shows Command + CornerDownLeft on mac', () => {
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
expect(
@@ -111,7 +99,11 @@ describe('RunQueryBtn', () => {
UserOperatingSystem.WINDOWS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
@@ -120,11 +112,20 @@ describe('RunQueryBtn', () => {
).toBeInTheDocument();
});
test('renders custom label when provided', () => {
test('renders custom label and calls onStageRunQuery on click', async () => {
const user = userEvent.setup();
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(
screen.getByRole('button', { name: /stage & run query/i }),
).toBeInTheDocument();
render(
<RunQueryBtn
onStageRunQuery={onRun}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
label="Stage & Run Query"
/>,
);
const btn = screen.getByRole('button', { name: /stage & run query/i });
expect(btn).toBeInTheDocument();
await user.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
});
});

View File

@@ -42,7 +42,7 @@ export default function RightToolbarActions({
if (showLiveLogs) {
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn />
<RunQueryBtn disabled />
</div>
);
}
@@ -59,7 +59,7 @@ export default function RightToolbarActions({
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn
isLoadingQueries={isLoadingQueries}
isLoadingQueries={!!isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onStageRunQuery={onStageRunQuery}
/>

View File

@@ -431,7 +431,7 @@ export const useAlertRuleDuplicate = ({
const params = useUrlQuery();
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALLERTS, {
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALERTS, {
queryFn: getAll,
cacheTime: 0,
});

View File

@@ -41,7 +41,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
RequestContentType: "application/json",
Response: new(citypes.GettableAccountWithConnectionArtifact),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
@@ -138,7 +138,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "List services metadata",
Description: "This endpoint lists the services metadata for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.ListServicesMetadataParams),
RequestContentType: "",
Response: new(citypes.GettableServicesMetadata),
ResponseContentType: "application/json",
@@ -159,7 +158,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "Get service",
Description: "This endpoint gets a service for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.GetServiceParams),
RequestContentType: "",
Response: new(citypes.Service),
ResponseContentType: "application/json",

View File

@@ -63,7 +63,6 @@ type RetryConfig struct {
func newConfig() factory.Config {
return Config{
Provider: "noop",
BufferSize: 1000,
BatchSize: 100,
FlushInterval: time.Second,

View File

@@ -14,7 +14,7 @@ import (
sdkmetric "go.opentelemetry.io/otel/metric"
sdkmetricnoop "go.opentelemetry.io/otel/metric/noop"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
sdktrace "go.opentelemetry.io/otel/trace"
)

View File

@@ -208,7 +208,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewComment().Wrap)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz)

View File

@@ -207,16 +207,23 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
indexes := []telemetrytypes.JSONDataTypeIndex{}
fieldContextsSeen := map[telemetrytypes.FieldContext]bool{}
dataTypesSeen := map[telemetrytypes.FieldDataType]bool{}
jsonTypesSeen := map[string]*telemetrytypes.JSONDataType{}
for _, matchingKey := range matchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
for _, matchingKey := range contextPrefixedMatchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
key.Materialized = materialized
@@ -241,6 +248,15 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
break
}
}
if len(jsonTypesSeen) == 1 && key.JSONDataType == nil {
// all matching keys have same JSON data type, use it
for _, jt := range jsonTypesSeen {
actions = append(actions, fmt.Sprintf("Adjusting key %s to have JSON data type %s", key, jt.StringValue()))
key.JSONDataType = jt
break
}
}
}
return actions

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/emailing"
@@ -124,9 +123,6 @@ type Config struct {
// ServiceAccount config
ServiceAccount serviceaccount.Config `mapstructure:"serviceaccount"`
// Auditor config
Auditor auditor.Config `mapstructure:"auditor"`
}
func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.ResolverConfig) (Config, error) {
@@ -157,7 +153,6 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
user.NewConfigFactory(),
identn.NewConfigFactory(),
serviceaccount.NewConfigFactory(),
auditor.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -3,8 +3,6 @@ package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/noopauditor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
@@ -314,12 +312,6 @@ func NewGlobalProviderFactories(identNConfig identn.Config) factory.NamedMap[fac
)
}
func NewAuditorProviderFactories() factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return factory.MustNewNamedMap(
noopauditor.NewFactory(),
)
}
func NewFlaggerProviderFactories(registry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config]] {
return factory.MustNewNamedMap(
configflagger.NewFactory(registry),

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/apiserver"
@@ -76,7 +75,6 @@ type SigNoz struct {
QueryParser queryparser.QueryParser
Flagger flagger.Flagger
Gateway gateway.Gateway
Auditor auditor.Auditor
}
func New(
@@ -96,7 +94,6 @@ func New(
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
) (*SigNoz, error) {
// Initialize instrumentation
@@ -374,12 +371,6 @@ func New(
return nil, err
}
// Initialize auditor from the variant-specific provider factories
auditor, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Auditor, auditorProviderFactories(licensing), config.Auditor.Provider)
if err != nil {
return nil, err
}
// Initialize authns
store := sqlauthnstore.NewStore(sqlstore)
authNs, err := authNsCallback(ctx, providerSettings, store, licensing)
@@ -479,7 +470,6 @@ func New(
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
factory.NewNamedService(factory.MustNewName("authz"), authz),
factory.NewNamedService(factory.MustNewName("user"), userService, factory.MustNewName("authz")),
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
)
if err != nil {
return nil, err
@@ -526,6 +516,5 @@ func New(
QueryParser: queryParser,
Flagger: flagger,
Gateway: gateway,
Auditor: auditor,
}, nil
}

View File

@@ -276,12 +276,11 @@ func (m *fieldMapper) FieldFor(ctx context.Context, tsStart, tsEnd uint64, key *
continue
}
if key.FieldDataType == telemetrytypes.FieldDataTypeUnspecified {
if key.JSONDataType == nil {
return "", qbtypes.ErrColumnNotFound
}
jdt := key.GetJSONDataType()
if key.KeyNameContainsArray() && !jdt.IsArray {
if key.KeyNameContainsArray() && !key.JSONDataType.IsArray {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "FieldFor not supported for nested fields; only supported for flat paths (e.g. body.status.detail) and paths of Array type: %s(%s)", key.Name, key.FieldDataType)
}

View File

@@ -220,7 +220,7 @@ func TestJSONStmtBuilder_PrimitivePaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')) OR ((LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%25%", "%25%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64 name=user.age,context=body,datatype=string]."},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64,jsondatatype=Int64 name=user.age,context=body,datatype=string,jsondatatype=String]."},
},
},
{
@@ -414,7 +414,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -441,7 +441,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -455,7 +455,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -549,7 +549,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> toFloat64OrNull(dynamicElement(`body_v2.education`.`type`, 'String')) = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> dynamicElement(`body_v2.education`.`type`, 'Int64') = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{uint64(1747945619), uint64(1747983448), int64(10001), int64(10001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string name=education[].type,context=body,datatype=int64]."},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string,jsondatatype=String name=education[].type,context=body,datatype=int64,jsondatatype=Int64]."},
},
},
{
@@ -576,7 +576,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -585,7 +585,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "passed", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -594,7 +594,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -603,7 +603,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) IN (?, ?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, "1.65", "1.99", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -612,7 +612,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", float64(1.65), "%1.65%", float64(1.65), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -622,7 +622,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
WhereClause: "(has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?) OR has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?))",
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic].",
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)].",
},
},
},
@@ -702,7 +702,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64 name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string]."},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
},
},
{
@@ -946,16 +946,16 @@ func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetryty
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.SetStaticFields(IntrinsicFields)
types, _ := telemetrytypes.TestJSONTypeSet()
for path, fieldDataTypes := range types {
for _, fdt := range fieldDataTypes {
for path, jsonTypes := range types {
for _, jsonType := range jsonTypes {
key := &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: fdt,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[jsonType],
JSONDataType: &jsonType,
}
if addIndexes {
jsonType := telemetrytypes.MappingFieldDataTypeToJSONDataType[fdt]
idx := slices.IndexFunc(telemetrytypes.TestIndexedPaths, func(entry telemetrytypes.TestIndexedPathEntry) bool {
return entry.Path == path && entry.Type == jsonType
})

View File

@@ -875,6 +875,7 @@ func TestAdjustKey(t *testing.T) {
require.Equal(t, c.expectedKey.FieldContext, key.FieldContext, "field context should match")
require.Equal(t, c.expectedKey.FieldDataType, key.FieldDataType, "field data type should match")
require.Equal(t, c.expectedKey.Materialized, key.Materialized, "materialized should match")
require.Equal(t, c.expectedKey.JSONDataType, key.JSONDataType, "json data type should match")
require.Equal(t, c.expectedKey.Indexes, key.Indexes, "json exists should match")
})
}

View File

@@ -21,72 +21,133 @@ import (
)
var (
defaultPathLimit = 100 // Default limit to prevent full table scans
CodeUnknownJSONDataType = errors.MustNewCode("unknown_json_data_type")
CodeFailLoadPromotedPaths = errors.MustNewCode("fail_load_promoted_paths")
CodeFailCheckPathPromoted = errors.MustNewCode("fail_check_path_promoted")
CodeFailIterateBodyJSONKeys = errors.MustNewCode("fail_iterate_body_json_keys")
CodeFailExtractBodyJSONKeys = errors.MustNewCode("fail_extract_body_json_keys")
CodeFailLoadLogsJSONIndexes = errors.MustNewCode("fail_load_logs_json_indexes")
CodeFailListJSONValues = errors.MustNewCode("fail_list_json_values")
CodeFailScanJSONValue = errors.MustNewCode("fail_scan_json_value")
CodeFailScanVariant = errors.MustNewCode("fail_scan_variant")
CodeFailBuildJSONPathsQuery = errors.MustNewCode("fail_build_json_paths_query")
CodeNoPathsToQueryIndexes = errors.MustNewCode("no_paths_to_query_indexes_provided")
CodeFailedToPrepareBatch = errors.MustNewCode("failed_to_prepare_batch_promoted_paths")
CodeFailedToSendBatch = errors.MustNewCode("failed_to_send_batch_promoted_paths")
CodeFailedToAppendPath = errors.MustNewCode("failed_to_append_path_promoted_paths")
)
// enrichBodyKeys enriches body-context keys with promoted path info, indexes,
// and JSON access plans.
// parentTypeCache contains parent array types (ArrayJSON/ArrayDynamic) pre-fetched in the main UNION query.
func (t *telemetryMetaStore) enrichBodyKeys(ctx context.Context, keys []*telemetrytypes.TelemetryFieldKey, parentTypeCache map[string][]telemetrytypes.FieldDataType) error {
if len(keys) == 0 {
return nil
}
// fetchBodyJSONPaths extracts body JSON paths from the path_types table
// This function can be used by both JSONQueryBuilder and metadata extraction
// uniquePathLimit: 0 for no limit, >0 for maximum number of unique paths to return
// - For startup load: set to 10000 to get top 10k unique paths
// - For lookup: set to 0 (no limit needed for single path)
// - For metadata API: set to desired pagination limit
//
// searchOperator: LIKE for pattern matching, EQUAL for exact match.
func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, []string, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "metadata",
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
})
var filteredKeys []*telemetrytypes.TelemetryFieldKey
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextBody {
filteredKeys = append(filteredKeys, key)
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to extract body JSON keys")
}
defer rows.Close()
fieldKeys := []*telemetrytypes.TelemetryFieldKey{}
paths := []string{}
rowCount := 0
for rows.Next() {
var path string
var typesArray []string // ClickHouse returns array as []string
var lastSeen uint64
err = rows.Scan(&path, &typesArray, &lastSeen)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to scan body JSON key row")
}
for _, typ := range typesArray {
mapping, found := telemetrytypes.MappingStringToJSONDataType[typ]
if !found {
t.logger.ErrorContext(ctx, "failed to map type string to JSON data type", slog.String("type", typ), slog.String("path", path))
continue
}
fieldKeys = append(fieldKeys, &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[mapping],
JSONDataType: &mapping,
})
}
paths = append(paths, path)
rowCount++
}
if rows.Err() != nil {
return nil, nil, false, errors.WrapInternalf(rows.Err(), CodeFailIterateBodyJSONKeys, "error iterating body JSON keys")
}
// collect paths for batch queries
paths := make([]string, 0, len(filteredKeys))
for _, key := range filteredKeys {
paths = append(paths, key.Name)
}
// fetch promoted paths
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return err
}
// fetch JSON path indexes
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return err
}
// apply promoted/index metadata to keys
for _, key := range filteredKeys {
promotedKey := strings.Split(key.Name, telemetrytypes.ArraySep)[0]
key.Materialized = promoted[promotedKey]
key.Indexes = indexes[key.Name]
}
// build JSON access plans using the pre-fetched parent type cache
return t.buildJSONPlans(ctx, filteredKeys, parentTypeCache)
return fieldKeys, paths, rowCount <= limit, nil
}
// buildJSONPlans builds JSON access plans for the given keys
// using the provided parent type cache (pre-fetched in the main UNION query).
func (t *telemetryMetaStore) buildJSONPlans(_ context.Context, keys []*telemetrytypes.TelemetryFieldKey, typeCache map[string][]telemetrytypes.FieldDataType) error {
if len(keys) == 0 {
return nil
func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
fieldKeys, paths, finished, err := t.fetchBodyJSONPaths(ctx, fieldKeySelectors)
if err != nil {
return nil, false, err
}
columnMeta := t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody]
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return nil, false, err
}
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return nil, false, err
}
for _, fieldKey := range fieldKeys {
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
fieldKey.Materialized = promoted[promotedKey]
fieldKey.Indexes = indexes[fieldKey.Name]
}
return fieldKeys, finished, t.buildJSONPlans(ctx, fieldKeys)
}
func (t *telemetryMetaStore) buildJSONPlans(ctx context.Context, keys []*telemetrytypes.TelemetryFieldKey) error {
parentSelectors := make([]*telemetrytypes.FieldKeySelector, 0, len(keys))
for _, key := range keys {
if err := key.SetJSONAccessPlan(columnMeta, typeCache); err != nil {
parentSelectors = append(parentSelectors, key.ArrayParentSelectors()...)
}
parentKeys, _, _, err := t.fetchBodyJSONPaths(ctx, parentSelectors)
if err != nil {
return err
}
typeCache := make(map[string][]telemetrytypes.JSONDataType)
for _, key := range parentKeys {
typeCache[key.Name] = append(typeCache[key.Name], *key.JSONDataType)
}
// build plans for keys now
for _, key := range keys {
err = key.SetJSONAccessPlan(t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody], typeCache)
if err != nil {
return err
}
}
@@ -94,6 +155,51 @@ func (t *telemetryMetaStore) buildJSONPlans(_ context.Context, keys []*telemetry
return nil
}
func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySelector) (string, []any, int) {
if len(fieldKeySelectors) == 0 {
return "", nil, defaultPathLimit
}
from := fmt.Sprintf("%s.%s", DBName, PathTypesTableName)
// Build a better query using GROUP BY to deduplicate at database level
// This aggregates all types per path and gets the max last_seen, then applies LIMIT
sb := sqlbuilder.Select(
"path",
"groupArray(DISTINCT type) AS types",
"max(last_seen) AS last_seen",
).From(from)
limit := 0
// Add search filter if provided
orClauses := []string{}
for _, fieldKeySelector := range fieldKeySelectors {
// replace [*] with []
fieldKeySelector.Name = strings.ReplaceAll(fieldKeySelector.Name, telemetrytypes.ArrayAnyIndex, telemetrytypes.ArraySep)
// Extract search text for body JSON keys
keyName := CleanPathPrefixes(fieldKeySelector.Name)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
orClauses = append(orClauses, sb.Equal("path", keyName))
} else {
// Pattern matching for metadata API (defaults to LIKE behavior for other operators)
orClauses = append(orClauses, sb.ILike("path", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(keyName))))
}
limit += fieldKeySelector.Limit
}
sb.Where(sb.Or(orClauses...))
// Group by path to get unique paths with aggregated types
sb.GroupBy("path")
// Order by max last_seen to get most recent paths first
sb.OrderBy("last_seen DESC")
if limit == 0 {
limit = defaultPathLimit
}
sb.Limit(limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return query, args, limit
}
func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...string) (map[string][]telemetrytypes.JSONDataTypeIndex, error) {
filteredPaths := []string{}
for _, path := range paths {

View File

@@ -7,9 +7,99 @@ import (
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/require"
)
func TestBuildGetBodyJSONPathsQuery(t *testing.T) {
testCases := []struct {
name string
fieldKeySelectors []*telemetrytypes.FieldKeySelector
expectedSQL string
expectedArgs []any
expectedLimit int
}{
{
name: "Single search text with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Single search text with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", 100},
expectedLimit: 100,
},
{
name: "Multiple search texts with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
{
Name: "user.age",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (path = ? OR path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", "user.age", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Multiple search texts with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
{
Name: "admin",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?) OR LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", "%admin%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Search with Contains operator (should default to LIKE)",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "test",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%test%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, args, limit := buildGetBodyJSONPathsQuery(tc.fieldKeySelectors)
require.Equal(t, tc.expectedSQL, query)
require.Equal(t, tc.expectedArgs, args)
require.Equal(t, tc.expectedLimit, limit)
})
}
}
func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
testCases := []struct {
name string

View File

@@ -368,19 +368,6 @@ func (t *telemetryMetaStore) logsTblStatementToFieldKeys(ctx context.Context) ([
return materialisedKeys, nil
}
// logKeysUnionArm declares one arm of the UNION ALL in getLogsKeys.
// All per-table variance is captured here so the loop body can stay uniform.
type logKeysUnionArm struct {
shouldQuery bool
fieldContext telemetrytypes.FieldContext
table string
dataTypeColumn string // column used in WHERE/GROUP BY
dataTypeSelectExpr string // expression used in SELECT (may wrap with lower())
addBaseFilters func(sb *sqlbuilder.SelectBuilder) // mandatory WHERE filters (e.g., signal, field_context)
encodeDataType func(telemetrytypes.FieldDataType) string // how to render a FieldDataType in WHERE values
extraOrBranch func(sb *sqlbuilder.SelectBuilder) string // optional extra OR branch (e.g., body parent-types)
}
// getLogsKeys returns the keys from the spans that match the field selection criteria.
func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
@@ -410,149 +397,90 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
// tables to query based on field selectors
queryAttributeTable := false
queryResourceTable := false
queryBodyTable := false
for _, selector := range fieldKeySelectors {
if selector.FieldContext == telemetrytypes.FieldContextUnspecified {
// unspecified context, query all tables
// unspecified context, query both tables
queryAttributeTable = true
queryResourceTable = true
queryBodyTable = true
break
} else if selector.FieldContext == telemetrytypes.FieldContextAttribute {
queryAttributeTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextResource {
queryResourceTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled {
queryBodyTable = true
}
}
// body keys are gated behind the feature flag
queryBodyTable = queryBodyTable && querybuilder.BodyJSONQueryEnabled
// pre-compute parent array path names from body selectors for JSON plan building;
// these will be fetched as a separate UNION arm filtered to ArrayJSON/ArrayDynamic only
parentPaths := make(map[string]bool)
if queryBodyTable {
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextBody &&
sel.FieldContext != telemetrytypes.FieldContextUnspecified {
continue
}
if !strings.Contains(sel.Name, telemetrytypes.ArraySep) {
continue
}
key := &telemetrytypes.TelemetryFieldKey{
Name: sel.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
}
for _, ps := range key.ArrayParentSelectors() {
parentPaths[ps.Name] = true
}
}
tablesToQuery := []struct {
fieldContext telemetrytypes.FieldContext
shouldQuery bool
}{
{telemetrytypes.FieldContextAttribute, queryAttributeTable},
{telemetrytypes.FieldContextResource, queryResourceTable},
}
// Each UNION arm differs only in: table, data-type column name and SELECT
// expression (lower-wrapped for historical mixed-case in attr/resource),
// base WHERE filters, the per-selector data-type encoding, and (for body)
// an extra OR branch that fetches parent array types for JSON plan building.
// All other logic is shared by the loop below.
tablesToQuery := []logKeysUnionArm{
{
shouldQuery: queryAttributeTable,
fieldContext: telemetrytypes.FieldContextAttribute,
table: t.logsDBName + "." + t.logAttributeKeysTblName,
dataTypeColumn: "datatype",
dataTypeSelectExpr: "lower(datatype)",
addBaseFilters: func(*sqlbuilder.SelectBuilder) {},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.TagDataType() },
extraOrBranch: func(*sqlbuilder.SelectBuilder) string { return "" },
},
{
shouldQuery: queryResourceTable,
fieldContext: telemetrytypes.FieldContextResource,
table: t.logsDBName + "." + t.logResourceKeysTblName,
dataTypeColumn: "datatype",
dataTypeSelectExpr: "lower(datatype)",
addBaseFilters: func(*sqlbuilder.SelectBuilder) {},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.TagDataType() },
extraOrBranch: func(*sqlbuilder.SelectBuilder) string { return "" },
},
{
shouldQuery: queryBodyTable,
fieldContext: telemetrytypes.FieldContextBody,
table: fmt.Sprintf("%s.%s", DBName, FieldKeysTable),
dataTypeColumn: "field_data_type",
dataTypeSelectExpr: "field_data_type",
addBaseFilters: func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.E("signal", telemetrytypes.SignalLogs.StringValue()))
sb.Where(sb.E("field_context", telemetrytypes.FieldContextBody.StringValue()))
},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.StringValue() },
extraOrBranch: func(sb *sqlbuilder.SelectBuilder) string {
if len(parentPaths) == 0 {
return ""
}
names := make([]any, 0, len(parentPaths))
for n := range parentPaths {
names = append(names, n)
}
return sb.And(
sb.In("name", names...),
sb.In("field_data_type",
telemetrytypes.FieldDataTypeArrayDynamic.StringValue(),
telemetrytypes.FieldDataTypeArrayJSON.StringValue(),
),
)
},
},
}
for _, arm := range tablesToQuery {
if !arm.shouldQuery {
for _, table := range tablesToQuery {
if !table.shouldQuery {
continue
}
fieldContext := table.fieldContext
// table name based on field context
var tblName string
if fieldContext == telemetrytypes.FieldContextAttribute {
tblName = t.logsDBName + "." + t.logAttributeKeysTblName
} else {
tblName = t.logsDBName + "." + t.logResourceKeysTblName
}
sb := sqlbuilder.Select(
"name AS tag_key",
fmt.Sprintf("'%s' AS tag_type", arm.fieldContext.TagType()),
arm.dataTypeSelectExpr+" AS tag_data_type",
fmt.Sprintf("%d AS priority", getPriorityForContext(arm.fieldContext)),
).From(arm.table)
fmt.Sprintf("'%s' AS tag_type", fieldContext.TagType()),
"lower(datatype) AS tag_data_type", // in logs, we had some historical data with capital and small case
fmt.Sprintf(`%d AS priority`, getPriorityForContext(fieldContext)),
).From(tblName)
arm.addBaseFilters(sb)
var limit int
conds := []string{}
branches := []string{}
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextUnspecified && sel.FieldContext != arm.fieldContext {
for _, fieldKeySelector := range fieldKeySelectors {
// Include this selector if:
// 1. It has unspecified context (matches all tables)
// 2. Its context matches the current table's context
if fieldKeySelector.FieldContext != telemetrytypes.FieldContextUnspecified &&
fieldKeySelector.FieldContext != fieldContext {
continue
}
parts := []string{}
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
parts = append(parts, sb.E("name", sel.Name))
// key part of the selector
fieldKeyConds := []string{}
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("name", fieldKeySelector.Name))
} else {
parts = append(parts, sb.ILike("name", "%"+escapeForLike(sel.Name)+"%"))
fieldKeyConds = append(fieldKeyConds, sb.ILike("name", "%"+escapeForLike(fieldKeySelector.Name)+"%"))
}
if sel.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
parts = append(parts, sb.E(arm.dataTypeColumn, arm.encodeDataType(sel.FieldDataType)))
// now look at the field data type
if fieldKeySelector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("datatype", fieldKeySelector.FieldDataType.TagDataType()))
}
if len(parts) > 0 {
branches = append(branches, sb.And(parts...))
if len(fieldKeyConds) > 0 {
conds = append(conds, sb.And(fieldKeyConds...))
}
limit += fieldKeySelector.Limit
}
if extra := arm.extraOrBranch(sb); extra != "" {
branches = append(branches, extra)
if len(conds) > 0 {
sb.Where(sb.Or(conds...))
}
if len(branches) > 0 {
sb.Where(sb.Or(branches...))
sb.GroupBy("name", "datatype")
if limit == 0 {
limit = 1000
}
sb.GroupBy("name", arm.dataTypeColumn)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
queries = append(queries, query)
allArgs = append(allArgs, args...)
@@ -589,7 +517,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
defer rows.Close()
keys := []*telemetrytypes.TelemetryFieldKey{}
parentTypeCache := make(map[string][]telemetrytypes.FieldDataType)
rowCount := 0
searchTexts := []string{}
@@ -613,17 +540,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
if err != nil {
return nil, false, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, ErrFailedToGetLogsKeys.Error())
}
// body keys with ArrayJSON/ArrayDynamic types are internal container types
// used only for JSON access plan building; route to parentTypeCache, not to results
switch fieldDataType {
case telemetrytypes.FieldDataTypeArrayJSON, telemetrytypes.FieldDataTypeArrayDynamic:
if fieldContext == telemetrytypes.FieldContextBody && parentPaths[name] {
parentTypeCache[name] = append(parentTypeCache[name], fieldDataType)
continue
}
}
key, ok := mapOfKeys[name+";"+fieldContext.StringValue()+";"+fieldDataType.StringValue()]
// if there is no materialised column, create a key with the field context and data type
@@ -677,11 +593,13 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
}
}
// enrich body keys with promoted paths, indexes, and JSON access plans
if querybuilder.BodyJSONQueryEnabled {
if err := t.enrichBodyKeys(ctx, keys, parentTypeCache); err != nil {
t.logger.ErrorContext(ctx, "failed to enrich body JSON keys", errors.Attr(err))
bodyJSONPaths, finished, err := t.buildBodyJSONPaths(ctx, fieldKeySelectors) // LIKE for pattern matching
if err != nil {
t.logger.ErrorContext(ctx, "failed to extract body JSON paths", errors.Attr(err))
}
keys = append(keys, bodyJSONPaths...)
complete = complete && finished
}
if _, err := t.updateColumnEvolutionMetadataForKeys(ctx, keys); err != nil {

View File

@@ -1,11 +1,13 @@
package telemetrymetadata
import otelcollectorconst "github.com/SigNoz/signoz-otel-collector/constants"
const (
DBName = "signoz_metadata"
AttributesMetadataTableName = "distributed_attributes_metadata"
AttributesMetadataLocalTableName = "attributes_metadata"
ColumnEvolutionMetadataTableName = "distributed_column_evolution_metadata"
FieldKeysTable = "distributed_field_keys"
PathTypesTableName = otelcollectorconst.DistributedPathTypesTable
// Column Evolution table stores promoted paths as (signal, column_name, field_context, field_name); see signoz-otel-collector metadata_migrations.
PromotedPathsTableName = "distributed_column_evolution_metadata"
SkipIndexTableName = "system.data_skipping_indices"

View File

@@ -62,10 +62,6 @@ type GettableServicesMetadata struct {
Services []*ServiceMetadata `json:"services" required:"true" nullable:"false"`
}
type ListServicesMetadataParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
// Service represents a cloud integration service with its definition,
// cloud integration service is non nil only when the service entry exists in DB with ANY config (enabled or disabled).
type Service struct {
@@ -73,10 +69,6 @@ type Service struct {
CloudIntegrationService *CloudIntegrationService `json:"cloudIntegrationService" required:"true" nullable:"true"`
}
type GetServiceParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
type UpdatableService struct {
Config *ServiceConfig `json:"config" required:"true" nullable:"false"`
}

View File

@@ -37,6 +37,7 @@ type TelemetryFieldKey struct {
FieldContext FieldContext `json:"fieldContext,omitzero"`
FieldDataType FieldDataType `json:"fieldDataType,omitzero"`
JSONDataType *JSONDataType `json:"-"`
JSONPlan JSONAccessPlan `json:"-"`
Indexes []JSONDataTypeIndex `json:"-"`
Materialized bool `json:"-"` // refers to promoted in case of body.... fields
@@ -79,12 +80,6 @@ func (f *TelemetryFieldKey) ArrayParentSelectors() []*FieldKeySelector {
return selectors
}
// GetJSONDataType derives the JSONDataType from FieldDataType.
// Callers should check FieldDataType != FieldDataTypeUnspecified before calling.
func (f *TelemetryFieldKey) GetJSONDataType() JSONDataType {
return MappingFieldDataTypeToJSONDataType[f.FieldDataType]
}
func (f TelemetryFieldKey) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "name=%s", f.Name)
@@ -97,6 +92,9 @@ func (f TelemetryFieldKey) String() string {
if f.Materialized {
sb.WriteString(",materialized=true")
}
if f.JSONDataType != nil {
fmt.Fprintf(&sb, ",jsondatatype=%s", f.JSONDataType.StringValue())
}
if len(f.Indexes) > 0 {
sb.WriteString(",indexes=[")
for i, index := range f.Indexes {
@@ -119,6 +117,7 @@ func (f TelemetryFieldKey) Text() string {
func (f *TelemetryFieldKey) OverrideMetadataFrom(src *TelemetryFieldKey) {
f.FieldContext = src.FieldContext
f.FieldDataType = src.FieldDataType
f.JSONDataType = src.JSONDataType
f.Indexes = src.Indexes
f.Materialized = src.Materialized
f.JSONPlan = src.JSONPlan

View File

@@ -31,7 +31,7 @@ var (
FieldDataTypeArrayInt64 = FieldDataType{valuer.NewString("[]int64")}
FieldDataTypeArrayNumber = FieldDataType{valuer.NewString("[]number")}
FieldDataTypeArrayJSON = FieldDataType{valuer.NewString("[]object")}
FieldDataTypeArrayObject = FieldDataType{valuer.NewString("[]object")}
FieldDataTypeArrayDynamic = FieldDataType{valuer.NewString("[]dynamic")}
// Map string representations to FieldDataType values
@@ -72,8 +72,6 @@ var (
"[]float64": FieldDataTypeArrayFloat64,
"[]number": FieldDataTypeArrayNumber,
"[]bool": FieldDataTypeArrayBool,
"[]json": FieldDataTypeArrayJSON,
"[]dynamic": FieldDataTypeArrayDynamic,
// c-style array types
"string[]": FieldDataTypeArrayString,
@@ -81,8 +79,6 @@ var (
"float64[]": FieldDataTypeArrayFloat64,
"number[]": FieldDataTypeArrayNumber,
"bool[]": FieldDataTypeArrayBool,
"json[]": FieldDataTypeArrayJSON,
"dynamic[]": FieldDataTypeArrayDynamic,
}
fieldDataTypeToCHDataType = map[FieldDataType]string{

View File

@@ -43,7 +43,7 @@ type JSONAccessNode struct {
isRoot bool // marked true for only body_v2 and body_promoted
// Precomputed type information (single source of truth)
AvailableTypes []FieldDataType
AvailableTypes []JSONDataType
// Array type branches (Array(JSON) vs Array(Dynamic))
Branches map[JSONAccessBranchType]*JSONAccessNode
@@ -106,7 +106,7 @@ type planBuilder struct {
paths []string // cumulative paths for type cache lookups
segments []string // individual path segments for node names
isPromoted bool
typeCache map[string][]FieldDataType
typeCache map[string][]JSONDataType
}
// buildPlan recursively builds the path plan tree.
@@ -155,14 +155,14 @@ func (pb *planBuilder) buildPlan(index int, parent *JSONAccessNode, isDynArrChil
MaxDynamicPaths: maxPaths,
}
hasJSON := slices.Contains(node.AvailableTypes, FieldDataTypeArrayJSON)
hasDynamic := slices.Contains(node.AvailableTypes, FieldDataTypeArrayDynamic)
hasJSON := slices.Contains(node.AvailableTypes, ArrayJSON)
hasDynamic := slices.Contains(node.AvailableTypes, ArrayDynamic)
// Configure terminal if this is the last part
if isTerminal {
node.TerminalConfig = &TerminalConfig{
Key: pb.key,
ElemType: pb.key.GetJSONDataType(),
ElemType: *pb.key.JSONDataType,
}
} else {
var err error
@@ -185,7 +185,7 @@ func (pb *planBuilder) buildPlan(index int, parent *JSONAccessNode, isDynArrChil
// buildJSONAccessPlan builds a tree structure representing the complete JSON path traversal
// that precomputes all possible branches and their types.
func (key *TelemetryFieldKey) SetJSONAccessPlan(columnInfo JSONColumnMetadata, typeCache map[string][]FieldDataType,
func (key *TelemetryFieldKey) SetJSONAccessPlan(columnInfo JSONColumnMetadata, typeCache map[string][]JSONDataType,
) error {
// if path is empty, return nil
if key.Name == "" {

View File

@@ -19,11 +19,11 @@ const (
// ============================================================================
// makeKey creates a TelemetryFieldKey for testing.
func makeKey(name string, dataType FieldDataType, materialized bool) *TelemetryFieldKey {
func makeKey(name string, dataType JSONDataType, materialized bool) *TelemetryFieldKey {
return &TelemetryFieldKey{
Name: name,
FieldDataType: dataType,
Materialized: materialized,
Name: name,
JSONDataType: &dataType,
Materialized: materialized,
}
}
@@ -242,7 +242,7 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
}{
{
name: "Simple path not promoted",
key: makeKey("user.name", FieldDataTypeString, false),
key: makeKey("user.name", String, false),
expectedYAML: fmt.Sprintf(`
- name: user.name
column: %s
@@ -255,7 +255,7 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
},
{
name: "Simple path promoted",
key: makeKey("user.name", FieldDataTypeString, true),
key: makeKey("user.name", String, true),
expectedYAML: fmt.Sprintf(`
- name: user.name
column: %s
@@ -276,7 +276,7 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
},
{
name: "Empty path returns error",
key: makeKey("", FieldDataTypeString, false),
key: makeKey("", String, false),
expectErr: true,
expectedYAML: "",
},
@@ -431,7 +431,7 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := makeKey(tt.path, FieldDataTypeString, false)
key := makeKey(tt.path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -450,7 +450,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
path := "education[].awards[].type"
t.Run("Non-promoted plan", func(t *testing.T) {
key := makeKey(path, FieldDataTypeString, false)
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -493,7 +493,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
})
t.Run("Promoted plan", func(t *testing.T) {
key := makeKey(path, FieldDataTypeString, true)
key := makeKey(path, String, true)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -666,12 +666,12 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Choose key type based on path; operator does not affect the tree shape asserted here.
keyType := FieldDataTypeString
keyType := String
switch tt.path {
case "education":
keyType = FieldDataTypeArrayJSON
keyType = ArrayJSON
case "education[].type":
keyType = FieldDataTypeString
keyType = String
}
key := makeKey(tt.path, keyType, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
@@ -692,7 +692,7 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
func TestPlanJSON_TreeStructure(t *testing.T) {
types, _ := TestJSONTypeSet()
path := "education[].awards[].participated[].team[].branch"
key := makeKey(path, FieldDataTypeString, false)
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,

View File

@@ -46,6 +46,14 @@ var MappingStringToJSONDataType = map[string]JSONDataType{
"Array(JSON)": ArrayJSON,
}
var ScalerTypeToArrayType = map[JSONDataType]JSONDataType{
String: ArrayString,
Int64: ArrayInt64,
Float64: ArrayFloat64,
Bool: ArrayBool,
Dynamic: ArrayDynamic,
}
var MappingFieldDataTypeToJSONDataType = map[FieldDataType]JSONDataType{
FieldDataTypeString: String,
FieldDataTypeInt64: Int64,
@@ -55,8 +63,18 @@ var MappingFieldDataTypeToJSONDataType = map[FieldDataType]JSONDataType{
FieldDataTypeArrayString: ArrayString,
FieldDataTypeArrayInt64: ArrayInt64,
FieldDataTypeArrayFloat64: ArrayFloat64,
FieldDataTypeArrayNumber: ArrayFloat64,
FieldDataTypeArrayBool: ArrayBool,
FieldDataTypeArrayDynamic: ArrayDynamic,
FieldDataTypeArrayJSON: ArrayJSON,
}
var MappingJSONDataTypeToFieldDataType = map[JSONDataType]FieldDataType{
String: FieldDataTypeString,
Int64: FieldDataTypeInt64,
Float64: FieldDataTypeFloat64,
Bool: FieldDataTypeBool,
ArrayString: FieldDataTypeArrayString,
ArrayInt64: FieldDataTypeArrayInt64,
ArrayFloat64: FieldDataTypeArrayFloat64,
ArrayBool: FieldDataTypeArrayBool,
ArrayDynamic: FieldDataTypeArrayDynamic,
ArrayJSON: FieldDataTypeArrayObject,
}

View File

@@ -4,69 +4,69 @@ package telemetrytypes
// Test JSON Type Set Data Setup
// ============================================================================
// TestJSONTypeSet returns a map of path->field data types for testing.
// TestJSONTypeSet returns a map of path->types for testing.
// This represents the type information available in the test JSON structure.
func TestJSONTypeSet() (map[string][]FieldDataType, MetadataStore) {
types := map[string][]FieldDataType{
func TestJSONTypeSet() (map[string][]JSONDataType, MetadataStore) {
types := map[string][]JSONDataType{
// ── user (primitives) ─────────────────────────────────────────────
"user.name": {FieldDataTypeString},
"user.permissions": {FieldDataTypeArrayString},
"user.age": {FieldDataTypeInt64, FieldDataTypeString}, // Int64/String ambiguity
"user.height": {FieldDataTypeFloat64},
"user.active": {FieldDataTypeBool}, // Bool — not IndexSupported
"user.name": {String},
"user.permissions": {ArrayString},
"user.age": {Int64, String}, // Int64/String ambiguity
"user.height": {Float64},
"user.active": {Bool}, // Bool — not IndexSupported
// Deeper non-array nesting (a.b.c — no array hops)
"user.address.zip": {FieldDataTypeInt64},
"user.address.zip": {Int64},
// ── education[] ───────────────────────────────────────────────────
// Pattern: x[].y
"education": {FieldDataTypeArrayJSON},
"education[].name": {FieldDataTypeString},
"education[].type": {FieldDataTypeString, FieldDataTypeInt64},
"education[].year": {FieldDataTypeInt64},
"education[].scores": {FieldDataTypeArrayInt64},
"education[].parameters": {FieldDataTypeArrayFloat64, FieldDataTypeArrayDynamic},
"education": {ArrayJSON},
"education[].name": {String},
"education[].type": {String, Int64},
"education[].year": {Int64},
"education[].scores": {ArrayInt64},
"education[].parameters": {ArrayFloat64, ArrayDynamic},
// Pattern: x[].y[]
"education[].awards": {FieldDataTypeArrayDynamic, FieldDataTypeArrayJSON},
"education[].awards": {ArrayDynamic, ArrayJSON},
// Pattern: x[].y[].z
"education[].awards[].name": {FieldDataTypeString},
"education[].awards[].type": {FieldDataTypeString},
"education[].awards[].semester": {FieldDataTypeInt64},
"education[].awards[].name": {String},
"education[].awards[].type": {String},
"education[].awards[].semester": {Int64},
// Pattern: x[].y[].z[]
"education[].awards[].participated": {FieldDataTypeArrayDynamic, FieldDataTypeArrayJSON},
"education[].awards[].participated": {ArrayDynamic, ArrayJSON},
// Pattern: x[].y[].z[].w
"education[].awards[].participated[].members": {FieldDataTypeArrayString},
"education[].awards[].participated[].members": {ArrayString},
// Pattern: x[].y[].z[].w[]
"education[].awards[].participated[].team": {FieldDataTypeArrayJSON},
"education[].awards[].participated[].team": {ArrayJSON},
// Pattern: x[].y[].z[].w[].v
"education[].awards[].participated[].team[].branch": {FieldDataTypeString},
"education[].awards[].participated[].team[].branch": {String},
// ── interests[] ───────────────────────────────────────────────────
"interests": {FieldDataTypeArrayJSON},
"interests[].entities": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions[].name": {FieldDataTypeString},
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {FieldDataTypeArrayInt64, FieldDataTypeArrayString},
"http-events": {FieldDataTypeArrayJSON},
"http-events[].request-info.host": {FieldDataTypeString},
"ids": {FieldDataTypeArrayDynamic},
"interests": {ArrayJSON},
"interests[].entities": {ArrayJSON},
"interests[].entities[].reviews": {ArrayJSON},
"interests[].entities[].reviews[].entries": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions[].name": {String},
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {ArrayInt64, ArrayString},
"http-events": {ArrayJSON},
"http-events[].request-info.host": {String},
"ids": {ArrayDynamic},
// ── top-level primitives ──────────────────────────────────────────
"message": {FieldDataTypeString},
"http-status": {FieldDataTypeInt64, FieldDataTypeString}, // hyphen in root key, ambiguous
"message": {String},
"http-status": {Int64, String}, // hyphen in root key, ambiguous
// ── top-level nested objects (no array hops) ───────────────────────
"response.time-taken": {FieldDataTypeFloat64}, // hyphen inside nested key
"response.time-taken": {Float64}, // hyphen inside nested key
}
return types, nil