Compare commits

..

9 Commits

Author SHA1 Message Date
grandwizard28
48da875d5b fix: add Receiver request type to channel mutation endpoints
CreateChannel, UpdateChannelByID, TestChannel, and TestChannelDeprecated
all read req.Body as a Receiver config. The OpenAPIDef must declare
the request type so the generated SDK includes the body parameter.
2026-04-15 18:44:56 +05:30
grandwizard28
7877340cac fix: use typed response for GetAlerts endpoint 2026-04-15 18:38:22 +05:30
grandwizard28
2169fe5c73 fix: add required/nullable/enum tags to alertmanager OpenAPI types
- PostableRoutePolicy: mark expression, name, channels as required
- GettableRoutePolicy: change CreatedAt/UpdatedAt from pointer to value
- Channel: mark name, type, data, orgId as required
- ExpressionKind: add Enum() for rule/policy values
- Regenerate OpenAPI spec and frontend clients
2026-04-15 18:26:35 +05:30
grandwizard28
a444626468 chore: regenerate frontend API clients for alertmanager routes 2026-04-15 18:13:56 +05:30
grandwizard28
ee219e9f31 fix(review): use routepolicies tag without hyphen 2026-04-15 18:10:45 +05:30
grandwizard28
9de616f86c fix(review): remove section comments from alertmanager routes 2026-04-15 18:01:13 +05:30
grandwizard28
787d8a5623 fix(review): use camelCase for channel orgId json tag 2026-04-15 17:58:29 +05:30
grandwizard28
21607612ee fix(review): rename param, add /api/v1/channels/test endpoint
- Rename `am` to `alertmanagerService` in NewHandlers
- Add /api/v1/channels/test as the canonical test endpoint
- Mark /api/v1/testChannel as deprecated
- Regenerate OpenAPI spec
2026-04-15 17:50:44 +05:30
grandwizard28
caa440b205 refactor(alertmanager): move API handlers to signozapiserver
Extract Handler interface in pkg/alertmanager/handler.go and move
the implementation from api.go to signozalertmanager/handler.go.

Register all alertmanager routes (channels, route policies, alerts)
in signozapiserver via handler.New() with OpenAPIDef. Remove
AlertmanagerAPI injection from http_handler.go.

This enables future AuditDef instrumentation on these routes.
2026-04-15 17:44:08 +05:30
35 changed files with 5635 additions and 629 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -49,7 +48,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),

View File

@@ -0,0 +1,97 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetAlerts200, RenderErrorResponseDTO } from '../sigNoz.schemas';
/**
* This endpoint returns alerts for the organization
* @summary Get alerts
*/
export const getAlerts = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAlerts200>({
url: `/api/v1/alerts`,
method: 'GET',
signal,
});
};
export const getGetAlertsQueryKey = () => {
return [`/api/v1/alerts`] as const;
};
export const getGetAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAlertsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAlerts>>> = ({
signal,
}) => getAlerts(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAlerts>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAlertsQueryResult = NonNullable<
Awaited<ReturnType<typeof getAlerts>>
>;
export type GetAlertsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get alerts
*/
export function useGetAlerts<
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAlertsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get alerts
*/
export const invalidateGetAlerts = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAlertsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,646 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
GetChannelByIDPathParameters,
ListChannels200,
RenderErrorResponseDTO,
UpdateChannelByIDPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all notification channels for the organization
* @summary List notification channels
*/
export const listChannels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListChannels200>({
url: `/api/v1/channels`,
method: 'GET',
signal,
});
};
export const getListChannelsQueryKey = () => {
return [`/api/v1/channels`] as const;
};
export const getListChannelsQueryOptions = <
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListChannelsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listChannels>>> = ({
signal,
}) => listChannels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListChannelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listChannels>>
>;
export type ListChannelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List notification channels
*/
export function useListChannels<
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListChannelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List notification channels
*/
export const invalidateListChannels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListChannelsQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a notification channel
* @summary Create notification channel
*/
export const createChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateChannel201>({
url: `/api/v1/channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getCreateChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['createChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return createChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createChannel>>
>;
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create notification channel
*/
export const useCreateChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getCreateChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a notification channel by ID
* @summary Delete notification channel
*/
export const deleteChannelByID = ({ id }: DeleteChannelByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'DELETE',
});
};
export const getDeleteChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteChannelByID>>,
{ pathParams: DeleteChannelByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteChannelByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteChannelByID>>
>;
export type DeleteChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete notification channel
*/
export const useDeleteChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a notification channel by ID
* @summary Get notification channel by ID
*/
export const getChannelByID = (
{ id }: GetChannelByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetChannelByID200>({
url: `/api/v1/channels/${id}`,
method: 'GET',
signal,
});
};
export const getGetChannelByIDQueryKey = ({
id,
}: GetChannelByIDPathParameters) => {
return [`/api/v1/channels/${id}`] as const;
};
export const getGetChannelByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetChannelByIDQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getChannelByID>>> = ({
signal,
}) => getChannelByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChannelByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getChannelByID>>
>;
export type GetChannelByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get notification channel by ID
*/
export function useGetChannelByID<
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChannelByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get notification channel by ID
*/
export const invalidateGetChannelByID = async (
queryClient: QueryClient,
{ id }: GetChannelByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChannelByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a notification channel by ID
* @summary Update notification channel
*/
export const updateChannelByID = (
{ id }: UpdateChannelByIDPathParameters,
configReceiverDTO: BodyType<ConfigReceiverDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
});
};
export const getUpdateChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationKey = ['updateChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateChannelByID>>,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateChannelByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateChannelByID>>
>;
export type UpdateChannelByIDMutationBody = BodyType<ConfigReceiverDTO>;
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update notification channel
*/
export const useUpdateChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationOptions = getUpdateChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint tests a notification channel by sending a test notification
* @summary Test notification channel
*/
export const testChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannel>>
>;
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test notification channel
*/
export const useTestChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Deprecated: use /api/v1/channels/test instead
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const testChannelDeprecated = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/testChannel`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannelDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannelDeprecated>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannelDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannelDeprecated>>
>;
export type TestChannelDeprecatedMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelDeprecatedMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const useTestChannelDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelDeprecatedMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -0,0 +1,482 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AlertmanagertypesPostableRoutePolicyDTO,
CreateRoutePolicy201,
DeleteRoutePolicyByIDPathParameters,
GetAllRoutePolicies200,
GetRoutePolicyByID200,
GetRoutePolicyByIDPathParameters,
RenderErrorResponseDTO,
UpdateRoutePolicy200,
UpdateRoutePolicyPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all route policies for the organization
* @summary List route policies
*/
export const getAllRoutePolicies = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAllRoutePolicies200>({
url: `/api/v1/route_policies`,
method: 'GET',
signal,
});
};
export const getGetAllRoutePoliciesQueryKey = () => {
return [`/api/v1/route_policies`] as const;
};
export const getGetAllRoutePoliciesQueryOptions = <
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAllRoutePoliciesQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAllRoutePolicies>>
> = ({ signal }) => getAllRoutePolicies(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAllRoutePoliciesQueryResult = NonNullable<
Awaited<ReturnType<typeof getAllRoutePolicies>>
>;
export type GetAllRoutePoliciesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List route policies
*/
export function useGetAllRoutePolicies<
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAllRoutePoliciesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List route policies
*/
export const invalidateGetAllRoutePolicies = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAllRoutePoliciesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a route policy
* @summary Create route policy
*/
export const createRoutePolicy = (
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRoutePolicy201>({
url: `/api/v1/route_policies`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
signal,
});
};
export const getCreateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationKey = ['createRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRoutePolicy>>,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> }
> = (props) => {
const { data } = props ?? {};
return createRoutePolicy(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof createRoutePolicy>>
>;
export type CreateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type CreateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create route policy
*/
export const useCreateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationOptions = getCreateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a route policy by ID
* @summary Delete route policy
*/
export const deleteRoutePolicyByID = ({
id,
}: DeleteRoutePolicyByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/route_policies/${id}`,
method: 'DELETE',
});
};
export const getDeleteRoutePolicyByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteRoutePolicyByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
{ pathParams: DeleteRoutePolicyByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRoutePolicyByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRoutePolicyByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>
>;
export type DeleteRoutePolicyByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete route policy
*/
export const useDeleteRoutePolicyByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteRoutePolicyByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a route policy by ID
* @summary Get route policy by ID
*/
export const getRoutePolicyByID = (
{ id }: GetRoutePolicyByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRoutePolicyByID200>({
url: `/api/v1/route_policies/${id}`,
method: 'GET',
signal,
});
};
export const getGetRoutePolicyByIDQueryKey = ({
id,
}: GetRoutePolicyByIDPathParameters) => {
return [`/api/v1/route_policies/${id}`] as const;
};
export const getGetRoutePolicyByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRoutePolicyByIDQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRoutePolicyByID>>
> = ({ signal }) => getRoutePolicyByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRoutePolicyByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getRoutePolicyByID>>
>;
export type GetRoutePolicyByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get route policy by ID
*/
export function useGetRoutePolicyByID<
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRoutePolicyByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get route policy by ID
*/
export const invalidateGetRoutePolicyByID = async (
queryClient: QueryClient,
{ id }: GetRoutePolicyByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRoutePolicyByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a route policy by ID
* @summary Update route policy
*/
export const updateRoutePolicy = (
{ id }: UpdateRoutePolicyPathParameters,
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
) => {
return GeneratedAPIInstance<UpdateRoutePolicy200>({
url: `/api/v1/route_policies/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
});
};
export const getUpdateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationKey = ['updateRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRoutePolicy>>,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRoutePolicy(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRoutePolicy>>
>;
export type UpdateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type UpdateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update route policy
*/
export const useUpdateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
package alertmanager
import "net/http"
type Handler interface {
GetAlerts(http.ResponseWriter, *http.Request)
TestReceiver(http.ResponseWriter, *http.Request)
ListChannels(http.ResponseWriter, *http.Request)
ListAllChannels(http.ResponseWriter, *http.Request)
GetChannelByID(http.ResponseWriter, *http.Request)
CreateChannel(http.ResponseWriter, *http.Request)
UpdateChannelByID(http.ResponseWriter, *http.Request)
DeleteChannelByID(http.ResponseWriter, *http.Request)
GetAllRoutePolicies(http.ResponseWriter, *http.Request)
GetRoutePolicyByID(http.ResponseWriter, *http.Request)
CreateRoutePolicy(http.ResponseWriter, *http.Request)
UpdateRoutePolicy(http.ResponseWriter, *http.Request)
DeleteRoutePolicyByID(http.ResponseWriter, *http.Request)
}

View File

@@ -1,4 +1,4 @@
package alertmanager
package signozalertmanager
import (
"context"
@@ -7,6 +7,7 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
@@ -15,17 +16,15 @@ import (
"github.com/gorilla/mux"
)
type API struct {
alertmanager Alertmanager
type handler struct {
alertmanager alertmanager.Alertmanager
}
func NewAPI(alertmanager Alertmanager) *API {
return &API{
alertmanager: alertmanager,
}
func NewHandler(alertmanager alertmanager.Alertmanager) alertmanager.Handler {
return &handler{alertmanager: alertmanager}
}
func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetAlerts(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -41,7 +40,7 @@ func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
return
}
alerts, err := api.alertmanager.GetAlerts(ctx, claims.OrgID, params)
alerts, err := handler.alertmanager.GetAlerts(ctx, claims.OrgID, params)
if err != nil {
render.Error(rw, err)
return
@@ -50,7 +49,7 @@ func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, alerts)
}
func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) TestReceiver(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -73,7 +72,7 @@ func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
err = handler.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -82,7 +81,7 @@ func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) ListChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -92,7 +91,7 @@ func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
return
}
channels, err := api.alertmanager.ListChannels(ctx, claims.OrgID)
channels, err := handler.alertmanager.ListChannels(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
@@ -106,11 +105,11 @@ func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channels)
}
func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
channels, err := api.alertmanager.ListAllChannels(ctx)
channels, err := handler.alertmanager.ListAllChannels(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -119,7 +118,7 @@ func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channels)
}
func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -147,7 +146,7 @@ func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
channel, err := api.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
channel, err := handler.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -156,7 +155,7 @@ func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channel)
}
func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -197,7 +196,7 @@ func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
err = handler.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
if err != nil {
render.Error(rw, err)
return
@@ -206,7 +205,7 @@ func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -234,7 +233,7 @@ func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
err = handler.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -243,7 +242,7 @@ func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) CreateChannel(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -266,7 +265,7 @@ func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
return
}
channel, err := api.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
channel, err := handler.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -275,7 +274,7 @@ func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusCreated, channel)
}
func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -300,7 +299,7 @@ func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
return
}
result, err := api.alertmanager.CreateRoutePolicy(ctx, &policy)
result, err := handler.alertmanager.CreateRoutePolicy(ctx, &policy)
if err != nil {
render.Error(rw, err)
return
@@ -309,11 +308,11 @@ func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusCreated, result)
}
func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
policies, err := api.alertmanager.GetAllRoutePolicies(ctx)
policies, err := handler.alertmanager.GetAllRoutePolicies(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -322,7 +321,7 @@ func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, policies)
}
func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -333,7 +332,7 @@ func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
return
}
policy, err := api.alertmanager.GetRoutePolicyByID(ctx, policyID)
policy, err := handler.alertmanager.GetRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -342,7 +341,7 @@ func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, policy)
}
func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -353,7 +352,7 @@ func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request)
return
}
err := api.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
err := handler.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -362,7 +361,7 @@ func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -392,7 +391,7 @@ func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
return
}
result, err := api.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
result, err := handler.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
if err != nil {
render.Error(rw, err)
return

View File

@@ -309,8 +309,8 @@ func (provider *provider) CreateRoutePolicy(ctx context.Context, routeRequest *a
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -365,8 +365,8 @@ func (provider *provider) CreateRoutePolicies(ctx context.Context, routeRequests
results = append(results, &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -405,8 +405,8 @@ func (provider *provider) GetRoutePolicyByID(ctx context.Context, routeID string
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -439,8 +439,8 @@ func (provider *provider) GetAllRoutePolicies(ctx context.Context) ([]*alertmana
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -508,8 +508,8 @@ func (provider *provider) UpdateRoutePolicyByID(ctx context.Context, routeID str
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *route,
ID: updatedRoute.ID.StringValue(),
CreatedAt: &updatedRoute.CreatedAt,
UpdatedAt: &updatedRoute.UpdatedAt,
CreatedAt: updatedRoute.CreatedAt,
UpdatedAt: updatedRoute.UpdatedAt,
CreatedBy: &updatedRoute.CreatedBy,
UpdatedBy: &updatedRoute.UpdatedBy,
}, nil

View File

@@ -0,0 +1,235 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.ListChannels), handler.OpenAPIDef{
ID: "ListChannels",
Tags: []string{"channels"},
Summary: "List notification channels",
Description: "This endpoint lists all notification channels for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.Channel, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetChannelByID), handler.OpenAPIDef{
ID: "GetChannelByID",
Tags: []string{"channels"},
Summary: "Get notification channel by ID",
Description: "This endpoint returns a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
Description: "This endpoint creates a notification channel",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
Description: "This endpoint updates a notification channel by ID",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
Description: "This endpoint deletes a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/test", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannel",
Tags: []string{"channels"},
Summary: "Test notification channel",
Description: "This endpoint tests a notification channel by sending a test notification",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/testChannel", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannelDeprecated",
Tags: []string{"channels"},
Summary: "Test notification channel (deprecated)",
Description: "Deprecated: use /api/v1/channels/test instead",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAllRoutePolicies), handler.OpenAPIDef{
ID: "GetAllRoutePolicies",
Tags: []string{"routepolicies"},
Summary: "List route policies",
Description: "This endpoint lists all route policies for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.GettableRoutePolicy, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetRoutePolicyByID), handler.OpenAPIDef{
ID: "GetRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Get route policy by ID",
Description: "This endpoint returns a route policy by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
Description: "This endpoint creates a route policy",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
Description: "This endpoint updates a route policy by ID",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
Description: "This endpoint deletes a route policy by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/alerts", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAlerts), handler.OpenAPIDef{
ID: "GetAlerts",
Tags: []string{"alerts"},
Summary: "Get alerts",
Description: "This endpoint returns alerts for the organization",
Request: nil,
RequestContentType: "",
Response: make(alertmanagertypes.DeprecatedGettableAlerts, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -3,6 +3,7 @@ package signozapiserver
import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
@@ -57,6 +58,7 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
alertmanagerHandler alertmanager.Handler
}
func NewFactory(
@@ -83,6 +85,7 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -112,6 +115,7 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
alertmanagerHandler,
)
})
}
@@ -143,6 +147,7 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -172,6 +177,7 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
alertmanagerHandler: alertmanagerHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAlertmanagerRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -27,7 +27,6 @@ import (
"github.com/prometheus/prometheus/promql"
"github.com/SigNoz/signoz/pkg/alertmanager"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
@@ -140,8 +139,6 @@ type APIHandler struct {
pvcsRepo *inframetrics.PvcsRepo
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -168,8 +165,6 @@ type APIHandlerOpts struct {
// Flux Interval
FluxInterval time.Duration
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -230,7 +225,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
AlertmanagerAPI: opts.AlertmanagerAPI,
LicensingAPI: opts.LicensingAPI,
Signoz: opts.Signoz,
QueryParserAPI: opts.QueryParserAPI,
@@ -502,21 +496,6 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels", am.ViewAccess(aH.AlertmanagerAPI.ListChannels)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetChannelByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateChannelByID)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteChannelByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/channels", am.EditAccess(aH.AlertmanagerAPI.CreateChannel)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/testChannel", am.EditAccess(aH.AlertmanagerAPI.TestReceiver)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies", am.ViewAccess(aH.AlertmanagerAPI.GetAllRoutePolicies)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetRoutePolicyByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies", am.AdminAccess(aH.AlertmanagerAPI.CreateRoutePolicy)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteRoutePolicyByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateRoutePolicy)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)

View File

@@ -136,7 +136,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: nooplicensing.NewLicenseAPI(),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),

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

@@ -1,6 +1,8 @@
package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/signozauthzapi"
@@ -62,6 +64,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
AlertmanagerHandler alertmanager.Handler
}
func NewHandlers(
@@ -77,6 +80,7 @@ func NewHandlers(
authz authz.AuthZ,
zeusService zeus.Zeus,
registryHandler factory.Handler,
alertmanagerService alertmanager.Alertmanager,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
@@ -99,5 +103,6 @@ func NewHandlers(
RegistryHandler: registryHandler,
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
}
}

View File

@@ -56,7 +56,7 @@ func TestNewHandlers(t *testing.T) {
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -7,6 +7,7 @@ import (
"os"
"reflect"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/authz"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ alertmanager.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -288,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AlertmanagerHandler,
),
)
}

View File

@@ -500,7 +500,7 @@ func New(
// Initialize all handlers for the modules
registryHandler := factory.NewHandler(registry)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager)
// Initialize the API server (after registry so it can access service health)
apiserverInstance, err := factory.NewProviderFromNamedMap(

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

@@ -173,9 +173,29 @@ func (c *jsonConditionBuilder) terminalIndexedCondition(node *telemetrytypes.JSO
return "", errors.NewInternalf(CodeArrayNavigationFailed, "can not build index condition for array field %s", fieldPath)
}
indexedExpr := assumeNotNull(fmt.Sprintf("dynamicElement(%s, '%s')", fieldPath, node.TerminalConfig.ElemType.StringValue()))
elemType := node.TerminalConfig.ElemType
dynamicExpr := fmt.Sprintf("dynamicElement(%s, '%s')", fieldPath, elemType.StringValue())
indexedExpr := assumeNotNull(dynamicExpr)
// switch the operator and value for exists and not exists
switch operator {
case qbtypes.FilterOperatorExists:
operator = qbtypes.FilterOperatorNotEqual
value = getEmptyValue(elemType)
case qbtypes.FilterOperatorNotExists:
operator = qbtypes.FilterOperatorEqual
value = getEmptyValue(elemType)
default:
// do nothing
}
indexedExpr, formattedValue := querybuilder.DataTypeCollisionHandledFieldName(node.TerminalConfig.Key, value, indexedExpr, operator)
return c.applyOperator(sb, indexedExpr, operator, formattedValue)
cond, err := c.applyOperator(sb, indexedExpr, operator, formattedValue)
if err != nil {
return "", err
}
return cond, nil
}
// buildPrimitiveTerminalCondition builds the condition if the terminal node is a primitive type
@@ -184,31 +204,32 @@ func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetryty
fieldPath := node.FieldPath()
conditions := []string{}
// Utilize indexes when available, except for EXISTS/NOT EXISTS checks.
// Indexed columns always store a default empty value for absent fields (e.g. "" for strings,
// 0 for numbers), so using the index for existence checks would incorrectly exclude rows where
// the field genuinely holds the empty/zero value.
// utilize indexes for the condition if available
//
// Note: indexing is also skipped for Array Nested fields because they cannot be indexed.
// Note: Indexing code doesn't get executed for Array Nested fields because they can not be indexed
indexed := slices.ContainsFunc(node.TerminalConfig.Key.Indexes, func(index telemetrytypes.JSONDataTypeIndex) bool {
return index.Type == node.TerminalConfig.ElemType
})
isExistsCheck := operator == qbtypes.FilterOperatorExists || operator == qbtypes.FilterOperatorNotExists
if node.TerminalConfig.ElemType.IndexSupported && indexed && !isExistsCheck {
if node.TerminalConfig.ElemType.IndexSupported && indexed {
indexCond, err := c.terminalIndexedCondition(node, operator, value, sb)
if err != nil {
return "", err
}
// With a concrete non-zero value the index condition is self-contained.
// if qb has a definitive value, we can skip adding a condition to
// check the existence of the path in the json column
if value != nil && value != getEmptyValue(node.TerminalConfig.ElemType) {
return indexCond, nil
}
// The value is nil or the type's zero/empty value. Because indexed columns always store
// that zero value for absent fields, the index alone cannot distinguish "field is absent"
// from "field exists with zero value". Append a path-existence check (IS NOT NULL) as a
// second condition and AND them together.
conditions = append(conditions, indexCond)
operator = qbtypes.FilterOperatorExists
// Switch operator to EXISTS except when operator is NOT EXISTS since
// indexed paths on assumedNotNull, indexes will always have a default
// value so we flip the operator to Exists and filter the rows that
// actually have the value
if operator != qbtypes.FilterOperatorNotExists {
operator = qbtypes.FilterOperatorExists
}
}
var formattedValue = value
@@ -218,15 +239,20 @@ func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetryty
fieldExpr := fmt.Sprintf("dynamicElement(%s, '%s')", fieldPath, node.TerminalConfig.ElemType.StringValue())
// For non-nested paths with a negative comparison operator (e.g. !=, NOT LIKE, NOT IN),
// wrap in assumeNotNull so ClickHouse treats absent paths as the zero value rather than NULL,
// which would otherwise cause them to be silently dropped from results.
// NOT EXISTS is excluded: we want a true NULL check there, not a zero-value stand-in.
// if operator is negative and has a value comparison i.e. excluding EXISTS and NOT EXISTS, we need to assume that the field exists everywhere
//
// Note: for nested array paths, buildAccessNodeBranches already inverts the operator before
// reaching here, so IsNonNestedPath() guards against double-applying the wrapping.
if node.IsNonNestedPath() && operator.IsNegativeOperator() && operator != qbtypes.FilterOperatorNotExists {
fieldExpr = assumeNotNull(fieldExpr)
// Note: here applyNotCondition will return true only if; top level path is being queried and operator is a negative operator
// Otherwise this code will be triggered by buildAccessNodeBranches; Where operator would've been already inverted if needed.
if node.IsNonNestedPath() {
yes, _ := applyNotCondition(operator)
if yes {
switch operator {
case qbtypes.FilterOperatorNotExists:
// skip
default:
fieldExpr = assumeNotNull(fieldExpr)
}
}
}
fieldExpr, formattedValue = querybuilder.DataTypeCollisionHandledFieldName(node.TerminalConfig.Key, formattedValue, fieldExpr, operator)

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))]."},
},
},
{
@@ -844,10 +844,7 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Args: []any{uint64(1747945619), uint64(1747983448), float64(110001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed exists: index is skipped; emits plain IS NOT NULL ──────────
// EXISTS/NOT EXISTS bypass the index because indexed columns store a default
// empty value for absent fields, making != "" unreliable for existence checks
// (a field with value "" would be incorrectly excluded).
// ── indexed exists: emits assumeNotNull != nil AND dynamicElement IS NOT NULL ─
{
name: "Indexed String Exists",
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
@@ -856,8 +853,8 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "(dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL)",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
WhereClause: "((assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) <> ? AND dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL))",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed not-equal: assumeNotNull wrapping + no path index ─────────
@@ -873,6 +870,22 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Args: []any{uint64(1747945619), uint64(1747983448), "alice", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed not-exists: assumeNotNull = "" AND assumeNotNull IS NOT NULL ─
// FilterOperatorNotExists → Equal + emptyValue("") in the indexed branch,
// then a second condition flipped to Exists (IS NOT NULL) on the same
// assumeNotNull expr, producing AND(= "", IS NOT NULL).
{
name: "Indexed String NotExists",
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body.user.name NOT EXISTS"},
Limit: 10,
},
expected: TestExpected{
WhereClause: "((assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) = ? AND dynamicElement(body_v2.`user.name`, 'String') IS NULL))",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── special-character indexed paths ───────────────────────────────────
{
@@ -929,171 +942,20 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
})
}
}
func TestJSONStmtBuilder_SelectField(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "select_x_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "education[].awards[].participated[].members",
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "select_x_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "user.name",
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, dynamicElement(body_v2.`user.name`, 'String') AS `user.name` FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func TestJSONStmtBuilder_OrderBy(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "order_by_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "education[].awards[].participated[].members",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` asc LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "order_by_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user.name",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY dynamicElement(body_v2.`user.name`, 'String') AS `user.name` asc LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetrytypestest.MockMetadataStore {
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,84 +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")
)
// enrichJSONKeys 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.
// 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
//
// NOTE: enrichment can not work with FuzzySelectors; QB requests exact matches for query building so
// parentTypeCache will actually have proper matches and
// FuzzyMatching is for Suggestions API so enrichment is not needed
func (t *telemetryMetaStore) enrichJSONKeys(ctx context.Context, selectors []*telemetrytypes.FieldKeySelector, keys []*telemetrytypes.TelemetryFieldKey, parentTypeCache map[string][]telemetrytypes.FieldDataType) error {
mapOfExactSelectors := make(map[string]*telemetrytypes.FieldKeySelector)
for _, selector := range selectors {
if selector.SelectorMatchType != telemetrytypes.FieldSelectorMatchTypeExact {
continue
// 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",
})
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")
}
mapOfExactSelectors[selector.Name] = selector
}
var filteredKeys []*telemetrytypes.TelemetryFieldKey
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextBody && mapOfExactSelectors[key.Name] != nil {
filteredKeys = append(filteredKeys, key)
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")
}
if len(filteredKeys) == 0 {
return nil
}
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
}
}
@@ -106,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_field_keys 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_field_keys 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_field_keys 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_field_keys 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_field_keys 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,20 +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
nameColumn string // column holding the field/key name (e.g., "name" or "field_name")
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{
@@ -411,152 +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
}
key := &telemetrytypes.TelemetryFieldKey{
Name: sel.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
}
if !key.KeyNameContainsArray() {
continue
}
for _, parent := range key.ArrayParentPaths() {
parentPaths[parent] = 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,
nameColumn: "name",
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,
nameColumn: "name",
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),
nameColumn: "field_name",
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("field_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(
arm.nameColumn+" 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)
"name AS tag_key",
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(arm.nameColumn, 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(arm.nameColumn, "%"+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(arm.nameColumn, arm.dataTypeColumn)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
queries = append(queries, query)
allArgs = append(allArgs, args...)
@@ -593,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{}
@@ -617,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
@@ -681,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.enrichJSONKeys(ctx, fieldKeySelectors, keys, parentTypeCache); err != nil {
return nil, false, 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,13 +1,13 @@
package telemetrymetadata
import otelconst "github.com/SigNoz/signoz-otel-collector/constants"
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 = otelconst.DistributedFieldKeysTable
PathTypesTableName = otelcollectorconst.DistributedFieldKeysTable
// 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

@@ -34,10 +34,10 @@ type Channel struct {
types.Identifiable
types.TimeAuditable
Name string `json:"name" bun:"name"`
Type string `json:"type" bun:"type"`
Data string `json:"data" bun:"data"`
OrgID string `json:"org_id" bun:"org_id"`
Name string `json:"name" required:"true" bun:"name"`
Type string `json:"type" required:"true" bun:"type"`
Data string `json:"data" required:"true" bun:"data"`
OrgID string `json:"orgId" required:"true" bun:"org_id"`
}
// NewChannelFromReceiver creates a new Channel from a Receiver.

View File

@@ -12,12 +12,12 @@ import (
)
type PostableRoutePolicy struct {
Expression string `json:"expression"`
Expression string `json:"expression" required:"true"`
ExpressionKind ExpressionKind `json:"kind"`
Channels []string `json:"channels"`
Name string `json:"name"`
Channels []string `json:"channels" required:"true"`
Name string `json:"name" required:"true"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty"`
Tags []string `json:"tags,omitempty" nullable:"true"`
}
func (p *PostableRoutePolicy) Validate() error {
@@ -53,15 +53,13 @@ func (p *PostableRoutePolicy) Validate() error {
}
type GettableRoutePolicy struct {
PostableRoutePolicy // Embedded
PostableRoutePolicy
ID string `json:"id"`
// Audit fields
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CreatedBy *string `json:"createdBy"`
UpdatedBy *string `json:"updatedBy"`
ID string `json:"id" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
CreatedBy *string `json:"createdBy" nullable:"true"`
UpdatedBy *string `json:"updatedBy" nullable:"true"`
}
type ExpressionKind struct {
@@ -73,6 +71,13 @@ var (
PolicyBasedExpression = ExpressionKind{valuer.NewString("policy")}
)
func (ExpressionKind) Enum() []any {
return []any{
RuleBasedExpression,
PolicyBasedExpression,
}
}
// RoutePolicy represents the database model for expression routes.
type RoutePolicy struct {
bun.BaseModel `bun:"table:route_policy"`

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,11 +80,6 @@ func (f *TelemetryFieldKey) ArrayParentSelectors() []*FieldKeySelector {
return selectors
}
// GetJSONDataType derives the JSONDataType from FieldDataType.
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)
@@ -96,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 {
@@ -118,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("[]json")}
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

@@ -19,8 +19,7 @@ var (
BranchJSON = JSONAccessBranchType{valuer.NewString("json")}
BranchDynamic = JSONAccessBranchType{valuer.NewString("dynamic")}
CodePlanIndexOutOfBounds = errors.MustNewCode("plan_index_out_of_bounds")
CodePlanFieldDataTypeMissing = errors.MustNewCode("field_data_type_missing")
CodePlanIndexOutOfBounds = errors.MustNewCode("plan_index_out_of_bounds")
)
type JSONColumnMetadata struct {
@@ -43,6 +42,9 @@ type JSONAccessNode struct {
IsTerminal bool
isRoot bool // marked true for only body_v2 and body_promoted
// Precomputed type information (single source of truth)
AvailableTypes []JSONDataType
// Array type branches (Array(JSON) vs Array(Dynamic))
Branches map[JSONAccessBranchType]*JSONAccessNode
@@ -104,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.
@@ -136,41 +138,34 @@ func (pb *planBuilder) buildPlan(index int, parent *JSONAccessNode, isDynArrChil
}
}
// Use cached types from the batched metadata query
types, ok := pb.typeCache[pathSoFar]
if !ok {
return nil, errors.NewInternalf(errors.CodeInvalidInput, "types missing for path %s", pathSoFar)
}
// Create node for this path segment
node := &JSONAccessNode{
Name: segmentName,
IsTerminal: isTerminal,
AvailableTypes: types,
Branches: make(map[JSONAccessBranchType]*JSONAccessNode),
Parent: parent,
MaxDynamicTypes: maxTypes,
MaxDynamicPaths: maxPaths,
}
hasJSON := slices.Contains(node.AvailableTypes, ArrayJSON)
hasDynamic := slices.Contains(node.AvailableTypes, ArrayDynamic)
// Configure terminal if this is the last part
if isTerminal {
// fielddatatype must not be unspecified else expression can not be generated
if pb.key.FieldDataType == FieldDataTypeUnspecified {
return nil, errors.NewInternalf(CodePlanFieldDataTypeMissing, "field data type is missing for path %s", pathSoFar)
}
node.TerminalConfig = &TerminalConfig{
Key: pb.key,
ElemType: pb.key.GetJSONDataType(),
ElemType: *pb.key.JSONDataType,
}
} else {
var err error
// Use cached types from the batched metadata query
types, ok := pb.typeCache[pathSoFar]
if !ok {
return nil, errors.NewInternalf(errors.CodeInvalidInput, "types missing for path %s", pathSoFar)
}
hasJSON := slices.Contains(types, FieldDataTypeArrayJSON)
hasDynamic := slices.Contains(types, FieldDataTypeArrayDynamic)
if !hasJSON && !hasDynamic {
return nil, errors.NewInternalf(CodePlanFieldDataTypeMissing, "array data type missing for path %s", pathSoFar)
}
if hasJSON {
node.Branches[BranchJSON], err = pb.buildPlan(index+1, node, false)
if err != nil {
@@ -190,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,
}
}
@@ -65,6 +65,14 @@ func toTestNode(n *JSONAccessNode) *jsonAccessTestNode {
out.Column = n.Parent.Name
}
// AvailableTypes as strings (using StringValue for stable representation)
if len(n.AvailableTypes) > 0 {
out.AvailableTypes = make([]string, 0, len(n.AvailableTypes))
for _, t := range n.AvailableTypes {
out.AvailableTypes = append(out.AvailableTypes, t.StringValue())
}
}
// Terminal config
if n.TerminalConfig != nil {
out.ElemType = n.TerminalConfig.ElemType.StringValue()
@@ -234,12 +242,12 @@ 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
availableTypes:
- string
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
@@ -247,19 +255,19 @@ 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
availableTypes:
- string
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
- name: user.name
column: %s
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -268,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: "",
},
@@ -307,13 +315,13 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: name
availableTypes:
- string
- String
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -326,27 +334,27 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -360,42 +368,42 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: interests
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: entities
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: reviews
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: entries
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: metadata
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 1
branches:
json:
name: positions
availableTypes:
- "[]json"
- Array(JSON)
branches:
json:
name: name
availableTypes:
- string
- String
isTerminal: true
elemType: String
`, bodyV2Column),
@@ -407,13 +415,13 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: name
availableTypes:
- string
- String
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -423,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,
@@ -442,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,
@@ -454,27 +462,27 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -485,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,
@@ -497,27 +505,27 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -525,22 +533,22 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
maxDynamicPaths: 64
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
maxDynamicPaths: 16
isTerminal: true
@@ -548,7 +556,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -580,42 +588,42 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: interests
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: entities
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: reviews
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: entries
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: metadata
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 1
branches:
json:
name: positions
availableTypes:
- "[]json"
- Array(JSON)
branches:
json:
name: name
availableTypes:
- string
- String
isTerminal: true
elemType: String
`, bodyV2Column),
@@ -627,14 +635,14 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: type
availableTypes:
- string
- int64
- String
- Int64
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -647,7 +655,7 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
isTerminal: true
elemType: Array(JSON)
@@ -658,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{
@@ -684,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,
@@ -696,47 +704,47 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: participated
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 1
isTerminal: true
elemType: String
dynamic:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 8
maxDynamicPaths: 64
isTerminal: true
@@ -744,22 +752,22 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
dynamic:
name: participated
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
maxDynamicPaths: 64
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 4
maxDynamicPaths: 16
isTerminal: true
@@ -767,14 +775,14 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
dynamic:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 8
maxDynamicPaths: 64
isTerminal: true

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