mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-14 14:10:32 +01:00
Compare commits
10 Commits
nv/patch-d
...
chore/aler
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd0376125 | ||
|
|
b0c797b507 | ||
|
|
fe586228d3 | ||
|
|
5363dc6b0e | ||
|
|
83fa73c3e8 | ||
|
|
59a757f9bb | ||
|
|
16267e3172 | ||
|
|
b236a29a99 | ||
|
|
828459ab30 | ||
|
|
b572e30045 |
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
@@ -104,8 +103,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, tagModule)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
},
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM node:22-bookworm AS build
|
||||
WORKDIR /opt/
|
||||
COPY ./frontend/ ./
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
RUN CI=1 npm i -g pnpm
|
||||
RUN CI=1 npm i -g pnpm@10
|
||||
RUN CI=1 pnpm install
|
||||
RUN CI=1 pnpm build
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ import (
|
||||
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
@@ -152,8 +151,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
|
||||
1507
docs/api/openapi.yml
1507
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
@@ -32,9 +30,9 @@ type module struct {
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, sqlstore, settings, analytics, orgGetter, queryParser, tagModule)
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
|
||||
|
||||
return &module{
|
||||
pkgDashboardModule: pkgDashboardModule,
|
||||
@@ -199,68 +197,6 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
|
||||
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
|
||||
}
|
||||
|
||||
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, postable)
|
||||
}
|
||||
|
||||
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updateable)
|
||||
}
|
||||
|
||||
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) CreatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, postable dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.PublicConfig != nil {
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", id)
|
||||
}
|
||||
|
||||
publicDashboard := dashboardtypes.NewPublicDashboard(postable.TimeRangeEnabled, postable.DefaultTimeRange, id)
|
||||
if err := module.store.CreatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing.PublicConfig = publicDashboard
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.PublicConfig == nil {
|
||||
return nil, errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", id)
|
||||
}
|
||||
|
||||
existing.PublicConfig.Update(updatable.TimeRangeEnabled, updatable.DefaultTimeRange)
|
||||
if err := module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(existing.PublicConfig)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
@@ -18,34 +18,19 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateDashboardV2201,
|
||||
CreatePublicDashboard201,
|
||||
CreatePublicDashboardPathParameters,
|
||||
CreatePublicDashboardV2200,
|
||||
CreatePublicDashboardV2PathParameters,
|
||||
DashboardtypesJSONPatchDocumentDTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
DashboardtypesPostablePublicDashboardDTO,
|
||||
DashboardtypesUpdatablePublicDashboardDTO,
|
||||
DeletePublicDashboardPathParameters,
|
||||
GetDashboardV2200,
|
||||
GetDashboardV2PathParameters,
|
||||
GetPublicDashboard200,
|
||||
GetPublicDashboardData200,
|
||||
GetPublicDashboardDataPathParameters,
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
LockDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
UnlockDashboardV2PathParameters,
|
||||
UpdateDashboardV2200,
|
||||
UpdateDashboardV2PathParameters,
|
||||
UpdatePublicDashboardPathParameters,
|
||||
UpdatePublicDashboardV2200,
|
||||
UpdatePublicDashboardV2PathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
@@ -649,739 +634,3 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.
|
||||
* @summary Create dashboard (v2)
|
||||
*/
|
||||
export const createDashboardV2 = (
|
||||
dashboardtypesPostableDashboardV2DTO: BodyType<DashboardtypesPostableDashboardV2DTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDashboardV2201>({
|
||||
url: `/api/v2/dashboards`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardV2DTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDashboardV2'];
|
||||
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 createDashboardV2>>,
|
||||
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createDashboardV2(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>
|
||||
>;
|
||||
export type CreateDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create dashboard (v2)
|
||||
*/
|
||||
export const useCreateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint returns a v2-shape dashboard with its tags and public sharing config (if any).
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
export const getDashboardV2 = (
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetDashboardV2QueryKey = ({
|
||||
id,
|
||||
}: GetDashboardV2PathParameters) => {
|
||||
return [`/api/v2/dashboards/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetDashboardV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetDashboardV2QueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDashboardV2>>> = ({
|
||||
signal,
|
||||
}) => getDashboardV2({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetDashboardV2QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>
|
||||
>;
|
||||
export type GetDashboardV2QueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
|
||||
export function useGetDashboardV2<
|
||||
TData = Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetDashboardV2QueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
export const invalidateGetDashboardV2 = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetDashboardV2QueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Locked dashboards are rejected.
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const patchDashboardV2 = (
|
||||
{ id }: PatchDashboardV2PathParameters,
|
||||
dashboardtypesJSONPatchDocumentDTO: BodyType<DashboardtypesJSONPatchDocumentDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PatchDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesJSONPatchDocumentDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPatchDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['patchDashboardV2'];
|
||||
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 patchDashboardV2>>,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return patchDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PatchDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>
|
||||
>;
|
||||
export type PatchDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
export type PatchDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const usePatchDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesJSONPatchDocumentDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getPatchDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.
|
||||
* @summary Update dashboard (v2)
|
||||
*/
|
||||
export const updateDashboardV2 = (
|
||||
{ id }: UpdateDashboardV2PathParameters,
|
||||
dashboardtypesPostableDashboardV2DTO: BodyType<DashboardtypesPostableDashboardV2DTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardV2DTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateDashboardV2'];
|
||||
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 updateDashboardV2>>,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>
|
||||
>;
|
||||
export type UpdateDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
export type UpdateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update dashboard (v2)
|
||||
*/
|
||||
export const useUpdateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const unlockDashboardV2 = ({ id }: UnlockDashboardV2PathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getUnlockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['unlockDashboardV2'];
|
||||
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 unlockDashboardV2>>,
|
||||
{ pathParams: UnlockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return unlockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UnlockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type UnlockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const useUnlockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUnlockDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const lockDashboardV2 = ({ id }: LockDashboardV2PathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'PUT',
|
||||
});
|
||||
};
|
||||
|
||||
export const getLockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['lockDashboardV2'];
|
||||
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 lockDashboardV2>>,
|
||||
{ pathParams: LockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return lockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type LockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type LockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const useLockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getLockDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint creates the public sharing config for a v2 dashboard and returns the dashboard with the new public config attached. Lock state does not gate this endpoint.
|
||||
* @summary Make a dashboard v2 public
|
||||
*/
|
||||
export const createPublicDashboardV2 = (
|
||||
{ id }: CreatePublicDashboardV2PathParameters,
|
||||
dashboardtypesPostablePublicDashboardDTO: BodyType<DashboardtypesPostablePublicDashboardDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreatePublicDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/public`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostablePublicDashboardDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreatePublicDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createPublicDashboardV2'];
|
||||
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 createPublicDashboardV2>>,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createPublicDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreatePublicDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>
|
||||
>;
|
||||
export type CreatePublicDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
export type CreatePublicDashboardV2MutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Make a dashboard v2 public
|
||||
*/
|
||||
export const useCreatePublicDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createPublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesPostablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreatePublicDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint updates the public sharing config (time range settings) of an already-public v2 dashboard. Lock state does not gate this endpoint.
|
||||
* @summary Update public sharing config for a dashboard v2
|
||||
*/
|
||||
export const updatePublicDashboardV2 = (
|
||||
{ id }: UpdatePublicDashboardV2PathParameters,
|
||||
dashboardtypesUpdatablePublicDashboardDTO: BodyType<DashboardtypesUpdatablePublicDashboardDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdatePublicDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}/public`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesUpdatablePublicDashboardDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdatePublicDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updatePublicDashboardV2'];
|
||||
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 updatePublicDashboardV2>>,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updatePublicDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdatePublicDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>
|
||||
>;
|
||||
export type UpdatePublicDashboardV2MutationBody =
|
||||
BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
export type UpdatePublicDashboardV2MutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update public sharing config for a dashboard v2
|
||||
*/
|
||||
export const useUpdatePublicDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updatePublicDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdatePublicDashboardV2PathParameters;
|
||||
data: BodyType<DashboardtypesUpdatablePublicDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdatePublicDashboardV2MutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
InframonitoringtypesPostableNamespacesDTO,
|
||||
InframonitoringtypesPostableNodesDTO,
|
||||
InframonitoringtypesPostablePodsDTO,
|
||||
InframonitoringtypesPostableStatefulSetsDTO,
|
||||
InframonitoringtypesPostableVolumesDTO,
|
||||
ListClusters200,
|
||||
ListDeployments200,
|
||||
@@ -25,6 +26,7 @@ import type {
|
||||
ListNamespaces200,
|
||||
ListNodes200,
|
||||
ListPods200,
|
||||
ListStatefulSets200,
|
||||
ListVolumes200,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -620,3 +622,87 @@ export const useListVolumes = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List StatefulSets for Infra Monitoring
|
||||
*/
|
||||
export const listStatefulSets = (
|
||||
inframonitoringtypesPostableStatefulSetsDTO: BodyType<InframonitoringtypesPostableStatefulSetsDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListStatefulSets200>({
|
||||
url: `/api/v2/infra_monitoring/statefulsets`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostableStatefulSetsDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListStatefulSetsMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listStatefulSets>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableStatefulSetsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listStatefulSets>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableStatefulSetsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listStatefulSets'];
|
||||
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 listStatefulSets>>,
|
||||
{ data: BodyType<InframonitoringtypesPostableStatefulSetsDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listStatefulSets(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListStatefulSetsMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listStatefulSets>>
|
||||
>;
|
||||
export type ListStatefulSetsMutationBody =
|
||||
BodyType<InframonitoringtypesPostableStatefulSetsDTO>;
|
||||
export type ListStatefulSetsMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List StatefulSets for Infra Monitoring
|
||||
*/
|
||||
export const useListStatefulSets = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listStatefulSets>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableStatefulSetsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listStatefulSets>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableStatefulSetsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListStatefulSetsMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -437,11 +437,16 @@ export function convertTraceOperatorToV5(
|
||||
panelType,
|
||||
);
|
||||
|
||||
// Skip aggregation for raw request type
|
||||
// Skip aggregation for raw request type. Force dataSource to traces so
|
||||
// createAggregation never takes the metrics branch (which would emit a
|
||||
// metricName field the backend rejects for trace operators).
|
||||
const aggregations =
|
||||
requestType === 'raw'
|
||||
? undefined
|
||||
: createAggregation(traceOperatorData, panelType);
|
||||
: createAggregation(
|
||||
{ ...traceOperatorData, dataSource: DataSource.TRACES },
|
||||
panelType,
|
||||
);
|
||||
|
||||
const spec: QueryEnvelope['spec'] = {
|
||||
name: queryName,
|
||||
|
||||
@@ -596,6 +596,7 @@ function CustomTimePicker({
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoComplete="off"
|
||||
className={cx(
|
||||
'timeSelection-input',
|
||||
inputStatus === CustomTimePickerInputStatus.ERROR ? 'error' : '',
|
||||
|
||||
@@ -220,6 +220,7 @@ export function buildCreateThresholdAlertRulePayload(
|
||||
builderQueries: {
|
||||
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||
...mapQueryDataToApi(query.builder.queryTraceOperator, 'queryName').data,
|
||||
},
|
||||
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||
|
||||
@@ -91,7 +91,6 @@ function ChartPreview({
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -107,7 +106,6 @@ function ChartPreview({
|
||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { CreateAlertProvider } from '../../context';
|
||||
import ChartPreview from '../ChartPreview/ChartPreview';
|
||||
|
||||
const REQUESTS_PER_SEC = 'requests/sec';
|
||||
const CHART_PREVIEW_NAME = 'Chart Preview';
|
||||
const QUERY_TYPE_TEST_ID = 'query-type';
|
||||
const GRAPH_TYPE_TEST_ID = 'graph-type';
|
||||
const CHART_PREVIEW_COMPONENT_TEST_ID = 'chart-preview-component';
|
||||
@@ -34,7 +33,6 @@ jest.mock(
|
||||
return (
|
||||
<div data-testid={CHART_PREVIEW_COMPONENT_TEST_ID}>
|
||||
<div data-testid="headline">{props.headline}</div>
|
||||
<div data-testid="name">{props.name}</div>
|
||||
<div data-testid={QUERY_TYPE_TEST_ID}>{props.query?.queryType}</div>
|
||||
<div data-testid="selected-interval">
|
||||
{props.selectedInterval?.startTime}
|
||||
@@ -175,12 +173,6 @@ describe('ChartPreview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with empty name when query type is QUERY_BUILDER', () => {
|
||||
renderChartPreview();
|
||||
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with correct props', () => {
|
||||
renderChartPreview();
|
||||
|
||||
@@ -191,7 +183,6 @@ describe('ChartPreview', () => {
|
||||
expect(screen.getByTestId(GRAPH_TYPE_TEST_ID)).toHaveTextContent(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
);
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
expect(screen.getByTestId('headline')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('selected-interval')).toBeInTheDocument();
|
||||
});
|
||||
@@ -214,7 +205,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.PROM,
|
||||
);
|
||||
@@ -238,7 +228,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.CLICKHOUSE,
|
||||
);
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import get from 'lodash-es/get';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
@@ -35,22 +34,6 @@ export const prepareChartData = (
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
function hasSingleVisiblePointForSeries(series: QueryData): boolean {
|
||||
const rawValues = series.values ?? [];
|
||||
let validPointCount = 0;
|
||||
|
||||
for (const [, rawValue] of rawValues) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const prepareUPlotConfig = ({
|
||||
widget,
|
||||
isDarkMode,
|
||||
@@ -107,7 +90,7 @@ export const prepareUPlotConfig = ({
|
||||
}
|
||||
|
||||
apiResponse.data.result.forEach((series) => {
|
||||
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { AlertChartPanelType, buildAlertChartConfig } from './utils';
|
||||
|
||||
const CHART_ID_PREFIX = 'alert_legend_widget';
|
||||
|
||||
const buildChartId = (alertId?: string): string =>
|
||||
alertId ? `${CHART_ID_PREFIX}_${alertId}` : CHART_ID_PREFIX;
|
||||
|
||||
// Panel types that render through the UPlotConfigBuilder pipeline.
|
||||
// To support a new modern-chart panel type, add an entry here and extend
|
||||
// `AlertChartPanelType` / `buildAlertChartConfig` to handle its series setup.
|
||||
const SUPPORTED_CHARTS: Record<
|
||||
AlertChartPanelType,
|
||||
typeof TimeSeries | typeof BarChart
|
||||
> = {
|
||||
[PANEL_TYPES.TIME_SERIES]: TimeSeries,
|
||||
[PANEL_TYPES.BAR]: BarChart,
|
||||
};
|
||||
|
||||
const isSupportedPanelType = (
|
||||
panelType: PANEL_TYPES,
|
||||
): panelType is AlertChartPanelType => panelType in SUPPORTED_CHARTS;
|
||||
|
||||
export interface ChartContentProps {
|
||||
panelType: PANEL_TYPES;
|
||||
alertId?: string;
|
||||
query: Query;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
data: uPlot.AlignedData;
|
||||
thresholds: ThresholdProps[];
|
||||
yAxisUnit: string;
|
||||
legendPosition: LegendPosition;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
width: number;
|
||||
height: number;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
export default function ChartContent({
|
||||
panelType,
|
||||
alertId,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
data,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
width,
|
||||
height,
|
||||
legendPosition,
|
||||
}: ChartContentProps): JSX.Element | null {
|
||||
const supported = isSupportedPanelType(panelType);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
buildAlertChartConfig({
|
||||
id: buildChartId(alertId),
|
||||
panelType: panelType as AlertChartPanelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
}),
|
||||
[
|
||||
alertId,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
],
|
||||
);
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = SUPPORTED_CHARTS[panelType];
|
||||
|
||||
return (
|
||||
<Component
|
||||
config={config}
|
||||
data={data}
|
||||
width={width}
|
||||
height={height}
|
||||
legendConfig={{ position: legendPosition }}
|
||||
canPinTooltip
|
||||
yAxisUnit={yAxisUnit}
|
||||
timezone={timezone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
@@ -32,7 +30,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -41,7 +39,6 @@ import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -52,13 +49,16 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { AlertDetectionTypes } from '..';
|
||||
import ChartContent from './ChartContent';
|
||||
import { ChartContainer } from './styles';
|
||||
import { getThresholds } from './utils';
|
||||
|
||||
import './ChartPreview.styles.scss';
|
||||
|
||||
// Height reserved for the `.chart-preview-header` strip rendered above the chart.
|
||||
const CHART_PREVIEW_HEADER_HEIGHT = 48;
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | null;
|
||||
graphType?: PANEL_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
@@ -77,7 +77,6 @@ export interface ChartPreviewProps {
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
@@ -113,14 +112,6 @@ function ChartPreview({
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
@@ -219,18 +210,6 @@ function ChartPreview({
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||
|
||||
// Initialize graph visibility from localStorage
|
||||
useEffect(() => {
|
||||
if (queryResponse?.data?.payload?.data?.result) {
|
||||
const { graphVisibilityStates: localStoredVisibilityState } =
|
||||
getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data.payload.data.result,
|
||||
name: 'alert-chart-preview',
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [queryResponse?.data?.payload?.data?.result]);
|
||||
|
||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
@@ -288,59 +267,9 @@ function ChartPreview({
|
||||
return LegendPosition.RIGHT;
|
||||
}, [queryResponse?.data?.payload?.data?.result?.length, showSideLegend]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: 'alert_legend_widget',
|
||||
yAxisUnit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: {
|
||||
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
|
||||
width: containerDimensions?.width,
|
||||
},
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds: getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
panelType: graphType,
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
currentQuery,
|
||||
query: query || currentQuery,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
enhancedLegend: true,
|
||||
legendPosition,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
yAxisUnit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
t,
|
||||
optionName,
|
||||
graphType,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
query,
|
||||
graphVisibility,
|
||||
legendPosition,
|
||||
],
|
||||
const resolvedThresholds = useMemo(
|
||||
() => getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
[thresholds, t, optionName, yAxisUnit],
|
||||
);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
@@ -361,6 +290,12 @@ function ChartPreview({
|
||||
?.active || false;
|
||||
|
||||
const isWarning = !isEmpty(queryResponse.data?.warning);
|
||||
|
||||
const chartWidth = containerDimensions?.width ?? 0;
|
||||
const chartHeight = containerDimensions?.height
|
||||
? containerDimensions.height - CHART_PREVIEW_HEADER_HEIGHT
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="alert-chart-container" ref={graphRef}>
|
||||
<ChartContainer>
|
||||
@@ -384,16 +319,22 @@ function ChartPreview({
|
||||
)}
|
||||
|
||||
{chartDataAvailable && !isAnomalyDetectionAlert && (
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
<ChartContent
|
||||
panelType={graphType}
|
||||
data={chartData}
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={
|
||||
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
||||
}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
alertId={alertDef?.id}
|
||||
query={query || currentQuery}
|
||||
apiResponse={queryResponse.data?.payload}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
thresholds={resolvedThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
legendPosition={legendPosition}
|
||||
isDarkMode={isDarkMode}
|
||||
timezone={timezone}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
minTimeScale={minTimeScale}
|
||||
maxTimeScale={maxTimeScale}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import {
|
||||
BooleanFormats,
|
||||
@@ -11,6 +15,20 @@ import {
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DrawStyle,
|
||||
FillMode,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { get } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
dataFormatConfig,
|
||||
@@ -142,3 +160,107 @@ export const getThresholds = (
|
||||
});
|
||||
return thresholdsToReturn;
|
||||
};
|
||||
|
||||
export type AlertChartPanelType = PANEL_TYPES.TIME_SERIES | PANEL_TYPES.BAR;
|
||||
|
||||
export interface BuildAlertChartConfigParams {
|
||||
id: string;
|
||||
panelType: AlertChartPanelType;
|
||||
query: Query;
|
||||
thresholds: ThresholdProps[];
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
yAxisUnit?: string;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
}
|
||||
|
||||
export const buildAlertChartConfig = ({
|
||||
id,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
}: BuildAlertChartConfigParams): UPlotConfigBuilder => {
|
||||
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||
apiResponse,
|
||||
'data.newResult.meta.stepIntervals',
|
||||
{},
|
||||
);
|
||||
const stepIntervalValues = Object.values(stepIntervals);
|
||||
const minStepInterval = stepIntervalValues.length
|
||||
? Math.min(...stepIntervalValues)
|
||||
: undefined;
|
||||
|
||||
const builder = buildBaseConfig({
|
||||
id,
|
||||
panelType,
|
||||
panelMode: PanelMode.DASHBOARD_VIEW,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
stepInterval: minStepInterval,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
});
|
||||
|
||||
const seriesList = apiResponse?.data?.result;
|
||||
if (!seriesList?.length) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
const isBar = panelType === PANEL_TYPES.BAR;
|
||||
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '',
|
||||
series.legend || '',
|
||||
);
|
||||
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||
|
||||
if (isBar) {
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label,
|
||||
colorMapping: {},
|
||||
isDarkMode,
|
||||
stepInterval: get(stepIntervals, series.queryName, undefined),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
|
||||
label,
|
||||
colorMapping: {},
|
||||
spanGaps: true,
|
||||
lineStyle: LineStyle.Solid,
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
showPoints: hasSingleValidPoint,
|
||||
pointSize: 5,
|
||||
fillMode: FillMode.None,
|
||||
isDarkMode,
|
||||
metric: series.metric,
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
@@ -709,7 +709,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -729,7 +728,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { patchRulePartial } from 'api/alerts/patchRulePartial';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { invalidateGetRuleByID } from 'api/generated/services/rules';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesRuleDTO,
|
||||
@@ -28,6 +30,7 @@ function ToggleAlertState({
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onToggleHandler = async (
|
||||
id: string,
|
||||
@@ -60,6 +63,9 @@ function ToggleAlertState({
|
||||
loading: false,
|
||||
payload: updatedRule,
|
||||
}));
|
||||
|
||||
invalidateGetRuleByID(queryClient, { id });
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
});
|
||||
|
||||
@@ -53,7 +53,9 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
}
|
||||
} else if (q.type === 'builder_trace_operator') {
|
||||
if (spec.name) {
|
||||
builderQueries[spec.name] = spec as unknown as IBuilderTraceOperator;
|
||||
builderQueries[spec.name] = convertBuilderQueryToIBuilderQuery(
|
||||
spec as BuilderQuery,
|
||||
) as IBuilderTraceOperator;
|
||||
builderQueryTypes[spec.name] = 'builder_trace_operator';
|
||||
}
|
||||
} else if (q.type === 'promql') {
|
||||
|
||||
@@ -52,6 +52,28 @@ export function normalizePlotValue(
|
||||
return value as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at most one entry in `values` is a valid plot value.
|
||||
*
|
||||
* Used to decide whether a series should render as a single point (drawStyle:
|
||||
* Points) vs a line — a continuous line with only one visible sample is
|
||||
* invisible to the user.
|
||||
*/
|
||||
export function hasSingleVisiblePoint(
|
||||
values: ReadonlyArray<readonly [unknown, unknown]> | undefined,
|
||||
): boolean {
|
||||
let validPointCount = 0;
|
||||
for (const [, rawValue] of values ?? []) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface SeriesSpanGapsOption {
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function AlertState({
|
||||
let label;
|
||||
const isDarkMode = useIsDarkMode();
|
||||
switch (state) {
|
||||
case 'no-data':
|
||||
case 'nodata':
|
||||
icon = (
|
||||
<CircleOff
|
||||
size={18}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
deleteRuleByID,
|
||||
invalidateGetRuleByID,
|
||||
updateRuleByID,
|
||||
useGetRuleByID,
|
||||
useListRules,
|
||||
@@ -408,6 +409,7 @@ export const useAlertRuleStatusToggle = ({
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setAlertRuleState(data.data.state);
|
||||
invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
notifications.success({
|
||||
message: `Alert has been ${
|
||||
@@ -416,6 +418,7 @@ export const useAlertRuleStatusToggle = ({
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
|
||||
@@ -109,7 +109,8 @@ export type AlertRuleTimelineTableResponsePayload = {
|
||||
labels: AlertLabelsProps['labels'];
|
||||
};
|
||||
};
|
||||
type AlertState = 'firing' | 'normal' | 'no-data' | 'muted';
|
||||
|
||||
type AlertState = 'firing' | 'normal' | 'nodata' | 'muted';
|
||||
|
||||
export interface AlertRuleTimelineGraphResponse {
|
||||
start: number;
|
||||
|
||||
1
go.mod
1
go.mod
@@ -88,7 +88,6 @@ require (
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/api v0.265.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.35.2
|
||||
|
||||
@@ -14,148 +14,6 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.CreateV2), handler.OpenAPIDef{
|
||||
ID: "CreateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create dashboard (v2)",
|
||||
Description: "This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.",
|
||||
Request: new(dashboardtypes.PostableDashboardV2),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.GetV2), handler.OpenAPIDef{
|
||||
ID: "GetDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get dashboard (v2)",
|
||||
Description: "This endpoint returns a v2-shape dashboard with its tags and public sharing config (if any).",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
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/v2/dashboards/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.UpdateV2), handler.OpenAPIDef{
|
||||
ID: "UpdateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update dashboard (v2)",
|
||||
Description: "This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.",
|
||||
Request: new(dashboardtypes.UpdateableDashboardV2),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.PatchV2), handler.OpenAPIDef{
|
||||
ID: "PatchDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Patch dashboard (v2)",
|
||||
Description: "This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Locked dashboards are rejected.",
|
||||
Request: new(dashboardtypes.JSONPatchDocument),
|
||||
// Strictly per RFC 6902 the content type is `application/json-patch+json`,
|
||||
// but our OpenAPI generator only reflects schemas for content types it
|
||||
// understands (application/json, form-urlencoded, multipart) — anything
|
||||
// else degrades to `type: string`. Declaring application/json here keeps
|
||||
// the array-of-ops schema visible to spec consumers; the runtime decoder
|
||||
// parses JSON regardless of the request's actual Content-Type header.
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.LockV2), handler.OpenAPIDef{
|
||||
ID: "LockDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Lock dashboard (v2)",
|
||||
Description: "This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.UnlockV2), handler.OpenAPIDef{
|
||||
ID: "UnlockDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Unlock dashboard (v2)",
|
||||
Description: "This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublicV2), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Make a dashboard v2 public",
|
||||
Description: "This endpoint creates the public sharing config for a v2 dashboard and returns the dashboard with the new public config attached. Lock state does not gate this endpoint.",
|
||||
Request: new(dashboardtypes.PostablePublicDashboard),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublicV2), handler.OpenAPIDef{
|
||||
ID: "UpdatePublicDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update public sharing config for a dashboard v2",
|
||||
Description: "This endpoint updates the public sharing config (time range settings) of an already-public v2 dashboard. Lock state does not gate this endpoint.",
|
||||
Request: new(dashboardtypes.UpdatablePublicDashboard),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
|
||||
@@ -143,5 +143,24 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/statefulsets", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListStatefulSets),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListStatefulSets",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List StatefulSets for Infra Monitoring",
|
||||
Description: "Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.",
|
||||
Request: new(inframonitoringtypes.PostableStatefulSets),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.StatefulSets),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,24 +49,6 @@ type Module interface {
|
||||
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
|
||||
|
||||
statsreporter.StatsCollector
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// v2 dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error
|
||||
|
||||
CreatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, postable dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
UpdatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -89,23 +71,4 @@ type Handler interface {
|
||||
LockUnlock(http.ResponseWriter, *http.Request)
|
||||
|
||||
Delete(http.ResponseWriter, *http.Request)
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// v2 dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
CreateV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdateV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
PatchV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
LockV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UnlockV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
CreatePublicV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdatePublicV2(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
@@ -22,24 +20,20 @@ import (
|
||||
|
||||
type module struct {
|
||||
store dashboardtypes.Store
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ScopedProviderSettings
|
||||
analytics analytics.Analytics
|
||||
orgGetter organization.Getter
|
||||
queryParser queryparser.QueryParser
|
||||
tagModule tag.Module
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, tagModule tag.Module) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard")
|
||||
return &module{
|
||||
store: store,
|
||||
sqlstore: sqlstore,
|
||||
settings: scopedProviderSettings,
|
||||
analytics: analytics,
|
||||
orgGetter: orgGetter,
|
||||
queryParser: queryParser,
|
||||
tagModule: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
@@ -65,97 +63,6 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
func (store *store) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.StorableDashboard, *dashboardtypes.StorablePublicDashboard, error) {
|
||||
type joinedRow struct {
|
||||
*dashboardtypes.StorableDashboard `bun:",extend"`
|
||||
|
||||
PublicID *valuer.UUID `bun:"public_id"`
|
||||
PublicCreatedAt *time.Time `bun:"public_created_at"`
|
||||
PublicUpdatedAt *time.Time `bun:"public_updated_at"`
|
||||
PublicTimeRangeEnabled *bool `bun:"public_time_range_enabled"`
|
||||
PublicDefaultTimeRange *string `bun:"public_default_time_range"`
|
||||
}
|
||||
|
||||
row := &joinedRow{StorableDashboard: new(dashboardtypes.StorableDashboard)}
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(row).
|
||||
ColumnExpr("dashboard.id, dashboard.org_id, dashboard.data, dashboard.locked, dashboard.created_at, dashboard.created_by, dashboard.updated_at, dashboard.updated_by").
|
||||
ColumnExpr("pd.id AS public_id, pd.created_at AS public_created_at, pd.updated_at AS public_updated_at, pd.time_range_enabled AS public_time_range_enabled, pd.default_time_range AS public_default_time_range").
|
||||
Join("LEFT JOIN public_dashboard AS pd ON pd.dashboard_id = dashboard.id").
|
||||
Where("dashboard.id = ?", id).
|
||||
Where("dashboard.org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodeDashboardNotFound, "dashboard with id %s doesn't exist", id)
|
||||
}
|
||||
|
||||
if row.PublicID == nil {
|
||||
return row.StorableDashboard, nil, nil
|
||||
}
|
||||
public := &dashboardtypes.StorablePublicDashboard{
|
||||
Identifiable: types.Identifiable{ID: *row.PublicID},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: *row.PublicCreatedAt, UpdatedAt: *row.PublicUpdatedAt},
|
||||
TimeRangeEnabled: *row.PublicTimeRangeEnabled,
|
||||
DefaultTimeRange: *row.PublicDefaultTimeRange,
|
||||
DashboardID: row.ID.StringValue(),
|
||||
}
|
||||
return row.StorableDashboard, public, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.StorableDashboardData) error {
|
||||
res, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model((*dashboardtypes.StorableDashboard)(nil)).
|
||||
Set("data = ?", data).
|
||||
Set("updated_by = ?", updatedBy).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Defends against the race where a delete lands between the caller's
|
||||
// pre-update GetV2 and this update.
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardNotFound, "dashboard with id %s doesn't exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, locked bool, updatedBy string) error {
|
||||
res, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model((*dashboardtypes.StorableDashboard)(nil)).
|
||||
Set("locked = ?", locked).
|
||||
Set("updated_by = ?", updatedBy).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardNotFound, "dashboard with id %s doesn't exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) {
|
||||
storable := new(dashboardtypes.StorablePublicDashboard)
|
||||
err := store.
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PostableDashboardV2{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.CreateV2(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) GetV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.GetV2(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) LockV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.lockUnlockV2(rw, r, true)
|
||||
}
|
||||
|
||||
func (handler *handler) UnlockV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.lockUnlockV2(rw, r, false)
|
||||
}
|
||||
|
||||
func (handler *handler) lockUnlockV2(rw http.ResponseWriter, r *http.Request, lock bool) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
err = handler.authz.CheckWithTupleCreation(
|
||||
ctx,
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
if err == nil {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
if err := handler.module.LockUnlockV2(ctx, orgID, dashboardID, claims.Email, isAdmin, lock); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdateableDashboardV2{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.UpdateV2(ctx, orgID, dashboardID, claims.Email, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) PatchV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PatchableDashboardV2{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.PatchV2(ctx, orgID, dashboardID, claims.Email, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) CreatePublicV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PostablePublicDashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.CreatePublicV2(ctx, orgID, dashboardID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) UpdatePublicV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdatablePublicDashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.UpdatePublicV2(ctx, orgID, dashboardID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableDashboardV2FromDashboardV2(dashboard))
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
if err := postable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboard := dashboardtypes.NewDashboardV2(orgID, createdBy, postable, nil)
|
||||
var storableDashboard *dashboardtypes.StorableDashboard
|
||||
|
||||
err := module.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, dashboard.ID, postable.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dashboard.Info.Tags = resolvedTags
|
||||
|
||||
storable, err := dashboard.ToStorableDashboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storableDashboard = storable
|
||||
return module.store.Create(ctx, storable)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storable, public, err := module.store.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := module.tagModule.ListForResource(ctx, orgID, coretypes.KindDashboard, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardV2FromStorable(storable, public, tags)
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
if err := updateable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Locked-dashboard / state gate — independent of tags, so run it before the tx.
|
||||
if err := existing.CanUpdate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, updateable.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = existing.Update(updateable, updatedBy, resolvedTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storable, err := existing.ToStorableDashboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdateV2(ctx, orgID, id, updatedBy, storable.Data)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Locked-dashboard / state gate — independent of tags, so run it before the tx.
|
||||
if err := existing.CanUpdate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateable, err := patch.Apply(existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = module.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, updateable.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = existing.Update(*updateable, updatedBy, resolvedTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storable, err := existing.ToStorableDashboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdateV2(ctx, orgID, id, updatedBy, storable.Data)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
// CreatePublicV2 is not supported in the community build.
|
||||
func (module *module) CreatePublicV2(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
// UpdatePublicV2 is not supported in the community build.
|
||||
func (module *module) UpdatePublicV2(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := existing.LockUnlock(lock, isAdmin, updatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.LockUnlockV2(ctx, orgID, id, lock, updatedBy)
|
||||
}
|
||||
@@ -189,3 +189,27 @@ func (h *handler) ListDeployments(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListStatefulSets(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var parsedReq inframonitoringtypes.PostableStatefulSets
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListStatefulSets(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -706,3 +706,100 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.StatefulSets{}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: inframonitoringtypes.StatefulSetsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{statefulSetNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, statefulSetsTableMetricNamesList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(missingMetrics) > 0 {
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
|
||||
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
|
||||
|
||||
metadataMap, err := m.getStatefulSetsTableMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopStatefulSetGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a StatefulSet carry
|
||||
// k8s.statefulset.name as a resource attribute, so default-groupBy gives
|
||||
// per-statefulset phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildStatefulSetRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
148
pkg/modules/inframonitoring/implinframonitoring/statefulsets.go
Normal file
148
pkg/modules/inframonitoring/implinframonitoring/statefulsets.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// buildStatefulSetRecords assembles the page records. Pod phase counts come from
|
||||
// phaseCounts in both modes; every row is a group of pods (one statefulset in
|
||||
// list mode, an arbitrary roll-up in grouped_list mode), so there's no
|
||||
// per-row "current phase" concept.
|
||||
func buildStatefulSetRecords(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
) []inframonitoringtypes.StatefulSetRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.StatefulSetRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
statefulSetName := labels[statefulSetNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.StatefulSetRecord{ // initialize with default values
|
||||
StatefulSetName: statefulSetName,
|
||||
StatefulSetCPU: -1,
|
||||
StatefulSetCPURequest: -1,
|
||||
StatefulSetCPULimit: -1,
|
||||
StatefulSetMemory: -1,
|
||||
StatefulSetMemoryRequest: -1,
|
||||
StatefulSetMemoryLimit: -1,
|
||||
DesiredPods: -1,
|
||||
CurrentPods: -1,
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["A"]; exists {
|
||||
record.StatefulSetCPU = v
|
||||
}
|
||||
if v, exists := metrics["B"]; exists {
|
||||
record.StatefulSetCPURequest = v
|
||||
}
|
||||
if v, exists := metrics["C"]; exists {
|
||||
record.StatefulSetCPULimit = v
|
||||
}
|
||||
if v, exists := metrics["D"]; exists {
|
||||
record.StatefulSetMemory = v
|
||||
}
|
||||
if v, exists := metrics["E"]; exists {
|
||||
record.StatefulSetMemoryRequest = v
|
||||
}
|
||||
if v, exists := metrics["F"]; exists {
|
||||
record.StatefulSetMemoryLimit = v
|
||||
}
|
||||
if v, exists := metrics["H"]; exists {
|
||||
record.DesiredPods = int(v)
|
||||
}
|
||||
if v, exists := metrics["I"]; exists {
|
||||
record.CurrentPods = int(v)
|
||||
}
|
||||
}
|
||||
|
||||
if phaseCountsForGroup, ok := phaseCounts[compositeKey]; ok {
|
||||
record.PodCountsByPhase = inframonitoringtypes.PodCountsByPhase{
|
||||
Pending: phaseCountsForGroup.Pending,
|
||||
Running: phaseCountsForGroup.Running,
|
||||
Succeeded: phaseCountsForGroup.Succeeded,
|
||||
Failed: phaseCountsForGroup.Failed,
|
||||
Unknown: phaseCountsForGroup.Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func (m *module) getTopStatefulSetGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableStatefulSets,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
topReq := &qbtypes.QueryRangeRequest{
|
||||
Start: uint64(req.Start),
|
||||
End: uint64(req.End),
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: make([]qbtypes.QueryEnvelope, 0, len(queryNamesForOrderBy)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, envelope := range m.newStatefulSetsTableListQuery().CompositeQuery.Queries {
|
||||
if !slices.Contains(queryNamesForOrderBy, envelope.GetQueryName()) {
|
||||
continue
|
||||
}
|
||||
copied := envelope
|
||||
if copied.Type == qbtypes.QueryTypeBuilder {
|
||||
existingExpr := ""
|
||||
if f := copied.GetFilter(); f != nil {
|
||||
existingExpr = f.Expression
|
||||
}
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
merged := mergeFilterExpressions(existingExpr, reqFilterExpr)
|
||||
copied.SetFilter(&qbtypes.Filter{Expression: merged})
|
||||
copied.SetGroupBy(req.GroupBy)
|
||||
}
|
||||
topReq.CompositeQuery.Queries = append(topReq.CompositeQuery.Queries, copied)
|
||||
}
|
||||
|
||||
resp, err := m.querier.QueryRange(ctx, orgID, topReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
func (m *module) getStatefulSetsTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableStatefulSets) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range statefulSetAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
statefulSetNameAttrKey = "k8s.statefulset.name"
|
||||
statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
|
||||
)
|
||||
|
||||
var statefulSetNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: statefulSetNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
// statefulSetsTableMetricNamesList drives the existence/retention check.
|
||||
// Includes k8s.pod.phase even though phase isn't part of the QB composite query —
|
||||
// it is queried separately via getPerGroupPodPhaseCounts, and we want the
|
||||
// response to short-circuit cleanly when the phase metric is absent.
|
||||
var statefulSetsTableMetricNamesList = []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory.working_set",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
}
|
||||
|
||||
// Carried forward from v1 statefulSetAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/statefulsets.go:29-33).
|
||||
var statefulSetAttrKeysForMetadata = []string{
|
||||
"k8s.statefulset.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToStatefulSetsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking statefulset groups. v2 B/C/E/F are direct metrics, no
|
||||
// formula deps — so unlike v1 we don't carry A/D.
|
||||
var orderByToStatefulSetsQueryNames = map[string][]string{
|
||||
inframonitoringtypes.StatefulSetsOrderByCPU: {"A"},
|
||||
inframonitoringtypes.StatefulSetsOrderByCPURequest: {"B"},
|
||||
inframonitoringtypes.StatefulSetsOrderByCPULimit: {"C"},
|
||||
inframonitoringtypes.StatefulSetsOrderByMemory: {"D"},
|
||||
inframonitoringtypes.StatefulSetsOrderByMemoryRequest: {"E"},
|
||||
inframonitoringtypes.StatefulSetsOrderByMemoryLimit: {"F"},
|
||||
inframonitoringtypes.StatefulSetsOrderByDesiredPods: {"H"},
|
||||
inframonitoringtypes.StatefulSetsOrderByCurrentPods: {"I"},
|
||||
}
|
||||
|
||||
// newStatefulSetsTableListQuery builds the composite QB v5 request for the statefulsets list.
|
||||
// Eight builder queries: A..F roll up pod-level metrics by statefulset, H/I take the
|
||||
// latest statefulset-level desired/current counts. Restarts (v1 query G) is intentionally
|
||||
// omitted to match the v2 pods/deployments pattern.
|
||||
//
|
||||
// Every builder query carries a base filter `k8s.statefulset.name != ”`.
|
||||
// Reason: pod-level metrics (A..F) are emitted for every pod regardless of whether the
|
||||
// pod belongs to a StatefulSet; only StatefulSet-owned pods carry the
|
||||
// `k8s.statefulset.name` resource attribute. Without this filter, standalone pods and
|
||||
// pods owned by other workloads (Deployment/DaemonSet/Job/...) collapse into a single
|
||||
// empty-string group under the default groupBy. v1's GetStatefulSetList applied the same
|
||||
// filter via FilterOperatorExists; this matches v1 parity. The base filter merges
|
||||
// cleanly with user filters via mergeFilterExpressions / buildFullQueryRequest.
|
||||
func (m *module) newStatefulSetsTableListQuery() *qbtypes.QueryRangeRequest {
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
// Query A: k8s.pod.cpu.usage — sum of pod CPU within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query B: k8s.pod.cpu_request_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query C: k8s.pod.cpu_limit_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query D: k8s.pod.memory.working_set — sum of pod memory within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory.working_set",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query E: k8s.pod.memory_request_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "E",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query F: k8s.pod.memory_limit_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "F",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query H: k8s.statefulset.desired_pods — latest known desired replica count per group.
|
||||
// v1 used TimeAggregationAnyLast (v3) → mapped to TimeAggregationLatest in v5;
|
||||
// SpaceAggregationSum + ReduceToLast preserve v1's "latest, summed across the group".
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "H",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.statefulset.desired_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query I: k8s.statefulset.current_pods — latest known current replica count per group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "I",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.statefulset.current_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: statefulSetsBaseFilterExpr,
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{statefulSetNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type Handler interface {
|
||||
ListClusters(http.ResponseWriter, *http.Request)
|
||||
ListVolumes(http.ResponseWriter, *http.Request)
|
||||
ListDeployments(http.ResponseWriter, *http.Request)
|
||||
ListStatefulSets(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -26,4 +27,5 @@ type Module interface {
|
||||
ListClusters(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableClusters) (*inframonitoringtypes.Clusters, error)
|
||||
ListVolumes(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableVolumes) (*inframonitoringtypes.Volumes, error)
|
||||
ListDeployments(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (*inframonitoringtypes.Deployments, error)
|
||||
ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m *module) GetSpanPercentile(ctx context.Context, orgID valuer.UUID, req *
|
||||
|
||||
func transformToSpanPercentileResponse(queryResult *qbtypes.QueryRangeResponse) (*spanpercentiletypes.SpanPercentileResponse, error) {
|
||||
if len(queryResult.Data.Results) == 0 {
|
||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "no data returned from query")
|
||||
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria")
|
||||
}
|
||||
|
||||
scalarData, ok := queryResult.Data.Results[0].(*qbtypes.ScalarData)
|
||||
@@ -61,7 +61,7 @@ func transformToSpanPercentileResponse(queryResult *qbtypes.QueryRangeResponse)
|
||||
}
|
||||
|
||||
if len(scalarData.Data) == 0 {
|
||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "no rows returned from query")
|
||||
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria")
|
||||
}
|
||||
|
||||
row := scalarData.Data[0]
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package impltag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store tagtypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store tagtypes.Store) tag.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (m *module) SyncTags(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error) {
|
||||
var tags []*tagtypes.Tag
|
||||
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
resolved, err := m.createMany(ctx, orgID, kind, postable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIDs := make([]valuer.UUID, len(resolved))
|
||||
for i, t := range resolved {
|
||||
tagIDs[i] = t.ID
|
||||
}
|
||||
if err := m.syncLinksForResource(ctx, orgID, kind, resourceID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
tags = resolved
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (m *module) createMany(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error) {
|
||||
if len(postable) == 0 {
|
||||
return []*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
toCreate, matched, err := m.resolve(ctx, orgID, kind, postable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created, err := m.store.CreateOrGet(ctx, toCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(matched, created...), nil
|
||||
}
|
||||
|
||||
func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) error {
|
||||
return m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := m.store.CreateRelations(ctx, tagtypes.NewTagRelations(kind, resourceID, tagIDs)); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.store.DeleteRelationsExcept(ctx, orgID, kind, resourceID, tagIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *module) ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error) {
|
||||
return m.store.ListByResource(ctx, orgID, kind, resourceID)
|
||||
}
|
||||
|
||||
func (m *module) ListForResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error) {
|
||||
return m.store.ListByResources(ctx, orgID, kind, resourceIDs)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package impltag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// resolve canonicalizes a batch of user-supplied (key, value) tag pairs against
|
||||
// the existing tags for an org. Lookup is case-insensitive on both key and
|
||||
// value (matching the storage uniqueness rule); when an existing row matches,
|
||||
// its display casing is reused. Inputs are deduped on (LOWER(key), LOWER(value));
|
||||
// the first input's casing wins on collisions. Returns:
|
||||
// - toCreate: new Tag rows the caller should insert (with pre-generated IDs)
|
||||
// - matched: existing rows the caller's input already pointed to. They
|
||||
// already carry authoritative IDs from the store.
|
||||
func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
if len(postable) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
existing, err := m.store.List(ctx, orgID, kind)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
lowercaseTagsMap := make(map[string]*tagtypes.Tag, len(existing))
|
||||
for _, t := range existing {
|
||||
mapKey := strings.ToLower(t.Key) + "\x00" + strings.ToLower(t.Value)
|
||||
lowercaseTagsMap[mapKey] = t
|
||||
}
|
||||
|
||||
seenInRequestAlready := make(map[string]struct{}, len(postable)) // postable can have the same tag multiple times
|
||||
toCreate := make([]*tagtypes.Tag, 0)
|
||||
matched := make([]*tagtypes.Tag, 0)
|
||||
|
||||
for _, p := range postable {
|
||||
key, value, err := tagtypes.ValidatePostableTag(p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
lookup := strings.ToLower(key) + "\x00" + strings.ToLower(value)
|
||||
if _, dup := seenInRequestAlready[lookup]; dup {
|
||||
continue
|
||||
}
|
||||
seenInRequestAlready[lookup] = struct{}{}
|
||||
|
||||
if existingTag, ok := lowercaseTagsMap[lookup]; ok {
|
||||
matched = append(matched, existingTag)
|
||||
continue
|
||||
}
|
||||
toCreate = append(toCreate, tagtypes.NewTag(orgID, kind, key, value))
|
||||
}
|
||||
|
||||
return toCreate, matched, nil
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package impltag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes/tagtypestest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testKind = coretypes.KindDashboard
|
||||
|
||||
func TestModule_Resolve(t *testing.T) {
|
||||
t.Run("empty input does not hit store", func(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, toCreate)
|
||||
assert.Empty(t, matched)
|
||||
assert.Zero(t, store.ListCallCount, "should not hit store when input is empty")
|
||||
})
|
||||
|
||||
t.Run("creates missing pairs and reuses existing", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dbTag := tagtypes.NewTag(orgID, testKind, "team", "Pulse")
|
||||
dbTag2 := tagtypes.NewTag(orgID, testKind, "Database", "redis")
|
||||
store := tagtypestest.NewStore()
|
||||
store.Tags = []*tagtypes.Tag{dbTag, dbTag2}
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "events"}, // new
|
||||
{Key: "DATABASE", Value: "REDIS"}, // case-only conflict
|
||||
{Key: "Brand", Value: "New"}, // new
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
createdLowerKVs := []string{}
|
||||
for _, tg := range toCreate {
|
||||
createdLowerKVs = append(createdLowerKVs, strings.ToLower(tg.Key)+"\x00"+strings.ToLower(tg.Value))
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"team\x00events", "brand\x00new"}, createdLowerKVs,
|
||||
"only the two missing pairs should be returned for insertion")
|
||||
|
||||
require.Len(t, matched, 1, "DATABASE:REDIS should hit the existing 'Database:redis' tag")
|
||||
assert.Same(t, dbTag2, matched[0], "matched should return the existing pointer with its authoritative ID")
|
||||
})
|
||||
|
||||
t.Run("dedupes inputs that map to the same lower(key)+lower(value)", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "Foo", Value: "Bar"},
|
||||
{Key: "foo", Value: "bar"},
|
||||
{Key: "FOO", Value: "BAR"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, matched)
|
||||
require.Len(t, toCreate, 1, "duplicate inputs must collapse into a single insert")
|
||||
assert.Equal(t, "Foo", toCreate[0].Key, "first input's casing wins")
|
||||
assert.Equal(t, "Bar", toCreate[0].Value, "first input's casing wins")
|
||||
})
|
||||
|
||||
t.Run("preserves existing casing on case-only match", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dbTag := tagtypes.NewTag(orgID, testKind, "Team", "Pulse")
|
||||
store := tagtypestest.NewStore()
|
||||
store.Tags = []*tagtypes.Tag{dbTag}
|
||||
m := &module{store: store}
|
||||
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "PULSE"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, toCreate)
|
||||
require.Len(t, matched, 1)
|
||||
assert.Equal(t, "Team", matched[0].Key)
|
||||
assert.Equal(t, "Pulse", matched[0].Value)
|
||||
})
|
||||
|
||||
t.Run("propagates validation error from any input", func(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
_, _, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "pulse"},
|
||||
{Key: "", Value: "x"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("propagates regex validation error", func(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
_, _, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, []tagtypes.PostableTag{
|
||||
{Key: "team!eng", Value: "pulse"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package impltag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewStore(sqlstore sqlstore.SQLStore) tagtypes.Store {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (s *store) List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*tagtypes.Tag, error) {
|
||||
tags := make([]*tagtypes.Tag, 0)
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&tags).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("kind = ?", kind).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (s *store) ListByResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error) {
|
||||
tags := make([]*tagtypes.Tag, 0)
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&tags).
|
||||
Join("JOIN tag_relation AS tr ON tr.tag_id = tag.id").
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id = ?", resourceID).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (s *store) ListByResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error) {
|
||||
if len(resourceIDs) == 0 {
|
||||
return map[valuer.UUID][]*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
type joinedRow struct {
|
||||
tagtypes.Tag `bun:",extend"`
|
||||
ResourceID valuer.UUID `bun:"resource_id"`
|
||||
}
|
||||
|
||||
rows := make([]*joinedRow, 0)
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&rows).
|
||||
ColumnExpr("tag.*, tr.resource_id").
|
||||
Join("JOIN tag_relation AS tr ON tr.tag_id = tag.id").
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id IN (?)", bun.In(resourceIDs)).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[valuer.UUID][]*tagtypes.Tag)
|
||||
for _, r := range rows {
|
||||
tag := r.Tag
|
||||
out[r.ResourceID] = append(out[r.ResourceID], &tag)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *store) CreateOrGet(ctx context.Context, tags []*tagtypes.Tag) ([]*tagtypes.Tag, error) {
|
||||
if len(tags) == 0 {
|
||||
return tags, nil
|
||||
}
|
||||
// DO UPDATE on a self-set is a deliberate no-op write whose only purpose
|
||||
// is to make RETURNING fire on conflicting rows. Without it, RETURNING is
|
||||
// silent on the conflict path and we'd have to refetch by (key, value) to
|
||||
// learn the existing rows' IDs after a concurrent-insert race. Setting
|
||||
// key = tag.key (the existing row's value) preserves the first writer's
|
||||
// casing on case-only collisions.
|
||||
err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&tags).
|
||||
On("CONFLICT (org_id, kind, (LOWER(key)), (LOWER(value))) DO UPDATE").
|
||||
Set("key = tag.key").
|
||||
Returning("*").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (s *store) CreateRelations(ctx context.Context, relations []*tagtypes.TagRelation) error {
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&relations).
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO NOTHING").
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) DeleteRelationsExcept(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, keepTagIDs []valuer.UUID) error {
|
||||
// Scope the delete to the caller's org via a subquery on tag — bun's
|
||||
// DELETE-with-JOIN syntax isn't uniformly portable across Postgres/SQLite.
|
||||
tagIDsToDelete := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
TableExpr("tag").
|
||||
Column("id").
|
||||
Where("org_id = ?", orgID)
|
||||
if len(keepTagIDs) > 0 {
|
||||
tagIDsToDelete = tagIDsToDelete.Where("id NOT IN (?)", bun.In(keepTagIDs))
|
||||
}
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model((*tagtypes.TagRelation)(nil)).
|
||||
Where("kind = ?", kind).
|
||||
Where("resource_id = ?", resourceID).
|
||||
Where("tag_id IN (?)", tagIDsToDelete).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
|
||||
return s.sqlstore.RunInTxCtx(ctx, nil, cb)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package impltag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func newTestStore(t *testing.T) sqlstore.SQLStore {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
store, err := sqlitesqlstore.New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
Connection: sqlstore.ConnectionConfig{
|
||||
MaxOpenConns: 1,
|
||||
MaxConnLifetime: 0,
|
||||
},
|
||||
Sqlite: sqlstore.SqliteConfig{
|
||||
Path: dbPath,
|
||||
Mode: "wal",
|
||||
BusyTimeout: 5 * time.Second,
|
||||
TransactionMode: "deferred",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.BunDB().NewCreateTable().
|
||||
Model((*tagtypes.Tag)(nil)).
|
||||
IfNotExists().
|
||||
Exec(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.BunDB().Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_tag_org_kind_lower_key_lower_value ON tag (org_id, kind, LOWER(key), LOWER(value))`)
|
||||
require.NoError(t, err)
|
||||
return store
|
||||
}
|
||||
|
||||
var dashboardKind = coretypes.KindDashboard
|
||||
|
||||
func tagsByLowerKeyValue(t *testing.T, db *bun.DB) map[string]*tagtypes.Tag {
|
||||
t.Helper()
|
||||
all := make([]*tagtypes.Tag, 0)
|
||||
require.NoError(t, db.NewSelect().Model(&all).Scan(context.Background()))
|
||||
out := map[string]*tagtypes.Tag{}
|
||||
for _, tag := range all {
|
||||
out[strings.ToLower(tag.Key)+"\x00"+strings.ToLower(tag.Value)] = tag
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestStore_Create_PopulatesIDsOnFreshInsert(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstore := newTestStore(t)
|
||||
s := NewStore(sqlstore)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
tagA := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
tagB := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
|
||||
preIDA := tagA.ID
|
||||
preIDB := tagB.ID
|
||||
|
||||
got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{tagA, tagB})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
// No race → pre-generated IDs stand. The slice is what we passed in,
|
||||
// confirming Scan didn't reallocate.
|
||||
assert.Equal(t, preIDA, got[0].ID)
|
||||
assert.Equal(t, preIDB, got[1].ID)
|
||||
|
||||
// And the rows are in the DB.
|
||||
stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
|
||||
require.Contains(t, stored, "tag\x00database")
|
||||
require.Contains(t, stored, "team\x00blr")
|
||||
assert.Equal(t, preIDA, stored["tag\x00database"].ID)
|
||||
assert.Equal(t, preIDB, stored["team\x00blr"].ID)
|
||||
}
|
||||
|
||||
func TestStore_Create_ConflictReturnsExistingRowID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstore := newTestStore(t)
|
||||
s := NewStore(sqlstore)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
// Simulate a concurrent insert: someone else has already inserted "tag:Database".
|
||||
winner := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
_, err := s.CreateOrGet(ctx, []*tagtypes.Tag{winner})
|
||||
require.NoError(t, err)
|
||||
winnerID := winner.ID
|
||||
|
||||
// Now our request runs with a different pre-generated ID for the same
|
||||
// (key, value) — case differs but the functional unique index collapses
|
||||
// them. RETURNING should overwrite our stale ID with winner's ID.
|
||||
loser := tagtypes.NewTag(orgID, dashboardKind, "TAG", "DATABASE")
|
||||
loserPreID := loser.ID
|
||||
require.NotEqual(t, winnerID, loserPreID, "pre-generated IDs must differ for this test to be meaningful")
|
||||
|
||||
got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{loser})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
|
||||
assert.Equal(t, winnerID, got[0].ID, "returned slice should carry the existing row's ID, not our stale one")
|
||||
assert.Equal(t, winnerID, loser.ID, "input slice element is mutated in place")
|
||||
|
||||
// And the DB still has exactly one row for that (lower(key), lower(value)) — winner's, with winner's casing.
|
||||
stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
|
||||
require.Len(t, stored, 1)
|
||||
assert.Equal(t, winnerID, stored["tag\x00database"].ID)
|
||||
assert.Equal(t, "tag", stored["tag\x00database"].Key, "winner's casing preserved in key")
|
||||
assert.Equal(t, "Database", stored["tag\x00database"].Value, "winner's casing preserved in value")
|
||||
}
|
||||
|
||||
func TestStore_Create_MixedFreshAndConflict(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstore := newTestStore(t)
|
||||
s := NewStore(sqlstore)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
pre := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
_, err := s.CreateOrGet(ctx, []*tagtypes.Tag{pre})
|
||||
require.NoError(t, err)
|
||||
preExistingID := pre.ID
|
||||
|
||||
conflict := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
fresh := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
|
||||
freshPreID := fresh.ID
|
||||
|
||||
got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{conflict, fresh})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
|
||||
assert.Equal(t, preExistingID, got[0].ID, "conflicting row's ID overwritten with the existing row's")
|
||||
assert.Equal(t, freshPreID, got[1].ID, "fresh row's pre-generated ID is preserved")
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
// SyncTags resolves the given postable tags (creating new rows as needed)
|
||||
// and reconciles the resource's links to exactly that set, all in one transaction.
|
||||
SyncTags(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error)
|
||||
|
||||
ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error)
|
||||
|
||||
// Resources with no tags are absent from the returned map.
|
||||
ListForResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error)
|
||||
}
|
||||
@@ -134,7 +134,7 @@ type RuleStateHistory struct {
|
||||
// One of ["normal", "firing"]
|
||||
OverallState AlertState `json:"overallState" ch:"overall_state"`
|
||||
OverallStateChanged bool `json:"overallStateChanged" ch:"overall_state_changed"`
|
||||
// One of ["normal", "firing", "no_data", "muted"]
|
||||
// One of ["normal", "firing", "nodata", "muted"]
|
||||
State AlertState `json:"state" ch:"state"`
|
||||
StateChanged bool `json:"stateChanged" ch:"state_changed"`
|
||||
UnixMilli int64 `json:"unixMilli" ch:"unix_milli"`
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention/implretention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
@@ -46,8 +45,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
emailing := emailingtest.New()
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
require.NoError(t, err)
|
||||
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), sqlstore, providerSettings, nil, orgGetter, queryParser, tagModule)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
||||
|
||||
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
|
||||
require.NoError(t, err)
|
||||
@@ -55,9 +53,8 @@ func TestNewHandlers(t *testing.T) {
|
||||
userRoleStore := impluser.NewUserRoleStore(sqlstore, providerSettings)
|
||||
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger)
|
||||
|
||||
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, retentionGetter, flagger)
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
|
||||
@@ -45,7 +45,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
@@ -89,7 +88,6 @@ type Modules struct {
|
||||
TraceDetail tracedetail.Module
|
||||
SpanMapper spanmapper.Module
|
||||
LLMPricingRule llmpricingrule.Module
|
||||
Tag tag.Module
|
||||
}
|
||||
|
||||
func NewModules(
|
||||
@@ -115,7 +113,6 @@ func NewModules(
|
||||
cloudIntegrationModule cloudintegration.Module,
|
||||
retentionGetter retention.Getter,
|
||||
fl flagger.Flagger,
|
||||
tagModule tag.Module,
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
@@ -148,6 +145,5 @@ func NewModules(
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore)),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention/implretention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
@@ -47,8 +46,7 @@ func TestNewModules(t *testing.T) {
|
||||
emailing := emailingtest.New()
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
require.NoError(t, err)
|
||||
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), sqlstore, providerSettings, nil, orgGetter, queryParser, tagModule)
|
||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
||||
|
||||
flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry())
|
||||
require.NoError(t, err)
|
||||
@@ -60,8 +58,7 @@ func TestNewModules(t *testing.T) {
|
||||
serviceAccount := implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), nil, nil, nil, providerSettings, serviceaccount.Config{})
|
||||
|
||||
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
|
||||
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, implcloudintegration.NewModule(), retentionGetter, flagger)
|
||||
|
||||
reflectVal := reflect.ValueOf(modules)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -200,7 +200,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
|
||||
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -107,7 +105,7 @@ func New(
|
||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
|
||||
authzCallback func(context.Context, sqlstore.SQLStore, authz.Config, licensing.Licensing, []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing, tag.Module) dashboard.Module,
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
|
||||
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
meterReporterProviderFactories func(context.Context, factory.ProviderSettings, flagger.Flagger, licensing.Licensing, telemetrystore.TelemetryStore, retention.Getter, organization.Getter, zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string),
|
||||
@@ -334,13 +332,8 @@ func New(
|
||||
// Initialize query parser (needed for dashboard module)
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
|
||||
// Initialize tag module — shared across modules that link entities to tags
|
||||
// (currently dashboard; future: alerts, RBAC). Built once here and injected
|
||||
// where needed.
|
||||
tagModule := impltag.NewModule(impltag.NewStore(sqlstore))
|
||||
|
||||
// Initialize dashboard module (needed for authz registry)
|
||||
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
|
||||
// Initialize dashboard module
|
||||
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
|
||||
// Initialize user getter
|
||||
userGetter := impluser.NewGetter(userStore, userRoleStore, flagger)
|
||||
@@ -462,7 +455,7 @@ func New(
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger, tagModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, cloudIntegrationModule, retentionGetter, flagger)
|
||||
|
||||
// Initialize ruler from the variant-specific provider factories
|
||||
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addTags struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddTagsFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_tags"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTags{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTags) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTags) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
sqls := [][]byte{}
|
||||
|
||||
tagTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "tag",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "key", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "value", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "kind", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
sqls = append(sqls, tagTableSQLs...)
|
||||
|
||||
// Case-insensitive uniqueness on (org_id, kind, key, value) — both Postgres
|
||||
// and SQLite (modernc 3.50.x) support expression indexes.
|
||||
tagUniqueIndexSQLs := migration.sqlschema.Operator().CreateIndex(
|
||||
(&sqlschema.UniqueIndex{
|
||||
TableName: "tag",
|
||||
ColumnNames: []sqlschema.ColumnName{"org_id", "kind", "key", "value"},
|
||||
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
|
||||
}).Named("uq_tag_org_kind_lower_key_lower_value"),
|
||||
)
|
||||
sqls = append(sqls, tagUniqueIndexSQLs...)
|
||||
|
||||
tagRelationsTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "tag_relation",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "kind", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "resource_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "tag_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("tag_id"),
|
||||
ReferencedTableName: sqlschema.TableName("tag"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
sqls = append(sqls, tagRelationsTableSQLs...)
|
||||
|
||||
tagRelationUniqueIndexSQLs := migration.sqlschema.Operator().CreateIndex(
|
||||
&sqlschema.UniqueIndex{
|
||||
TableName: "tag_relation",
|
||||
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
|
||||
},
|
||||
)
|
||||
sqls = append(sqls, tagRelationUniqueIndexSQLs...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTags) Down(_ context.Context, _ *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package sqlschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -51,23 +49,9 @@ type Index interface {
|
||||
ToDropSQL(fmter SQLFormatter) []byte
|
||||
}
|
||||
|
||||
// UniqueIndex models a unique index on a table.
|
||||
//
|
||||
// In the common case the index keys on plain columns: set only ColumnNames and
|
||||
// the SQL is emitted with each column identifier-quoted by the formatter
|
||||
// (`CREATE UNIQUE INDEX uq_t_a_b ON t (a, b)`).
|
||||
//
|
||||
// For functional indexes (e.g. case-insensitive uniqueness on `LOWER(col)`),
|
||||
// set Expressions to the raw SQL parts and use ColumnNames as metadata for
|
||||
// "which columns does this index touch". When Expressions is non-empty, it
|
||||
// overrides ColumnNames for SQL emission — each entry is written verbatim, so
|
||||
// the caller owns well-formedness — and the auto-generated name uses a hash
|
||||
// suffix instead of a readable column join because expressions aren't valid
|
||||
// identifier fragments.
|
||||
type UniqueIndex struct {
|
||||
TableName TableName
|
||||
ColumnNames []ColumnName
|
||||
Expressions []string
|
||||
name string
|
||||
}
|
||||
|
||||
@@ -87,28 +71,16 @@ func (index *UniqueIndex) Name() string {
|
||||
}
|
||||
b.WriteString(string(column))
|
||||
}
|
||||
|
||||
if len(index.Expressions) > 0 {
|
||||
if len(index.ColumnNames) > 0 {
|
||||
b.WriteString("_")
|
||||
}
|
||||
hasher := fnv.New32a()
|
||||
_, _ = hasher.Write([]byte(strings.Join(index.Expressions, "\x00")))
|
||||
fmt.Fprintf(&b, "%08x", hasher.Sum32())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (index *UniqueIndex) Named(name string) Index {
|
||||
copyOfColumnNames := make([]ColumnName, len(index.ColumnNames))
|
||||
copy(copyOfColumnNames, index.ColumnNames)
|
||||
copyOfExpressions := make([]string, len(index.Expressions))
|
||||
copy(copyOfExpressions, index.Expressions)
|
||||
|
||||
return &UniqueIndex{
|
||||
TableName: index.TableName,
|
||||
ColumnNames: copyOfColumnNames,
|
||||
Expressions: copyOfExpressions,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
@@ -129,18 +101,7 @@ func (index *UniqueIndex) Equals(other Index) bool {
|
||||
if other.Type() != IndexTypeUnique {
|
||||
return false
|
||||
}
|
||||
otherUnique, ok := other.(*UniqueIndex)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Plain and functional indexes produce different SQL even if their column
|
||||
// sets overlap; require both shapes to match.
|
||||
if (len(index.Expressions) == 0) != (len(otherUnique.Expressions) == 0) {
|
||||
return false
|
||||
}
|
||||
if len(index.Expressions) > 0 && !slices.Equal(index.Expressions, otherUnique.Expressions) {
|
||||
return false
|
||||
}
|
||||
|
||||
return index.Name() == other.Name() && slices.Equal(index.Columns(), other.Columns())
|
||||
}
|
||||
|
||||
@@ -153,20 +114,12 @@ func (index *UniqueIndex) ToCreateSQL(fmter SQLFormatter) []byte {
|
||||
sql = fmter.AppendIdent(sql, string(index.TableName))
|
||||
sql = append(sql, " ("...)
|
||||
|
||||
if len(index.Expressions) > 0 {
|
||||
for i, expr := range index.Expressions {
|
||||
if i > 0 {
|
||||
sql = append(sql, ", "...)
|
||||
}
|
||||
sql = append(sql, expr...)
|
||||
}
|
||||
} else {
|
||||
for i, column := range index.ColumnNames {
|
||||
if i > 0 {
|
||||
sql = append(sql, ", "...)
|
||||
}
|
||||
sql = fmter.AppendIdent(sql, string(column))
|
||||
for i, column := range index.ColumnNames {
|
||||
if i > 0 {
|
||||
sql = append(sql, ", "...)
|
||||
}
|
||||
|
||||
sql = fmter.AppendIdent(sql, string(column))
|
||||
}
|
||||
|
||||
sql = append(sql, ")"...)
|
||||
|
||||
@@ -38,43 +38,6 @@ func TestIndexToCreateSQL(t *testing.T) {
|
||||
},
|
||||
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_index" ON "users" ("id", "name", "email")`,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_SingleExpression",
|
||||
index: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
},
|
||||
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_email_1e5a87f1" ON "users" (LOWER(email))`,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_MixedColumnsAndExpressions",
|
||||
index: &UniqueIndex{
|
||||
TableName: "tag",
|
||||
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
|
||||
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
|
||||
},
|
||||
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_tag_org_id_kind_key_value_57e8f81f" ON "tag" (org_id, kind, LOWER(key), LOWER(value))`,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_ComplexExpression",
|
||||
index: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"first_name", "last_name"},
|
||||
Expressions: []string{"LOWER(TRIM(first_name) || ' ' || TRIM(last_name))"},
|
||||
},
|
||||
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_first_name_last_name_adb1ff53" ON "users" (LOWER(TRIM(first_name) || ' ' || TRIM(last_name)))`,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_Named",
|
||||
index: &UniqueIndex{
|
||||
TableName: "tag",
|
||||
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
|
||||
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
|
||||
name: "uq_tag_org_kind_lower_key_lower_value",
|
||||
},
|
||||
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_tag_org_kind_lower_key_lower_value" ON "tag" (org_id, kind, LOWER(key), LOWER(value))`,
|
||||
},
|
||||
{
|
||||
name: "PartialUnique_1Column",
|
||||
index: &PartialUniqueIndex{
|
||||
@@ -266,47 +229,6 @@ func TestIndexEquals(t *testing.T) {
|
||||
},
|
||||
equals: false,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_Same",
|
||||
a: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
},
|
||||
b: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
},
|
||||
equals: true,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_DifferentExpressions",
|
||||
a: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
},
|
||||
b: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"UPPER(email)"},
|
||||
},
|
||||
equals: false,
|
||||
},
|
||||
{
|
||||
name: "Unique_Functional_NotEqualToPlainSameColumns",
|
||||
a: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
},
|
||||
b: &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
},
|
||||
equals: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -316,75 +238,6 @@ func TestIndexEquals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueIndexFunctionalName(t *testing.T) {
|
||||
t.Run("autogen uses uq_<table>_<hash>", func(t *testing.T) {
|
||||
idx := &UniqueIndex{
|
||||
TableName: "tag",
|
||||
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
|
||||
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
|
||||
}
|
||||
assert.Equal(t, "uq_tag_org_id_kind_key_value_57e8f81f", idx.Name())
|
||||
})
|
||||
|
||||
t.Run("same expressions produce the same name", func(t *testing.T) {
|
||||
a := &UniqueIndex{
|
||||
TableName: "users",
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
}
|
||||
b := &UniqueIndex{
|
||||
TableName: "users",
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
}
|
||||
assert.Equal(t, a.Name(), b.Name())
|
||||
})
|
||||
|
||||
t.Run("different expressions produce different names", func(t *testing.T) {
|
||||
a := &UniqueIndex{
|
||||
TableName: "users",
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
}
|
||||
b := &UniqueIndex{
|
||||
TableName: "users",
|
||||
Expressions: []string{"UPPER(email)"},
|
||||
}
|
||||
assert.NotEqual(t, a.Name(), b.Name())
|
||||
})
|
||||
|
||||
t.Run("expressions in different order produce different names", func(t *testing.T) {
|
||||
a := &UniqueIndex{
|
||||
TableName: "tag",
|
||||
Expressions: []string{"org_id", "LOWER(key)"},
|
||||
}
|
||||
b := &UniqueIndex{
|
||||
TableName: "tag",
|
||||
Expressions: []string{"LOWER(key)", "org_id"},
|
||||
}
|
||||
assert.NotEqual(t, a.Name(), b.Name())
|
||||
})
|
||||
|
||||
t.Run("functional autogen differs from plain autogen for same columns", func(t *testing.T) {
|
||||
plain := &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
}
|
||||
functional := &UniqueIndex{
|
||||
TableName: "users",
|
||||
ColumnNames: []ColumnName{"email"},
|
||||
Expressions: []string{"LOWER(email)"},
|
||||
}
|
||||
assert.Equal(t, "uq_users_email", plain.Name())
|
||||
assert.NotEqual(t, plain.Name(), functional.Name())
|
||||
})
|
||||
|
||||
t.Run("Named() override wins over hash", func(t *testing.T) {
|
||||
idx := (&UniqueIndex{
|
||||
TableName: "tag",
|
||||
Expressions: []string{"org_id", "LOWER(key)"},
|
||||
}).Named("my_functional_index")
|
||||
assert.Equal(t, "my_functional_index", idx.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPartialUniqueIndexName(t *testing.T) {
|
||||
a := &PartialUniqueIndex{
|
||||
TableName: "users",
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
)
|
||||
|
||||
const (
|
||||
SchemaVersion = "v6"
|
||||
MaxTagsPerDashboard = 5
|
||||
)
|
||||
|
||||
type DSLKey string
|
||||
|
||||
const (
|
||||
DSLKeyName DSLKey = "name"
|
||||
DSLKeyDescription DSLKey = "description"
|
||||
DSLKeyCreatedAt DSLKey = "created_at"
|
||||
DSLKeyUpdatedAt DSLKey = "updated_at"
|
||||
DSLKeyCreatedBy DSLKey = "created_by"
|
||||
DSLKeyLocked DSLKey = "locked"
|
||||
DSLKeyPublic DSLKey = "public"
|
||||
)
|
||||
|
||||
// reservedDSLKeys are dashboard column-level filter names in the list-query DSL.
|
||||
// A tag whose key collides with one of these would make the DSL ambiguous, so
|
||||
// they're rejected (case-insensitively) at write time.
|
||||
var reservedDSLKeys = map[DSLKey]struct{}{
|
||||
DSLKeyName: {},
|
||||
DSLKeyDescription: {},
|
||||
DSLKeyCreatedAt: {},
|
||||
DSLKeyUpdatedAt: {},
|
||||
DSLKeyCreatedBy: {},
|
||||
DSLKeyLocked: {},
|
||||
DSLKeyPublic: {},
|
||||
}
|
||||
|
||||
type DashboardV2 struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
Locked bool `json:"locked"`
|
||||
Info DashboardInfo `json:"info"`
|
||||
PublicConfig *PublicDashboard `json:"publicConfig,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardInfo is the serializable view of a dashboard's contents — what the UI renders as "the dashboard JSON".
|
||||
type DashboardInfo struct {
|
||||
StoredDashboardInfo
|
||||
Tags []*tagtypes.Tag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// StoredDashboardInfo is exactly what serializes into the dashboard.data column.
|
||||
type StoredDashboardInfo struct {
|
||||
Metadata DashboardMetadata `json:"metadata"`
|
||||
Data DashboardData `json:"data"`
|
||||
}
|
||||
|
||||
type DashboardMetadata struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Image string `json:"image,omitempty"`
|
||||
UploadedGrafana bool `json:"uploadedGrafana"`
|
||||
}
|
||||
|
||||
type PostableDashboardV2 struct {
|
||||
StoredDashboardInfo
|
||||
Tags []tagtypes.PostableTag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateableDashboardV2 = PostableDashboardV2
|
||||
|
||||
// PatchableDashboardV2 is an RFC 6902 JSON Patch document applied against a
|
||||
// PostableDashboardV2-shaped view of an existing dashboard. Patch ops can
|
||||
// target any field — including individual entries inside `data.panels`,
|
||||
// `data.panels.<id>.spec.queries`, or `tags` — without re-sending the rest of
|
||||
// the dashboard.
|
||||
type PatchableDashboardV2 struct {
|
||||
patch jsonpatch.Patch
|
||||
}
|
||||
|
||||
// JSONPatchDocument is the OpenAPI-facing schema for an RFC 6902 patch body.
|
||||
// PatchableDashboardV2 has only an internal `jsonpatch.Patch` field, so the
|
||||
// reflector would emit an empty schema; the handler def points at this type
|
||||
// instead so consumers see the array-of-ops shape.
|
||||
type JSONPatchDocument []JSONPatchOperation
|
||||
|
||||
// JSONPatchOperation is one RFC 6902 op. Not every field is valid on every
|
||||
// op kind (e.g. `value` is required for add/replace/test, ignored for remove;
|
||||
// `from` is required for move/copy) — the JSON Patch RFC governs that.
|
||||
type JSONPatchOperation struct {
|
||||
Op string `json:"op" required:"true"`
|
||||
Path string `json:"path" required:"true" description:"JSON Pointer (RFC 6901) into the dashboard's postable shape — e.g. /data/display/name, /data/panels/<id>, /data/panels/<id>/spec/queries/0, /tags/-."`
|
||||
Value any `json:"value,omitempty" description:"Value to add/replace/test against. The expected type depends on the path. Common shapes (see referenced schemas for the exact field set): /data/panels/<id> takes a DashboardtypesPanel; /data/panels/<id>/spec/queries/N (or /-) takes a DashboardtypesQuery; /data/variables/N takes a DashboardtypesVariable; /data/layouts/N takes a DashboardtypesLayout; /tags/N (or /-) takes a TagtypesPostableTag; /data/display/name and other leaf string fields take a string. Required for add/replace/test; ignored for remove/move/copy."`
|
||||
From string `json:"from,omitempty" description:"Source JSON Pointer for move/copy ops; ignored for other ops."`
|
||||
}
|
||||
|
||||
// PrepareJSONSchema constrains the `op` field to the six RFC 6902 verbs.
|
||||
func (JSONPatchOperation) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
op, ok := s.Properties["op"]
|
||||
if !ok || op.TypeObject == nil {
|
||||
return errors.NewInternalf(errors.CodeInternal, "JSONPatchOperation schema missing `op` property")
|
||||
}
|
||||
op.TypeObject.WithEnum("add", "remove", "replace", "move", "copy", "test")
|
||||
s.Properties["op"] = op
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchableDashboardV2) UnmarshalJSON(data []byte) error {
|
||||
patch, err := jsonpatch.DecodePatch(data)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
|
||||
}
|
||||
p.patch = patch
|
||||
return nil
|
||||
}
|
||||
|
||||
// patchableDashboardV2View is the JSON shape a patch is applied against.
|
||||
// It mirrors PostableDashboardV2 except `tags` is always emitted (even when
|
||||
// empty) — RFC 6902 `add /tags/-` requires the array to exist in the target
|
||||
// document, and PostableDashboardV2's own `omitempty` on tags would drop it.
|
||||
type patchableDashboardV2View struct {
|
||||
StoredDashboardInfo
|
||||
Tags []tagtypes.PostableTag `json:"tags"`
|
||||
}
|
||||
|
||||
// Apply runs the patch against the existing dashboard. The dashboard is
|
||||
// projected into the postable JSON shape, the patch is applied, and the
|
||||
// result is decoded back into an UpdateableDashboardV2 — which re-runs
|
||||
// the full v2 validation chain.
|
||||
func (p PatchableDashboardV2) Apply(existing *DashboardV2) (*UpdateableDashboardV2, error) {
|
||||
base := patchableDashboardV2View{
|
||||
StoredDashboardInfo: existing.Info.StoredDashboardInfo,
|
||||
Tags: tagtypes.NewPostableTagsFromTags(existing.Info.Tags),
|
||||
}
|
||||
raw, err := json.Marshal(base)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal existing dashboard for patch")
|
||||
}
|
||||
patched, err := p.patch.Apply(raw)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
|
||||
}
|
||||
out := &UpdateableDashboardV2{}
|
||||
if err := json.Unmarshal(patched, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *PostableDashboardV2) UnmarshalJSON(data []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
type alias PostableDashboardV2
|
||||
var tmp alias
|
||||
if err := dec.Decode(&tmp); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s", err.Error())
|
||||
}
|
||||
*p = PostableDashboardV2(tmp)
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableDashboardV2) Validate() error {
|
||||
if p.Metadata.SchemaVersion != SchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "metadata.schemaVersion must be %q, got %q", SchemaVersion, p.Metadata.SchemaVersion)
|
||||
}
|
||||
if p.Data.Display == nil || p.Data.Display.Name == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "data.display.name is required")
|
||||
}
|
||||
if err := p.validateTags(); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Data.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableDashboardV2) validateTags() error {
|
||||
if len(p.Tags) > MaxTagsPerDashboard {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "a dashboard can have at most %d tags", MaxTagsPerDashboard)
|
||||
}
|
||||
for _, tag := range p.Tags {
|
||||
if _, reserved := reservedDSLKeys[DSLKey(strings.ToLower(strings.TrimSpace(tag.Key)))]; reserved {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "tag key %q is reserved", tag.Key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GettableDashboardV2 struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
Locked bool `json:"locked"`
|
||||
Info GettableDashboardInfo `json:"info"`
|
||||
PublicConfig *GettablePublicDasbhboard `json:"publicConfig,omitempty"`
|
||||
}
|
||||
|
||||
type GettableDashboardInfo struct {
|
||||
StoredDashboardInfo
|
||||
Tags []*tagtypes.GettableTag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func NewGettableDashboardV2FromDashboardV2(dashboard *DashboardV2) *GettableDashboardV2 {
|
||||
gettable := &GettableDashboardV2{
|
||||
Identifiable: dashboard.Identifiable,
|
||||
TimeAuditable: dashboard.TimeAuditable,
|
||||
UserAuditable: dashboard.UserAuditable,
|
||||
OrgID: dashboard.OrgID,
|
||||
Locked: dashboard.Locked,
|
||||
Info: GettableDashboardInfo{
|
||||
StoredDashboardInfo: dashboard.Info.StoredDashboardInfo,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(dashboard.Info.Tags),
|
||||
},
|
||||
}
|
||||
if dashboard.PublicConfig != nil {
|
||||
gettable.PublicConfig = NewGettablePublicDashboard(dashboard.PublicConfig)
|
||||
}
|
||||
return gettable
|
||||
}
|
||||
|
||||
func NewDashboardV2(orgID valuer.UUID, createdBy string, postable PostableDashboardV2, resolvedTags []*tagtypes.Tag) *DashboardV2 {
|
||||
now := time.Now()
|
||||
|
||||
return &DashboardV2{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: createdBy, UpdatedBy: createdBy},
|
||||
OrgID: orgID,
|
||||
Locked: false,
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: StoredDashboardInfo{
|
||||
Metadata: postable.Metadata,
|
||||
Data: postable.Data,
|
||||
},
|
||||
Tags: resolvedTags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rejects rows that don't carry a v2-shape blob — those are pre-migration v1 dashboards that the v2 API can't render.
|
||||
func NewDashboardV2FromStorable(storable *StorableDashboard, public *StorablePublicDashboard, tags []*tagtypes.Tag) (*DashboardV2, error) {
|
||||
metadata, _ := storable.Data["metadata"].(map[string]any)
|
||||
if metadata == nil || metadata["schemaVersion"] != SchemaVersion {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, ErrCodeDashboardInvalidData, "dashboard %s is not in %s schema", storable.ID, SchemaVersion)
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(storable.Data)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal stored v2 dashboard data")
|
||||
}
|
||||
var stored StoredDashboardInfo
|
||||
if err := json.Unmarshal(raw, &stored); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "unmarshal stored v2 dashboard data")
|
||||
}
|
||||
|
||||
var publicConfig *PublicDashboard
|
||||
if public != nil {
|
||||
publicConfig = NewPublicDashboardFromStorablePublicDashboard(public)
|
||||
}
|
||||
|
||||
return &DashboardV2{
|
||||
Identifiable: storable.Identifiable,
|
||||
TimeAuditable: storable.TimeAuditable,
|
||||
UserAuditable: storable.UserAuditable,
|
||||
OrgID: storable.OrgID,
|
||||
Locked: storable.Locked,
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: stored,
|
||||
Tags: tags,
|
||||
},
|
||||
PublicConfig: publicConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) CanLockUnlock(lock bool, isAdmin bool, updatedBy string) error {
|
||||
if d.CreatedBy != updatedBy && !isAdmin {
|
||||
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard")
|
||||
}
|
||||
if d.Locked == lock {
|
||||
if lock {
|
||||
return errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "dashboard is already locked")
|
||||
}
|
||||
return errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "dashboard is already unlocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) LockUnlock(lock bool, isAdmin bool, updatedBy string) error {
|
||||
if err := d.CanLockUnlock(lock, isAdmin, updatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Locked = lock
|
||||
d.UpdatedBy = updatedBy
|
||||
d.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) CanUpdate() error {
|
||||
if d.Locked {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot update a locked dashboard, please unlock the dashboard to update")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardV2) Update(updateable UpdateableDashboardV2, updatedBy string, resolvedTags []*tagtypes.Tag) error {
|
||||
if err := d.CanUpdate(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Info.Metadata = updateable.Metadata
|
||||
d.Info.Data = updateable.Data
|
||||
d.Info.Tags = resolvedTags
|
||||
d.UpdatedBy = updatedBy
|
||||
d.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToStorableDashboard packages a Dashboard into the bun row that goes into
|
||||
// the dashboard table. Tags are intentionally omitted — they live in
|
||||
// tag_relations and are inserted separately by the caller.
|
||||
func (d *DashboardV2) ToStorableDashboard() (*StorableDashboard, error) {
|
||||
data, err := d.Info.toStorableDashboardData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: d.ID},
|
||||
TimeAuditable: d.TimeAuditable,
|
||||
UserAuditable: d.UserAuditable,
|
||||
OrgID: d.OrgID,
|
||||
Locked: d.Locked,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s StoredDashboardInfo) toStorableDashboardData() (StorableDashboardData, error) {
|
||||
raw, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal v2 dashboard data")
|
||||
}
|
||||
out := StorableDashboardData{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "unmarshal v2 dashboard data")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -1,552 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// basePostableJSON is the postable shape of a small but realistic v2
|
||||
// dashboard used as the base document for patch tests. Each panel carries
|
||||
// one builder query in the same shape production dashboards use
|
||||
// (aggregations, filter, groupBy populated), and the dashboard has one
|
||||
// variable — the variable is not patched in any test here, that's
|
||||
// covered in a separate variable-focused suite.
|
||||
const basePostableJSON = `{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {
|
||||
"display": {"name": "Service overview"},
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"name": "service",
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": false,
|
||||
"plugin": {
|
||||
"kind": "signoz/DynamicVariable",
|
||||
"spec": {"name": "service.name", "signal": "metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}],
|
||||
"filter": {"expression": "service.name IN $service"},
|
||||
"groupBy": [{"name": "service.name", "fieldDataType": "string", "fieldContext": "tag"}]
|
||||
}}}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"p2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/NumberPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "X",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_latency_count",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {"title": "Row 1"},
|
||||
"items": [
|
||||
{"x": 0, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p1"}},
|
||||
{"x": 6, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p2"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"duration": "1h"
|
||||
},
|
||||
"tags": [{"key": "team", "value": "alpha"}, {"key": "env", "value": "prod"}]
|
||||
}`
|
||||
|
||||
func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
// Apply doesn't mutate the input *DashboardV2 — it marshals it to
|
||||
// JSON, applies the patch, and unmarshals the result into a fresh
|
||||
// struct. Sharing one base across subtests is safe.
|
||||
var p PostableDashboardV2
|
||||
require.NoError(t, json.Unmarshal([]byte(basePostableJSON), &p), "base postable JSON must validate")
|
||||
base := &DashboardV2{
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: p.StoredDashboardInfo,
|
||||
Tags: []*tagtypes.Tag{
|
||||
{Key: "team", Value: "alpha"},
|
||||
{Key: "env", Value: "prod"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
decode := func(t *testing.T, body string) PatchableDashboardV2 {
|
||||
t.Helper()
|
||||
var patch PatchableDashboardV2
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &patch))
|
||||
return patch
|
||||
}
|
||||
|
||||
// jsonOf marshals the patched dashboard back to JSON so subtests can
|
||||
// assert on field values without reaching into the typed plugin specs.
|
||||
jsonOf := func(t *testing.T, out *UpdateableDashboardV2) string {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(out)
|
||||
require.NoError(t, err)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Successful patches
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("no-op preserves all fields", func(t *testing.T) {
|
||||
out, err := decode(t, `[]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, base.Info.Metadata, out.Metadata)
|
||||
assert.Equal(t, base.Info.Data.Display.Name, out.Data.Display.Name)
|
||||
require.Equal(t, len(base.Info.Data.Panels), len(out.Data.Panels))
|
||||
for k, panel := range base.Info.Data.Panels {
|
||||
require.Contains(t, out.Data.Panels, k)
|
||||
assert.Equal(t, panel.Spec.Plugin.Kind, out.Data.Panels[k].Spec.Plugin.Kind)
|
||||
}
|
||||
assert.Len(t, out.Tags, len(base.Info.Tags))
|
||||
assert.Len(t, out.Data.Variables, len(base.Info.Data.Variables))
|
||||
assert.Len(t, out.Data.Layouts, len(base.Info.Data.Layouts))
|
||||
})
|
||||
|
||||
t.Run("add metadata image", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/metadata/image", "value": "https://example.com/img.png"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://example.com/img.png", out.Metadata.Image)
|
||||
assert.Equal(t, SchemaVersion, out.Metadata.SchemaVersion, "schemaVersion preserved")
|
||||
})
|
||||
|
||||
t.Run("replace display name", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/display/name", "value": "Renamed"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Renamed", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
// Per RFC 6902 § 4.1, `add` on an existing object member replaces the
|
||||
// existing value rather than erroring — same effect as `replace`.
|
||||
t.Run("add overwrites existing display name", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/data/display/name", "value": "Overwritten"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Overwritten", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
t.Run("add data refreshInterval", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/data/refreshInterval", "value": "30s"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "30s", string(out.Data.RefreshInterval))
|
||||
})
|
||||
|
||||
t.Run("add panel leaves others untouched", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "add",
|
||||
"path": "/data/panels/p3",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": [{"expression": "count()"}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 3)
|
||||
assert.Contains(t, out.Data.Panels, "p1")
|
||||
assert.Contains(t, out.Data.Panels, "p2")
|
||||
assert.Contains(t, out.Data.Panels, "p3")
|
||||
})
|
||||
|
||||
t.Run("replace single panel", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p2",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/BarChartPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PanelPluginKind("signoz/BarChartPanel"), out.Data.Panels["p2"].Spec.Plugin.Kind)
|
||||
assert.Equal(t, PanelPluginKind("signoz/TimeSeriesPanel"), out.Data.Panels["p1"].Spec.Plugin.Kind, "p1 untouched")
|
||||
})
|
||||
|
||||
// Removing a panel realistically also drops its layout item — exercise
|
||||
// the multi-op shape the UI sends.
|
||||
t.Run("remove panel and its layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "remove", "path": "/data/panels/p2"},
|
||||
{"op": "remove", "path": "/data/layouts/0/spec/items/1"}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 1)
|
||||
assert.Contains(t, out.Data.Panels, "p1")
|
||||
assert.NotContains(t, out.Data.Panels, "p2")
|
||||
raw := jsonOf(t, out)
|
||||
assert.NotContains(t, raw, `"$ref":"#/spec/panels/p2"`)
|
||||
assert.Contains(t, raw, `"$ref":"#/spec/panels/p1"`)
|
||||
})
|
||||
|
||||
// The headline use case: edit a single field of a single query inside
|
||||
// one panel without re-sending any other part of the dashboard.
|
||||
t.Run("rename single query inside panel", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1/spec/queries/0/spec/plugin/spec/name",
|
||||
"value": "renamed"
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, out.Data.Panels["p1"].Spec.Queries, 1)
|
||||
assert.Contains(t, jsonOf(t, out), `"name":"renamed"`)
|
||||
})
|
||||
|
||||
// Replace a query at a specific index — swaps query "A" out for "B"
|
||||
// without re-sending the rest of the panel.
|
||||
t.Run("replace query at index", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1/spec/queries/0",
|
||||
"value": {
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{
|
||||
"metricName": "signoz_db_calls_total",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}]
|
||||
}}}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Data.Panels["p1"].Spec.Queries, 1)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"name":"B"`)
|
||||
assert.NotContains(t, raw, `"name":"A"`)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Layout edits
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("move panel by editing layout x coordinate", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/items/0/x", "value": 6}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
raw := jsonOf(t, out)
|
||||
// The first item used to live at x=0, now lives at x=6.
|
||||
assert.Contains(t, raw, `"x":6,"y":0,"width":6,"height":6,"content":{"$ref":"#/spec/panels/p1"}`)
|
||||
})
|
||||
|
||||
t.Run("resize panel by editing layout width", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/items/0/width", "value": 12}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"width":12`)
|
||||
})
|
||||
|
||||
t.Run("rename layout row title", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/data/layouts/0/spec/display/title", "value": "Latency"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, jsonOf(t, out), `"title":"Latency"`)
|
||||
})
|
||||
|
||||
t.Run("append layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[{
|
||||
"op": "add",
|
||||
"path": "/data/layouts/0/spec/items/-",
|
||||
"value": {"x": 0, "y": 6, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p1"}}
|
||||
}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
// Item count went 2 → 3.
|
||||
raw := jsonOf(t, out)
|
||||
assert.Equal(t, 3, strings.Count(raw, `"$ref":"#/spec/panels/`))
|
||||
})
|
||||
|
||||
// Composing add-panel + add-layout-item is the realistic shape of the
|
||||
// "add a new chart to my dashboard" UI flow — exercise it end-to-end.
|
||||
t.Run("add panel and corresponding layout item", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/data/panels/p3",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"aggregations": [{"expression": "count()"}]
|
||||
}}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/data/layouts/0/spec/items/-",
|
||||
"value": {"x": 0, "y": 6, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p3"}}
|
||||
}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.Data.Panels, 3)
|
||||
raw := jsonOf(t, out)
|
||||
assert.Contains(t, raw, `"$ref":"#/spec/panels/p3"`)
|
||||
})
|
||||
|
||||
t.Run("append tag", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "add", "path": "/tags/-", "value": {"key": "env", "value": "staging"}}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 3)
|
||||
assert.Equal(t, "env", out.Tags[2].Key)
|
||||
assert.Equal(t, "staging", out.Tags[2].Value)
|
||||
})
|
||||
|
||||
t.Run("append tag when none exist", func(t *testing.T) {
|
||||
noTagsBase := &DashboardV2{
|
||||
Info: DashboardInfo{
|
||||
StoredDashboardInfo: base.Info.StoredDashboardInfo,
|
||||
Tags: nil,
|
||||
},
|
||||
}
|
||||
out, err := decode(t, `[{"op": "add", "path": "/tags/-", "value": {"key": "team", "value": "new"}}]`).Apply(noTagsBase)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 1)
|
||||
assert.Equal(t, "team", out.Tags[0].Key)
|
||||
assert.Equal(t, "new", out.Tags[0].Value)
|
||||
})
|
||||
|
||||
t.Run("replace tag value", func(t *testing.T) {
|
||||
out, err := decode(t, `[{"op": "replace", "path": "/tags/0/value", "value": "beta"}]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.Tags, 2)
|
||||
assert.Equal(t, "team", out.Tags[0].Key)
|
||||
assert.Equal(t, "beta", out.Tags[0].Value)
|
||||
assert.Equal(t, "env", out.Tags[1].Key, "tag at index 1 untouched")
|
||||
assert.Equal(t, "prod", out.Tags[1].Value, "tag at index 1 untouched")
|
||||
for _, tag := range out.Tags {
|
||||
assert.NotEqual(t, "alpha", tag.Value, "old tag value must be gone")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple ops applied in order", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Multi-step"},
|
||||
{"op": "remove", "path": "/data/panels/p2"},
|
||||
{"op": "add", "path": "/tags/-", "value": {"key": "env", "value": "staging"}}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Multi-step", out.Data.Display.Name)
|
||||
assert.Len(t, out.Data.Panels, 1)
|
||||
assert.Len(t, out.Tags, 3)
|
||||
})
|
||||
|
||||
// `test` is an RFC 6902 precondition op: aborts the patch if the value
|
||||
// at the path doesn't equal the supplied value. Used for optimistic
|
||||
// concurrency. Here it matches, so the subsequent ops apply.
|
||||
t.Run("test op passes", func(t *testing.T) {
|
||||
out, err := decode(t, `[
|
||||
{"op": "test", "path": "/data/display/name", "value": "Service overview"},
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Confirmed"}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Confirmed", out.Data.Display.Name)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Failure cases
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
t.Run("decode rejects non-array body", func(t *testing.T) {
|
||||
var patch PatchableDashboardV2
|
||||
err := json.Unmarshal([]byte(`{"op": "replace"}`), &patch)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("decode rejects malformed JSON", func(t *testing.T) {
|
||||
var patch PatchableDashboardV2
|
||||
// Outer json.Unmarshal rejects non-JSON before PatchableDashboardV2's
|
||||
// UnmarshalJSON runs, so the error is a stdlib SyntaxError rather
|
||||
// than the InvalidInput-classified wrap.
|
||||
err := json.Unmarshal([]byte(`not json`), &patch)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
// `test` precondition fails — the whole patch is rejected, including
|
||||
// the subsequent replace.
|
||||
t.Run("test op failure rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[
|
||||
{"op": "test", "path": "/data/display/name", "value": "Wrong"},
|
||||
{"op": "replace", "path": "/data/display/name", "value": "Should not apply"}
|
||||
]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("remove at missing path rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/data/panels/does-not-exist"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("remove schemaVersion rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/metadata/schemaVersion"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("wrong schemaVersion rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "replace", "path": "/metadata/schemaVersion", "value": "v5"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), SchemaVersion)
|
||||
})
|
||||
|
||||
t.Run("empty display name rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "replace", "path": "/data/display/name", "value": ""}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "data.display.name is required")
|
||||
})
|
||||
|
||||
t.Run("unknown top-level field rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{"op": "add", "path": "/bogus", "value": 42}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "bogus")
|
||||
})
|
||||
|
||||
t.Run("invalid panel kind rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {"plugin": {"kind": "signoz/NotAPanel", "spec": {}}}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "NotAPanel")
|
||||
})
|
||||
|
||||
t.Run("query kind incompatible with panel rejected", func(t *testing.T) {
|
||||
// PromQLQuery is not allowed on ListPanel — verify the cross-check
|
||||
// in Validate still runs after a patch.
|
||||
_, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p2",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/ListPanel", "spec": {}},
|
||||
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("removing the only query rejected", func(t *testing.T) {
|
||||
// Validate requires exactly one query per panel — leaving zero is rejected.
|
||||
_, err := decode(t, `[{"op": "remove", "path": "/data/panels/p2/spec/queries/0"}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "panel must have one query")
|
||||
})
|
||||
|
||||
t.Run("two direct queries rejected", func(t *testing.T) {
|
||||
// Validate requires exactly one query per panel. To display multiple
|
||||
// data sources in one panel, wrap them in a CompositeQuery (see the
|
||||
// "replace query with composite" subtest below).
|
||||
_, err := decode(t, `[{
|
||||
"op": "replace",
|
||||
"path": "/data/panels/p1",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A", "signal": "metrics",
|
||||
"aggregations": [{"metricName": "signoz_calls_total", "temporality": "cumulative", "timeAggregation": "rate", "spaceAggregation": "sum"}]
|
||||
}}}},
|
||||
{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "B", "signal": "metrics",
|
||||
"aggregations": [{"metricName": "signoz_db_calls_total", "temporality": "cumulative", "timeAggregation": "rate", "spaceAggregation": "sum"}]
|
||||
}}}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "panel must have one query")
|
||||
})
|
||||
|
||||
t.Run("too many tags rejected", func(t *testing.T) {
|
||||
_, err := decode(t, `[
|
||||
{"op": "add", "path": "/tags/-", "value": {"key": "t", "value": "1"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"key": "t", "value": "2"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"key": "t", "value": "3"}},
|
||||
{"op": "add", "path": "/tags/-", "value": {"key": "t", "value": "4"}}
|
||||
]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "at most")
|
||||
})
|
||||
}
|
||||
@@ -32,13 +32,4 @@ type Store interface {
|
||||
DeletePublic(context.Context, string) error
|
||||
|
||||
RunInTx(context.Context, func(context.Context) error) error
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// v2 dashboard methods
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
GetV2(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, *StorablePublicDashboard, error)
|
||||
|
||||
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data StorableDashboardData) error
|
||||
|
||||
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, locked bool, updatedBy string) error
|
||||
}
|
||||
|
||||
105
pkg/types/inframonitoringtypes/statefulsets.go
Normal file
105
pkg/types/inframonitoringtypes/statefulsets.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type StatefulSets struct {
|
||||
Type ResponseType `json:"type" required:"true"`
|
||||
Records []StatefulSetRecord `json:"records" required:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
|
||||
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
type StatefulSetRecord struct {
|
||||
StatefulSetName string `json:"statefulSetName" required:"true"`
|
||||
StatefulSetCPU float64 `json:"statefulSetCPU" required:"true"`
|
||||
StatefulSetCPURequest float64 `json:"statefulSetCPURequest" required:"true"`
|
||||
StatefulSetCPULimit float64 `json:"statefulSetCPULimit" required:"true"`
|
||||
StatefulSetMemory float64 `json:"statefulSetMemory" required:"true"`
|
||||
StatefulSetMemoryRequest float64 `json:"statefulSetMemoryRequest" required:"true"`
|
||||
StatefulSetMemoryLimit float64 `json:"statefulSetMemoryLimit" required:"true"`
|
||||
DesiredPods int `json:"desiredPods" required:"true"`
|
||||
CurrentPods int `json:"currentPods" required:"true"`
|
||||
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
|
||||
Meta map[string]string `json:"meta" required:"true"`
|
||||
}
|
||||
|
||||
// PostableStatefulSets is the request body for the v2 statefulsets list API.
|
||||
type PostableStatefulSets struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Filter *qbtypes.Filter `json:"filter"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
|
||||
OrderBy *qbtypes.OrderBy `json:"orderBy"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// Validate ensures PostableStatefulSets contains acceptable values.
|
||||
func (req *PostableStatefulSets) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.Start <= 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid start time %d: start must be greater than 0",
|
||||
req.Start,
|
||||
)
|
||||
}
|
||||
|
||||
if req.End <= 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid end time %d: end must be greater than 0",
|
||||
req.End,
|
||||
)
|
||||
}
|
||||
|
||||
if req.Start >= req.End {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid time range: start (%d) must be less than end (%d)",
|
||||
req.Start,
|
||||
req.End,
|
||||
)
|
||||
}
|
||||
|
||||
if req.Limit < 1 || req.Limit > 5000 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be between 1 and 5000")
|
||||
}
|
||||
|
||||
if req.Offset < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
|
||||
}
|
||||
|
||||
if req.OrderBy != nil {
|
||||
if !slices.Contains(StatefulSetsValidOrderByKeys, req.OrderBy.Key.Name) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
|
||||
}
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON validates input immediately after decoding.
|
||||
func (req *PostableStatefulSets) UnmarshalJSON(data []byte) error {
|
||||
type raw PostableStatefulSets
|
||||
var decoded raw
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*req = PostableStatefulSets(decoded)
|
||||
return req.Validate()
|
||||
}
|
||||
23
pkg/types/inframonitoringtypes/statefulsets_constants.go
Normal file
23
pkg/types/inframonitoringtypes/statefulsets_constants.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const (
|
||||
StatefulSetsOrderByCPU = "cpu"
|
||||
StatefulSetsOrderByCPURequest = "cpu_request"
|
||||
StatefulSetsOrderByCPULimit = "cpu_limit"
|
||||
StatefulSetsOrderByMemory = "memory"
|
||||
StatefulSetsOrderByMemoryRequest = "memory_request"
|
||||
StatefulSetsOrderByMemoryLimit = "memory_limit"
|
||||
StatefulSetsOrderByDesiredPods = "desired_pods"
|
||||
StatefulSetsOrderByCurrentPods = "current_pods"
|
||||
)
|
||||
|
||||
var StatefulSetsValidOrderByKeys = []string{
|
||||
StatefulSetsOrderByCPU,
|
||||
StatefulSetsOrderByCPURequest,
|
||||
StatefulSetsOrderByCPULimit,
|
||||
StatefulSetsOrderByMemory,
|
||||
StatefulSetsOrderByMemoryRequest,
|
||||
StatefulSetsOrderByMemoryLimit,
|
||||
StatefulSetsOrderByDesiredPods,
|
||||
StatefulSetsOrderByCurrentPods,
|
||||
}
|
||||
273
pkg/types/inframonitoringtypes/statefulsets_test.go
Normal file
273
pkg/types/inframonitoringtypes/statefulsets_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableStatefulSets_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableStatefulSets
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid request",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time zero",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 0,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time negative",
|
||||
req: &PostableStatefulSets{
|
||||
Start: -1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end time zero",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 0,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time greater than end time",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 2000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start time equal to end time",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 1000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: -10,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "limit exceeds max",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 5001,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: -5,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy nil is valid",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key cpu and direction asc",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: StatefulSetsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key memory_limit and direction desc",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: StatefulSetsOrderByMemoryLimit,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key desired_pods and direction desc",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: StatefulSetsOrderByDesiredPods,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key current_pods and direction asc",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: StatefulSetsOrderByCurrentPods,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy with restarts key is rejected",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "restarts",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy with invalid key",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "unknown",
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy with valid key but invalid direction",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: StatefulSetsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirection{String: valuer.NewString("invalid")},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.req.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Ast(err, errors.TypeInvalidInput), "expected error to be of type InvalidInput")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package tagtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*Tag, error)
|
||||
|
||||
ListByResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*Tag, error)
|
||||
|
||||
ListByResources(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceIDs []valuer.UUID) (map[valuer.UUID][]*Tag, error)
|
||||
|
||||
// CreateOrGet upserts the given tags and returns them with authoritative IDs.
|
||||
// On conflict on (org_id, kind, LOWER(key), LOWER(value)) — which
|
||||
// happens only when a concurrent insert raced ours, including casing-only
|
||||
// collisions — the returned entry carries the existing row's ID rather
|
||||
// than the pre-generated one in the input.
|
||||
CreateOrGet(ctx context.Context, tags []*Tag) ([]*Tag, error)
|
||||
|
||||
// CreateRelations inserts tag-resource relations. Conflicts on the composite primary key are ignored.
|
||||
CreateRelations(ctx context.Context, relations []*TagRelation) error
|
||||
|
||||
DeleteRelationsExcept(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, keepTagIDs []valuer.UUID) error
|
||||
|
||||
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package tagtypes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_LEN_TAG_KEY = 32
|
||||
MAX_LEN_TAG_VALUE = 32
|
||||
)
|
||||
|
||||
var (
|
||||
tagKeyRegex = regexp.MustCompile(`^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$`)
|
||||
tagValueRegex = regexp.MustCompile(`^[a-zA-Z0-9$_@#{}:.+=/-]*$`)
|
||||
|
||||
ErrCodeTagInvalidKey = errors.MustNewCode("tag_invalid_key")
|
||||
ErrCodeTagInvalidValue = errors.MustNewCode("tag_invalid_value")
|
||||
ErrCodeTagNotFound = errors.MustNewCode("tag_not_found")
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
bun.BaseModel `bun:"table:tag,alias:tag"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Key string `json:"key" required:"true" bun:"key,type:text,notnull"`
|
||||
Value string `json:"value" required:"true" bun:"value,type:text,notnull"`
|
||||
OrgID valuer.UUID `json:"orgId" required:"true" bun:"org_id,type:text,notnull"`
|
||||
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
|
||||
}
|
||||
|
||||
type PostableTag struct {
|
||||
Key string `json:"key" required:"true"`
|
||||
Value string `json:"value" required:"true"`
|
||||
}
|
||||
|
||||
type GettableTag = PostableTag
|
||||
|
||||
func NewGettableTagFromTag(tag *Tag) *GettableTag {
|
||||
return &GettableTag{Key: tag.Key, Value: tag.Value}
|
||||
}
|
||||
|
||||
func NewGettableTagsFromTags(tags []*Tag) []*GettableTag {
|
||||
out := make([]*GettableTag, len(tags))
|
||||
for i, t := range tags {
|
||||
out[i] = NewGettableTagFromTag(t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NewPostableTagFromTag(tag *Tag) PostableTag {
|
||||
return PostableTag{Key: tag.Key, Value: tag.Value}
|
||||
}
|
||||
|
||||
func NewPostableTagsFromTags(tags []*Tag) []PostableTag {
|
||||
out := make([]PostableTag, len(tags))
|
||||
for i, t := range tags {
|
||||
out[i] = NewPostableTagFromTag(t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NewTag(orgID valuer.UUID, kind coretypes.Kind, key, value string) *Tag {
|
||||
now := time.Now()
|
||||
return &Tag{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Key: key,
|
||||
Value: value,
|
||||
OrgID: orgID,
|
||||
Kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePostableTag trims and validates a user-supplied (key, value) pair.
|
||||
// Returns the cleaned values on success. Entity-specific reserved-key checks
|
||||
// (e.g. dashboard column names that would collide with the list-query DSL) are
|
||||
// the caller's responsibility — perform them before calling into the tag module.
|
||||
func ValidatePostableTag(p PostableTag) (string, string, error) {
|
||||
key := strings.TrimSpace(p.Key)
|
||||
value := strings.TrimSpace(p.Value)
|
||||
if key == "" {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key cannot be empty")
|
||||
}
|
||||
if value == "" {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value cannot be empty")
|
||||
}
|
||||
if !tagKeyRegex.MatchString(key) {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key %q contains disallowed characters", key)
|
||||
}
|
||||
if !tagValueRegex.MatchString(value) {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value %q contains disallowed characters", value)
|
||||
}
|
||||
if utf8.RuneCountInString(key) > MAX_LEN_TAG_KEY {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidKey, "tag key %q exceeds the %d-character limit", key, MAX_LEN_TAG_KEY)
|
||||
}
|
||||
if utf8.RuneCountInString(value) > MAX_LEN_TAG_VALUE {
|
||||
return "", "", errors.Newf(errors.TypeInvalidInput, ErrCodeTagInvalidValue, "tag value %q exceeds the %d-character limit", value, MAX_LEN_TAG_VALUE)
|
||||
}
|
||||
return key, value, nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package tagtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type TagRelation struct {
|
||||
bun.BaseModel `bun:"table:tag_relation,alias:tag_relation"`
|
||||
|
||||
types.Identifiable
|
||||
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
|
||||
ResourceID valuer.UUID `json:"resourceId" required:"true" bun:"resource_id,type:text,notnull"`
|
||||
TagID valuer.UUID `json:"tagId" required:"true" bun:"tag_id,type:text,notnull"`
|
||||
}
|
||||
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID) *TagRelation {
|
||||
return &TagRelation{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
Kind: kind,
|
||||
ResourceID: resourceID,
|
||||
TagID: tagID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTagRelations(kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) []*TagRelation {
|
||||
relations := make([]*TagRelation, 0, len(tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID))
|
||||
}
|
||||
return relations
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package tagtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidatePostableTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input PostableTag
|
||||
wantKey string
|
||||
wantValue string
|
||||
wantError bool
|
||||
}{
|
||||
{name: "simple pair", input: PostableTag{Key: "team", Value: "pulse"}, wantKey: "team", wantValue: "pulse"},
|
||||
{name: "preserves casing", input: PostableTag{Key: "Team", Value: "Pulse"}, wantKey: "Team", wantValue: "Pulse"},
|
||||
{name: "trims key", input: PostableTag{Key: " team ", Value: "pulse"}, wantKey: "team", wantValue: "pulse"},
|
||||
{name: "trims value", input: PostableTag{Key: "team", Value: " pulse "}, wantKey: "team", wantValue: "pulse"},
|
||||
|
||||
{name: "empty key rejected", input: PostableTag{Key: "", Value: "pulse"}, wantError: true},
|
||||
{name: "empty value rejected", input: PostableTag{Key: "team", Value: ""}, wantError: true},
|
||||
{name: "whitespace-only key rejected", input: PostableTag{Key: " ", Value: "pulse"}, wantError: true},
|
||||
{name: "whitespace-only value rejected", input: PostableTag{Key: "team", Value: " "}, wantError: true},
|
||||
|
||||
{name: "slash accepted", input: PostableTag{Key: "team/eng", Value: "pulse/events"}, wantKey: "team/eng", wantValue: "pulse/events"},
|
||||
{name: "colon accepted", input: PostableTag{Key: "team:eng", Value: "env:prod"}, wantKey: "team:eng", wantValue: "env:prod"},
|
||||
{name: "extra punctuation accepted in both", input: PostableTag{Key: "a_b-c@d#e$f{g}h", Value: "a_b-c@d#e$f{g}h"}, wantKey: "a_b-c@d#e$f{g}h", wantValue: "a_b-c@d#e$f{g}h"},
|
||||
|
||||
// Key is strict; value allows the extra `. + =` plus leading digits.
|
||||
{name: "dot in key rejected", input: PostableTag{Key: "team.eng", Value: "pulse"}, wantError: true},
|
||||
{name: "dot in value accepted", input: PostableTag{Key: "team", Value: "pulse.events"}, wantKey: "team", wantValue: "pulse.events"},
|
||||
{name: "plus in key rejected", input: PostableTag{Key: "team+eng", Value: "pulse"}, wantError: true},
|
||||
{name: "plus in value accepted", input: PostableTag{Key: "team", Value: "a+b"}, wantKey: "team", wantValue: "a+b"},
|
||||
{name: "equals in key rejected", input: PostableTag{Key: "team=eng", Value: "pulse"}, wantError: true},
|
||||
{name: "equals in value accepted", input: PostableTag{Key: "team", Value: "a=b"}, wantKey: "team", wantValue: "a=b"},
|
||||
{name: "leading digit in key rejected", input: PostableTag{Key: "2024team", Value: "pulse"}, wantError: true},
|
||||
{name: "leading digit in value accepted", input: PostableTag{Key: "team", Value: "2024_team"}, wantKey: "team", wantValue: "2024_team"},
|
||||
|
||||
{name: "unicode letter in key rejected", input: PostableTag{Key: "チーム", Value: "pulse"}, wantError: true},
|
||||
{name: "unicode letter in value rejected", input: PostableTag{Key: "team", Value: "東京"}, wantError: true},
|
||||
|
||||
{name: "internal space in key rejected", input: PostableTag{Key: "team eng", Value: "pulse"}, wantError: true},
|
||||
{name: "internal space in value rejected", input: PostableTag{Key: "team", Value: "pulse two"}, wantError: true},
|
||||
|
||||
{name: "disallowed char in key rejected", input: PostableTag{Key: "team!eng", Value: "pulse"}, wantError: true},
|
||||
{name: "disallowed char in value rejected", input: PostableTag{Key: "team", Value: "pulse!one"}, wantError: true},
|
||||
{name: "control char rejected", input: PostableTag{Key: "team\tone", Value: "pulse"}, wantError: true},
|
||||
|
||||
{name: "key at the 32-char limit accepted", input: PostableTag{Key: "abcdefghijklmnopabcdefghijklmnop", Value: "pulse"}, wantKey: "abcdefghijklmnopabcdefghijklmnop", wantValue: "pulse"},
|
||||
{name: "value at the 32-char limit accepted", input: PostableTag{Key: "team", Value: "abcdefghijklmnopabcdefghijklmnop"}, wantKey: "team", wantValue: "abcdefghijklmnopabcdefghijklmnop"},
|
||||
{name: "key over the 32-char limit rejected", input: PostableTag{Key: "abcdefghijklmnopabcdefghijklmnopq", Value: "pulse"}, wantError: true},
|
||||
{name: "value over the 32-char limit rejected", input: PostableTag{Key: "team", Value: "abcdefghijklmnopabcdefghijklmnopq"}, wantError: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotKey, gotValue, err := ValidatePostableTag(tc.input)
|
||||
if tc.wantError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantKey, gotKey)
|
||||
assert.Equal(t, tc.wantValue, gotValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package tagtypestest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MockStore is an in-memory tagtypes.MockStore implementation for tests. Most methods
|
||||
// are inert no-ops; List returns the contents of Tags and increments
|
||||
// ListCallCount so tests can assert on lookup behavior. Set Tags directly to
|
||||
// preload fixtures.
|
||||
type MockStore struct {
|
||||
Tags []*tagtypes.Tag
|
||||
ListCallCount int
|
||||
}
|
||||
|
||||
func NewStore() *MockStore {
|
||||
return &MockStore{}
|
||||
}
|
||||
|
||||
func (s *MockStore) List(_ context.Context, _ valuer.UUID, _ coretypes.Kind) ([]*tagtypes.Tag, error) {
|
||||
s.ListCallCount++
|
||||
out := make([]*tagtypes.Tag, len(s.Tags))
|
||||
copy(out, s.Tags)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MockStore) CreateOrGet(_ context.Context, tags []*tagtypes.Tag) ([]*tagtypes.Tag, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (s *MockStore) CreateRelations(_ context.Context, _ []*tagtypes.TagRelation) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MockStore) ListByResource(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ valuer.UUID) ([]*tagtypes.Tag, error) {
|
||||
return []*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
func (s *MockStore) ListByResources(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, error) {
|
||||
return map[valuer.UUID][]*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
func (s *MockStore) DeleteRelationsExcept(_ context.Context, _ valuer.UUID, _ coretypes.Kind, _ valuer.UUID, _ []valuer.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MockStore) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
|
||||
return cb(ctx)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pytest_plugins = [
|
||||
"fixtures.seeder",
|
||||
"fixtures.serviceaccount",
|
||||
"fixtures.role",
|
||||
"fixtures.seed_golden_dataset",
|
||||
]
|
||||
|
||||
|
||||
|
||||
13
tests/e2e/bootstrap/global.setup.ts
Normal file
13
tests/e2e/bootstrap/global.setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, test as setup } from '@playwright/test';
|
||||
|
||||
const seederUrl = process.env.SIGNOZ_E2E_SEEDER_URL ?? '';
|
||||
|
||||
setup('refresh golden dataset', async ({ request }) => {
|
||||
expect(seederUrl, 'SIGNOZ_E2E_SEEDER_URL not set').not.toBe('');
|
||||
const response = await request.post(`${seederUrl}/seed/golden`, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[setup] refreshed golden dataset: ${await response.text()}`);
|
||||
});
|
||||
13
tests/e2e/bootstrap/global.teardown.ts
Normal file
13
tests/e2e/bootstrap/global.teardown.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, test as teardown } from '@playwright/test';
|
||||
|
||||
const seederUrl = process.env.SIGNOZ_E2E_SEEDER_URL ?? '';
|
||||
|
||||
teardown('clear seeded telemetry', async ({ request }) => {
|
||||
expect(seederUrl, 'SIGNOZ_E2E_SEEDER_URL not set').not.toBe('');
|
||||
for (const signal of ['metrics', 'traces', 'logs'] as const) {
|
||||
const response = await request.delete(`${seederUrl}/telemetry/${signal}`, {
|
||||
timeout: 60_000,
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
@@ -39,5 +40,17 @@ def test_teardown(
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
apply_license: types.Operation, # pylint: disable=unused-argument
|
||||
seeder: types.TestContainerDocker, # pylint: disable=unused-argument
|
||||
pytestconfig: pytest.Config,
|
||||
) -> None:
|
||||
"""Fixture dependencies trigger container teardown via --teardown."""
|
||||
"""Truncate seeded telemetry; containers come down via fixture
|
||||
dependency under `--teardown`."""
|
||||
cached = pytestconfig.cache.get("seeder", None)
|
||||
if not cached:
|
||||
return
|
||||
restored = types.TestContainerDocker.from_cache(cached)
|
||||
base = restored.host_configs["8080"].base().rstrip("/")
|
||||
for signal in ("metrics", "traces", "logs"):
|
||||
try:
|
||||
requests.delete(f"{base}/telemetry/{signal}", timeout=30).raise_for_status()
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
print(f"seeder DELETE /telemetry/{signal} failed: {e}")
|
||||
|
||||
@@ -50,12 +50,36 @@ export default defineConfig({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
|
||||
// Browser projects. No project-level auth — specs opt in via the
|
||||
// authedPage fixture in tests/e2e/fixtures/auth.ts, which logs a user
|
||||
// in on first use and caches the resulting storageState per worker.
|
||||
// `setup` runs `bootstrap/global.setup.ts` once before any browser
|
||||
// project — refreshes the golden dataset so chart-data assertions
|
||||
// land inside default panel time windows. Per
|
||||
// https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies.
|
||||
projects: [
|
||||
{ name: 'chromium', use: devices['Desktop Chrome'] },
|
||||
{ name: 'firefox', use: devices['Desktop Firefox'] },
|
||||
{ name: 'webkit', use: devices['Desktop Safari'] },
|
||||
{
|
||||
name: 'setup',
|
||||
testDir: './bootstrap',
|
||||
testMatch: /global\.setup\.ts/,
|
||||
teardown: 'teardown',
|
||||
},
|
||||
{
|
||||
name: 'teardown',
|
||||
testDir: './bootstrap',
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: devices['Desktop Chrome'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: devices['Desktop Firefox'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: devices['Desktop Safari'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["tests/**/*.ts", "helpers/**/*.ts", "fixtures/**/*.ts", "playwright.config.ts"],
|
||||
"include": ["tests/**/*.ts", "helpers/**/*.ts", "fixtures/**/*.ts", "bootstrap/**/*.ts", "playwright.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.41.0.tgz#09438448ab9479730582cd00fb86e4c33795a364"
|
||||
integrity sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==
|
||||
|
||||
"@oxfmt/binding-android-arm64@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.41.0.tgz#b01eef08ff2a6be90f5805fe28339ec0130a955c"
|
||||
integrity sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g==
|
||||
|
||||
"@oxfmt/binding-darwin-arm64@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.41.0.tgz#a2e24b7c52d40e3bb01939fa9c994d56923294ce"
|
||||
integrity sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA==
|
||||
|
||||
"@oxfmt/binding-darwin-x64@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.41.0.tgz#c114c0a30195a65cfe0247e2ffd000416df4d4bc"
|
||||
integrity sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw==
|
||||
|
||||
"@oxfmt/binding-freebsd-x64@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.41.0.tgz#a9057a31860ec79970a16b5a0a801a51fab99168"
|
||||
integrity sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ==
|
||||
|
||||
"@oxfmt/binding-linux-arm-gnueabihf@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.41.0.tgz#1a2be3cbbf2d9e90808858ba8fc38b24023ac44c"
|
||||
integrity sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g==
|
||||
|
||||
"@oxfmt/binding-linux-arm-musleabihf@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.41.0.tgz#f0a12feca29bfaf7437d94c32919ff3ac60c213f"
|
||||
integrity sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA==
|
||||
|
||||
"@oxfmt/binding-linux-arm64-gnu@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.41.0.tgz#b304c7ede72119f7fe7ce7861007c0ee6eb60c08"
|
||||
integrity sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA==
|
||||
|
||||
"@oxfmt/binding-linux-arm64-musl@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.41.0.tgz#295d6ce8d846ca1a287165bf4ff6e7b3cd51f823"
|
||||
integrity sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==
|
||||
|
||||
"@oxfmt/binding-linux-ppc64-gnu@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.41.0.tgz#6c702f37bd69bb88bf78264eef5df9dbcdcada49"
|
||||
integrity sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-gnu@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.41.0.tgz#9dac4f7f4cf10d94ade5a109554f810a43525595"
|
||||
integrity sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-musl@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.41.0.tgz#ddd573ad4fc8e5d017ab27177eaabea700d68754"
|
||||
integrity sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==
|
||||
|
||||
"@oxfmt/binding-linux-s390x-gnu@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.41.0.tgz#3300ee81681382c1ec0a4013807c362c27ef4821"
|
||||
integrity sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==
|
||||
|
||||
"@oxfmt/binding-linux-x64-gnu@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.41.0.tgz#be9068d48bb521eea83ea6bea2b0e08b39d7638b"
|
||||
integrity sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==
|
||||
|
||||
"@oxfmt/binding-linux-x64-musl@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.41.0.tgz#e0232abd879874823213b71016042d001a740acb"
|
||||
integrity sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==
|
||||
|
||||
"@oxfmt/binding-openharmony-arm64@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.41.0.tgz#3e5893cde8cae02494cbb5493de9c46613e77058"
|
||||
integrity sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==
|
||||
|
||||
"@oxfmt/binding-win32-arm64-msvc@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.41.0.tgz#31d8dfeeddc855dbecc9f45d11cd892635d2e42f"
|
||||
integrity sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ==
|
||||
|
||||
"@oxfmt/binding-win32-ia32-msvc@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.41.0.tgz#873fb408a3e8952fae67763b225e4ea77356e926"
|
||||
integrity sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA==
|
||||
|
||||
"@oxfmt/binding-win32-x64-msvc@0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.41.0.tgz#0adf46bca6908c6b49c790e598f4972b6f4cd191"
|
||||
integrity sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A==
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.20.0.tgz#6d704883904fa6e5fc922a7cef882bad4c734a0b"
|
||||
integrity sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.20.0.tgz#676d7a1cf82216d91e9bc143f676e7dbe59ce596"
|
||||
integrity sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.20.0.tgz#5a2e67cf18394b6b868301cf3d84c8b8be7ab6b9"
|
||||
integrity sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==
|
||||
|
||||
"@oxlint-tsgolint/linux-x64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-0.20.0.tgz#68468ad9253668748736191b6a7af51ae737c0a4"
|
||||
integrity sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.20.0.tgz#32a0eba60f0a88cd6d6d53cc03eb1b2e43ca0c50"
|
||||
integrity sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==
|
||||
|
||||
"@oxlint-tsgolint/win32-x64@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-0.20.0.tgz#90d5582b1df2ae877b2c5d7620e3a55acf9cc45f"
|
||||
integrity sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==
|
||||
|
||||
"@oxlint/binding-android-arm-eabi@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.61.0.tgz#c7a8fb22ac3084d6f4c873cdc9779f4f9e38b805"
|
||||
integrity sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==
|
||||
|
||||
"@oxlint/binding-android-arm64@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.61.0.tgz#5fe7c707e12513ede292da015d13e93781771e9f"
|
||||
integrity sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==
|
||||
|
||||
"@oxlint/binding-darwin-arm64@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.61.0.tgz#1f58a4c592b76e293e5b9072cc0e4e7a5bef8191"
|
||||
integrity sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==
|
||||
|
||||
"@oxlint/binding-darwin-x64@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.61.0.tgz#eade7425cc943c3146f3e0f0fb4d08d615ee78a9"
|
||||
integrity sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==
|
||||
|
||||
"@oxlint/binding-freebsd-x64@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.61.0.tgz#8d82382a07ce4895c91694a810dee7f39866951b"
|
||||
integrity sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.61.0.tgz#df76a5700651cda73ca5578de7308f9479b7cb1b"
|
||||
integrity sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.61.0.tgz#b198e5697cfb6cc55eff5ab5b9766732e143492d"
|
||||
integrity sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.61.0.tgz#a5cdae7ef9f9b79526ef764fb174bb362c3737ee"
|
||||
integrity sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.61.0.tgz#227f6d94a3c00140a88ea338436783bd9da6f4ae"
|
||||
integrity sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.61.0.tgz#e504e6cd8691fe460b202599d4c962fc7ad98f94"
|
||||
integrity sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.61.0.tgz#efe70d9e570756fe19fb60d05a39dd58e9e0f32d"
|
||||
integrity sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.61.0.tgz#f9c42095192f235b03a5d3d1f1fbfb45002d5a93"
|
||||
integrity sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.61.0.tgz#23773cd6f8087f98c54383caccb98808fab6cca0"
|
||||
integrity sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.61.0.tgz#f46b036cf74b382c03f6a00e18cd1ae53096517f"
|
||||
integrity sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==
|
||||
|
||||
"@oxlint/binding-linux-x64-musl@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.61.0.tgz#cff6c4c24de0863cecbce1c1f43adcafacd9011d"
|
||||
integrity sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==
|
||||
|
||||
"@oxlint/binding-openharmony-arm64@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.61.0.tgz#9fdd87fc10c2c51c510470f7c4136ab2df9ee7b3"
|
||||
integrity sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.61.0.tgz#25a87f62859a6703f2520d665f98f6c0f88a70e0"
|
||||
integrity sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.61.0.tgz#06f7df42b074f28ef27540d38be69875e5d5afcb"
|
||||
integrity sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc@1.61.0":
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.61.0.tgz#7bc84b261bb8b515ade20f1044547cc8bbee44ce"
|
||||
integrity sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==
|
||||
|
||||
"@playwright/test@^1.57.0-alpha-2025-10-09":
|
||||
version "1.57.0-alpha-2025-10-11"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.57.0-alpha-2025-10-11.tgz#75f6fac2f98fcff6e4bae1c907b48ad0b1a33bea"
|
||||
integrity sha512-xqp2RNcLCPSUAYCrP3+rYZ4LFlESvWqjjpFegjNbun7wLcGvUt9Mh+RHBvgeZAhMxxuVde78XO9Y888UYFH9ew==
|
||||
dependencies:
|
||||
playwright "1.57.0-alpha-2025-10-11"
|
||||
|
||||
"@types/node@^20.0.0":
|
||||
version "20.19.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.20.tgz#116f5432a1e6383415a57cf5e00b5ac9b2a2ad03"
|
||||
integrity sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
|
||||
dotenv@^16.0.0:
|
||||
version "16.6.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
|
||||
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
|
||||
|
||||
eslint-plugin-playwright@^2.10.2:
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-2.10.2.tgz#1f0bef35c0b9f996a6987a401e6d4e6a3a015861"
|
||||
integrity sha512-0N+2OWc3NZbOZ0gK8mp2TK6Qu3UWcJTQ9rqU0UM2yRJXgT758pvpY0lsOLIySfbyFrLqn3TcXjixbmcK90VnuQ==
|
||||
dependencies:
|
||||
globals "^17.3.0"
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
globals@^17.3.0:
|
||||
version "17.5.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.5.0.tgz#a82c641d898f8dfbe0e81f66fdff7d0de43f88c6"
|
||||
integrity sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==
|
||||
|
||||
oxfmt@^0.41.0:
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.41.0.tgz#7dbfb63d19704a85412f36678c3ace092ce88673"
|
||||
integrity sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==
|
||||
dependencies:
|
||||
tinypool "2.1.0"
|
||||
optionalDependencies:
|
||||
"@oxfmt/binding-android-arm-eabi" "0.41.0"
|
||||
"@oxfmt/binding-android-arm64" "0.41.0"
|
||||
"@oxfmt/binding-darwin-arm64" "0.41.0"
|
||||
"@oxfmt/binding-darwin-x64" "0.41.0"
|
||||
"@oxfmt/binding-freebsd-x64" "0.41.0"
|
||||
"@oxfmt/binding-linux-arm-gnueabihf" "0.41.0"
|
||||
"@oxfmt/binding-linux-arm-musleabihf" "0.41.0"
|
||||
"@oxfmt/binding-linux-arm64-gnu" "0.41.0"
|
||||
"@oxfmt/binding-linux-arm64-musl" "0.41.0"
|
||||
"@oxfmt/binding-linux-ppc64-gnu" "0.41.0"
|
||||
"@oxfmt/binding-linux-riscv64-gnu" "0.41.0"
|
||||
"@oxfmt/binding-linux-riscv64-musl" "0.41.0"
|
||||
"@oxfmt/binding-linux-s390x-gnu" "0.41.0"
|
||||
"@oxfmt/binding-linux-x64-gnu" "0.41.0"
|
||||
"@oxfmt/binding-linux-x64-musl" "0.41.0"
|
||||
"@oxfmt/binding-openharmony-arm64" "0.41.0"
|
||||
"@oxfmt/binding-win32-arm64-msvc" "0.41.0"
|
||||
"@oxfmt/binding-win32-ia32-msvc" "0.41.0"
|
||||
"@oxfmt/binding-win32-x64-msvc" "0.41.0"
|
||||
|
||||
oxlint-tsgolint@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-0.20.0.tgz#e011319219faa572faf340c40d65708404b37e10"
|
||||
integrity sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==
|
||||
optionalDependencies:
|
||||
"@oxlint-tsgolint/darwin-arm64" "0.20.0"
|
||||
"@oxlint-tsgolint/darwin-x64" "0.20.0"
|
||||
"@oxlint-tsgolint/linux-arm64" "0.20.0"
|
||||
"@oxlint-tsgolint/linux-x64" "0.20.0"
|
||||
"@oxlint-tsgolint/win32-arm64" "0.20.0"
|
||||
"@oxlint-tsgolint/win32-x64" "0.20.0"
|
||||
|
||||
oxlint@^1.59.0:
|
||||
version "1.61.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.61.0.tgz#e714742cfe7b815713feb14600b0ffe963d539a4"
|
||||
integrity sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==
|
||||
optionalDependencies:
|
||||
"@oxlint/binding-android-arm-eabi" "1.61.0"
|
||||
"@oxlint/binding-android-arm64" "1.61.0"
|
||||
"@oxlint/binding-darwin-arm64" "1.61.0"
|
||||
"@oxlint/binding-darwin-x64" "1.61.0"
|
||||
"@oxlint/binding-freebsd-x64" "1.61.0"
|
||||
"@oxlint/binding-linux-arm-gnueabihf" "1.61.0"
|
||||
"@oxlint/binding-linux-arm-musleabihf" "1.61.0"
|
||||
"@oxlint/binding-linux-arm64-gnu" "1.61.0"
|
||||
"@oxlint/binding-linux-arm64-musl" "1.61.0"
|
||||
"@oxlint/binding-linux-ppc64-gnu" "1.61.0"
|
||||
"@oxlint/binding-linux-riscv64-gnu" "1.61.0"
|
||||
"@oxlint/binding-linux-riscv64-musl" "1.61.0"
|
||||
"@oxlint/binding-linux-s390x-gnu" "1.61.0"
|
||||
"@oxlint/binding-linux-x64-gnu" "1.61.0"
|
||||
"@oxlint/binding-linux-x64-musl" "1.61.0"
|
||||
"@oxlint/binding-openharmony-arm64" "1.61.0"
|
||||
"@oxlint/binding-win32-arm64-msvc" "1.61.0"
|
||||
"@oxlint/binding-win32-ia32-msvc" "1.61.0"
|
||||
"@oxlint/binding-win32-x64-msvc" "1.61.0"
|
||||
|
||||
playwright-core@1.57.0-alpha-2025-10-11:
|
||||
version "1.57.0-alpha-2025-10-11"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-11.tgz#b444b75542a43bc354e512d076344d9258997e9c"
|
||||
integrity sha512-X6KAunryZlslAdEdlN5gIIP3sFU6Uot3vzLoGCZ9SNv0JvXd6e2g7ArjnpOQld36yKszq8J+wQJRlIvdXkIvRw==
|
||||
|
||||
playwright@1.57.0-alpha-2025-10-11:
|
||||
version "1.57.0-alpha-2025-10-11"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0-alpha-2025-10-11.tgz#af67a28d64b39fe57eea454b2b312e98bfff02a5"
|
||||
integrity sha512-a80kAd59up/kURcKE7THLzx3lN6a1G9RhsgP9ZfLGL7WtnOhOdRLxbHwmjWUG11ybEDeYNpj1qwT02MT4R+rew==
|
||||
dependencies:
|
||||
playwright-core "1.57.0-alpha-2025-10-11"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
tinypool@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.1.0.tgz#303a671d6ef68d03c9512cdc9a47c86b8a85f20c"
|
||||
integrity sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==
|
||||
|
||||
typescript@^5.0.0:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
361
tests/fixtures/seed_golden_dataset.py
vendored
Normal file
361
tests/fixtures/seed_golden_dataset.py
vendored
Normal file
@@ -0,0 +1,361 @@
|
||||
"""Golden dataset fixture — seeds OTel-demo-shaped metrics, traces, and
|
||||
logs into ClickHouse via the seeder on every test_setup invocation.
|
||||
|
||||
Timestamps are rebased to `now` so panels with default time windows
|
||||
always find data. To refresh the dataset shape on disk, run
|
||||
`uv run python -m fixtures.seed_golden_dataset regenerate`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_GOLDEN_DIR = Path(__file__).resolve().parent.parent / "seeder" / "golden"
|
||||
METRICS_PATH = _GOLDEN_DIR / "otel-demo-metrics-golden.jsonl"
|
||||
TRACES_PATH = _GOLDEN_DIR / "otel-demo-traces-golden.jsonl"
|
||||
LOGS_PATH = _GOLDEN_DIR / "otel-demo-logs-golden.jsonl"
|
||||
|
||||
|
||||
# ─── Generator ───────────────────────────────────────────────────────────
|
||||
|
||||
_SERVICES = [
|
||||
"adservice",
|
||||
"cartservice",
|
||||
"checkoutservice",
|
||||
"currencyservice",
|
||||
"frontend",
|
||||
"paymentservice",
|
||||
"productcatalogservice",
|
||||
"shippingservice",
|
||||
]
|
||||
_OPERATIONS = {
|
||||
"adservice": ["/ads/get", "/ads/list"],
|
||||
"cartservice": ["/cart/add", "/cart/get", "/cart/empty"],
|
||||
"checkoutservice": ["/checkout"],
|
||||
"currencyservice": ["/currency/convert"],
|
||||
"frontend": ["/", "/product", "/checkout"],
|
||||
"paymentservice": ["/payment/charge"],
|
||||
"productcatalogservice": ["/products/list", "/products/get"],
|
||||
"shippingservice": ["/shipping/quote", "/shipping/ship"],
|
||||
}
|
||||
_DB_SERVICES = {"cartservice", "productcatalogservice"}
|
||||
_ENV = "production"
|
||||
_BUCKET_MINUTES = 5
|
||||
_WINDOW_HOURS = 6
|
||||
|
||||
|
||||
def _generate_metrics() -> list[dict]:
|
||||
rng = random.Random(20260511)
|
||||
samples: list[dict] = []
|
||||
n_buckets = (_WINDOW_HOURS * 60) // _BUCKET_MINUTES
|
||||
base_counter = 1000
|
||||
|
||||
for service in _SERVICES:
|
||||
for operation in _OPERATIONS[service]:
|
||||
for status in ("STATUS_CODE_OK", "STATUS_CODE_ERROR"):
|
||||
weight = 9 if status == "STATUS_CODE_OK" else 1
|
||||
counter = base_counter
|
||||
latency_sum = 0
|
||||
for i in range(n_buckets):
|
||||
minutes_ago = (_WINDOW_HOURS * 60) - (i + 1) * _BUCKET_MINUTES
|
||||
bucket_calls = int(weight * (50 + 20 * (1 + i % 12 / 12.0) + rng.randint(0, 10)))
|
||||
counter += bucket_calls
|
||||
latency_sum += bucket_calls * rng.randint(100_000, 500_000)
|
||||
resource_attrs = {
|
||||
"service.name": service,
|
||||
"deployment.environment": _ENV,
|
||||
"k8s.namespace.name": f"signoz-{service}",
|
||||
}
|
||||
point_attrs = {
|
||||
"operation": operation,
|
||||
"status_code": status,
|
||||
"span_kind": "SPAN_KIND_SERVER",
|
||||
}
|
||||
for name, value in (
|
||||
("signoz_calls_total", counter),
|
||||
("signoz_latency_count", counter),
|
||||
("signoz_latency_sum", latency_sum),
|
||||
):
|
||||
samples.append(
|
||||
{
|
||||
"metric_name": name,
|
||||
"minutes_ago": minutes_ago,
|
||||
"value": value,
|
||||
"resource_attributes": resource_attrs,
|
||||
"attributes": point_attrs,
|
||||
"is_monotonic": True,
|
||||
}
|
||||
)
|
||||
if service in _DB_SERVICES:
|
||||
db_counter = 0
|
||||
for i in range(n_buckets):
|
||||
minutes_ago = (_WINDOW_HOURS * 60) - (i + 1) * _BUCKET_MINUTES
|
||||
db_counter += 20 + rng.randint(0, 15)
|
||||
samples.append(
|
||||
{
|
||||
"metric_name": "signoz_db_latency_count",
|
||||
"minutes_ago": minutes_ago,
|
||||
"value": db_counter,
|
||||
"resource_attributes": {
|
||||
"service.name": service,
|
||||
"deployment.environment": _ENV,
|
||||
"k8s.namespace.name": f"signoz-{service}",
|
||||
},
|
||||
"attributes": {
|
||||
"db.system": "postgresql" if service == "cartservice" else "mongodb",
|
||||
},
|
||||
"is_monotonic": True,
|
||||
}
|
||||
)
|
||||
return samples
|
||||
|
||||
|
||||
def _generate_traces() -> list[dict]:
|
||||
rng = random.Random(20260512)
|
||||
samples: list[dict] = []
|
||||
n_buckets = 12
|
||||
for service in _SERVICES:
|
||||
for operation in _OPERATIONS[service]:
|
||||
for i in range(n_buckets):
|
||||
minutes_ago = int((_WINDOW_HOURS * 60) - i * (_WINDOW_HOURS * 60 / n_buckets))
|
||||
http_status = "500" if rng.random() < 0.05 else "200"
|
||||
samples.append(
|
||||
{
|
||||
"name": f"{service} {operation}",
|
||||
"kind": "SERVER",
|
||||
"minutes_ago": minutes_ago,
|
||||
"duration_ms": rng.randint(50, 500),
|
||||
"status": "ERROR" if http_status == "500" else "OK",
|
||||
"resource_attributes": {
|
||||
"service.name": service,
|
||||
"deployment.environment": _ENV,
|
||||
"k8s.namespace.name": f"signoz-{service}",
|
||||
},
|
||||
"attributes": {
|
||||
"http.method": "GET" if "get" in operation.lower() or operation == "/" else "POST",
|
||||
"http.route": operation,
|
||||
"http.status_code": http_status,
|
||||
},
|
||||
}
|
||||
)
|
||||
return samples
|
||||
|
||||
|
||||
_LOG_SEVERITIES = [("INFO", 0.85), ("WARN", 0.10), ("ERROR", 0.05)]
|
||||
_LOG_BODIES = {
|
||||
"INFO": ["Handled request", "Cache hit", "Connection established"],
|
||||
"WARN": ["Slow response detected", "Cache miss", "Retrying upstream call"],
|
||||
"ERROR": ["Upstream call failed", "Database query timed out", "Auth failed"],
|
||||
}
|
||||
|
||||
|
||||
def _generate_logs() -> list[dict]:
|
||||
rng = random.Random(20260512)
|
||||
samples: list[dict] = []
|
||||
n_buckets = 24
|
||||
for service in _SERVICES:
|
||||
for i in range(n_buckets):
|
||||
minutes_ago = int((_WINDOW_HOURS * 60) - i * (_WINDOW_HOURS * 60 / n_buckets))
|
||||
r = rng.random()
|
||||
cumulative = 0.0
|
||||
severity = "INFO"
|
||||
for name, weight in _LOG_SEVERITIES:
|
||||
cumulative += weight
|
||||
if r < cumulative:
|
||||
severity = name
|
||||
break
|
||||
samples.append(
|
||||
{
|
||||
"body": f"[{service}] {rng.choice(_LOG_BODIES[severity])}",
|
||||
"severity": severity,
|
||||
"minutes_ago": minutes_ago,
|
||||
"resource_attributes": {
|
||||
"service.name": service,
|
||||
"deployment.environment": _ENV,
|
||||
"k8s.namespace.name": f"signoz-{service}",
|
||||
},
|
||||
"attributes": {"logger.name": f"{service}.app"},
|
||||
}
|
||||
)
|
||||
return samples
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, samples: list[dict]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w") as f:
|
||||
for s in samples:
|
||||
f.write(json.dumps(s, separators=(",", ":")))
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def regenerate() -> dict[str, int]:
|
||||
metrics = _generate_metrics()
|
||||
traces = _generate_traces()
|
||||
logs = _generate_logs()
|
||||
_write_jsonl(METRICS_PATH, metrics)
|
||||
_write_jsonl(TRACES_PATH, traces)
|
||||
_write_jsonl(LOGS_PATH, logs)
|
||||
return {"metrics": len(metrics), "traces": len(traces), "logs": len(logs)}
|
||||
|
||||
|
||||
# ─── Loader ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
_KIND_TO_INT = {
|
||||
"UNSPECIFIED": 0,
|
||||
"INTERNAL": 1,
|
||||
"SERVER": 2,
|
||||
"CLIENT": 3,
|
||||
"PRODUCER": 4,
|
||||
"CONSUMER": 5,
|
||||
}
|
||||
_STATUS_TO_INT = {"UNSET": 0, "OK": 1, "ERROR": 2}
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> Iterator[dict]:
|
||||
with path.open() as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
yield json.loads(line)
|
||||
|
||||
|
||||
def _iso_minus_minutes(now: datetime.datetime, minutes: float) -> str:
|
||||
ts = now - datetime.timedelta(minutes=minutes)
|
||||
return ts.replace(tzinfo=datetime.UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _rebased_metric(sample: dict, now: datetime.datetime) -> dict:
|
||||
out = {k: v for k, v in sample.items() if k != "minutes_ago"}
|
||||
out["timestamp"] = _iso_minus_minutes(now, sample["minutes_ago"])
|
||||
return out
|
||||
|
||||
|
||||
def _rebased_trace(sample: dict, now: datetime.datetime) -> dict:
|
||||
return {
|
||||
"timestamp": _iso_minus_minutes(now, sample["minutes_ago"]),
|
||||
"duration": f"PT{sample['duration_ms'] / 1000:.3f}S",
|
||||
"trace_id": sample.get("trace_id") or os.urandom(16).hex(),
|
||||
"span_id": sample.get("span_id") or os.urandom(8).hex(),
|
||||
"name": sample["name"],
|
||||
"kind": _KIND_TO_INT.get(str(sample.get("kind", "SERVER")).upper(), 2),
|
||||
"status_code": _STATUS_TO_INT.get(str(sample.get("status", "UNSET")).upper(), 0),
|
||||
"resources": sample.get("resource_attributes", {}),
|
||||
"attributes": sample.get("attributes", {}),
|
||||
}
|
||||
|
||||
|
||||
def _rebased_log(sample: dict, now: datetime.datetime) -> dict:
|
||||
return {
|
||||
"timestamp": _iso_minus_minutes(now, sample["minutes_ago"]),
|
||||
"body": sample["body"],
|
||||
"severity_text": str(sample.get("severity", "INFO")).upper(),
|
||||
"resources": sample.get("resource_attributes", {}),
|
||||
"attributes": sample.get("attributes", {}),
|
||||
}
|
||||
|
||||
|
||||
def _post_batches(url: str, rows: Iterator[dict], batch_size: int, timeout: int) -> int:
|
||||
batch: list[dict] = []
|
||||
total = 0
|
||||
for row in rows:
|
||||
batch.append(row)
|
||||
if len(batch) >= batch_size:
|
||||
response = requests.post(url, json=batch, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
total += len(batch)
|
||||
batch = []
|
||||
if batch:
|
||||
response = requests.post(url, json=batch, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
total += len(batch)
|
||||
return total
|
||||
|
||||
|
||||
def seed(
|
||||
seeder_base_url: str,
|
||||
*,
|
||||
batch_size: int = 500,
|
||||
timeout: int = 60,
|
||||
clear_first: bool = True,
|
||||
) -> dict[str, int]:
|
||||
"""Wipe each signal table (via DELETE /telemetry/<signal>) and replay
|
||||
the golden dataset with timestamps rebased to `now`. Each call leaves
|
||||
the stack in the exact state the JSONL files describe — chart-data
|
||||
assertions are reproducible across sessions regardless of how many
|
||||
earlier sessions seeded."""
|
||||
for path in (METRICS_PATH, TRACES_PATH, LOGS_PATH):
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"golden dataset missing at {path} — run `uv run python -m fixtures.seed_golden_dataset regenerate`")
|
||||
|
||||
now = datetime.datetime.now(datetime.UTC).replace(microsecond=0, tzinfo=None)
|
||||
base = seeder_base_url.rstrip("/")
|
||||
if clear_first:
|
||||
for signal in ("metrics", "traces", "logs"):
|
||||
requests.delete(f"{base}/telemetry/{signal}", timeout=timeout).raise_for_status()
|
||||
counts = {
|
||||
"metrics": _post_batches(
|
||||
base + "/telemetry/metrics",
|
||||
(_rebased_metric(s, now) for s in _read_jsonl(METRICS_PATH)),
|
||||
batch_size,
|
||||
timeout,
|
||||
),
|
||||
"traces": _post_batches(
|
||||
base + "/telemetry/traces",
|
||||
(_rebased_trace(s, now) for s in _read_jsonl(TRACES_PATH)),
|
||||
batch_size,
|
||||
timeout,
|
||||
),
|
||||
"logs": _post_batches(
|
||||
base + "/telemetry/logs",
|
||||
(_rebased_log(s, now) for s in _read_jsonl(LOGS_PATH)),
|
||||
batch_size,
|
||||
timeout,
|
||||
),
|
||||
}
|
||||
logger.info("seeded through %s: %s", base, counts)
|
||||
return counts
|
||||
|
||||
|
||||
# ─── Fixture ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture(name="golden_dataset", scope="package")
|
||||
def golden_dataset(seeder: types.TestContainerDocker) -> dict[str, int]:
|
||||
"""Seed metrics + traces + logs into the running stack via the
|
||||
seeder. Runs unconditionally on every test_setup invocation so the
|
||||
rebased timestamps always anchor against `now`."""
|
||||
return seed(seeder.host_configs["8080"].base())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if len(sys.argv) < 2:
|
||||
sys.stderr.write("usage: seed_golden_dataset.py seed <seeder-base-url> | regenerate\n")
|
||||
sys.exit(2)
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "regenerate":
|
||||
print(f"wrote {regenerate()}")
|
||||
elif cmd == "seed":
|
||||
if len(sys.argv) != 3:
|
||||
sys.stderr.write("usage: seed_golden_dataset.py seed <seeder-base-url>\n")
|
||||
sys.exit(2)
|
||||
print(f"seeded {seed(sys.argv[2])}")
|
||||
else:
|
||||
sys.stderr.write(f"unknown command: {cmd}\n")
|
||||
sys.exit(2)
|
||||
@@ -1,191 +0,0 @@
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.types import Operation, SigNoz
|
||||
|
||||
_PERSES_FIXTURE = (
|
||||
Path(__file__).parents[4]
|
||||
/ "pkg/types/dashboardtypes/dashboardtypesv2/testdata/perses.json"
|
||||
)
|
||||
|
||||
|
||||
def _post_dashboard(signoz: SigNoz, token: str, body: dict) -> requests.Response:
|
||||
return requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/dashboards"),
|
||||
json=body,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
|
||||
def test_empty_body_rejected_for_missing_schema_version(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = _post_dashboard(signoz, admin_token, {})
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
body = response.json()
|
||||
assert body["status"] == "error"
|
||||
assert body["error"]["code"] == "dashboard_invalid_input"
|
||||
assert body["error"]["message"] == 'metadata.schemaVersion must be "v6", got ""'
|
||||
|
||||
|
||||
def test_missing_display_name_rejected(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = _post_dashboard(signoz, admin_token, {"metadata": {"schemaVersion": "v6"}})
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
body = response.json()
|
||||
assert body["status"] == "error"
|
||||
assert body["error"]["code"] == "dashboard_invalid_input"
|
||||
assert body["error"]["message"] == "data.display.name is required"
|
||||
|
||||
|
||||
def test_minimal_valid_body_creates_dashboard(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {"display": {"name": "test name"}},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
body = response.json()
|
||||
assert body["status"] == "success"
|
||||
data = body["data"]
|
||||
assert data["info"]["data"]["display"]["name"] == "test name"
|
||||
assert data["info"]["metadata"]["schemaVersion"] == "v6"
|
||||
|
||||
|
||||
def test_unknown_root_field_rejected(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {"display": {"name": "test name"}},
|
||||
"unknownfieldattheroot": "shouldgiveanerror",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
body = response.json()
|
||||
assert body["status"] == "error"
|
||||
assert body["error"]["code"] == "dashboard_invalid_input"
|
||||
assert body["error"]["message"] == 'json: unknown field "unknownfieldattheroot"'
|
||||
|
||||
|
||||
def test_unknown_nested_field_rejected(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {
|
||||
"display": {
|
||||
"name": "test name",
|
||||
"unknownfieldinside": "shouldgiveanerror",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
body = response.json()
|
||||
assert body["status"] == "error"
|
||||
assert body["error"]["code"] == "dashboard_invalid_input"
|
||||
assert body["error"]["message"] == 'json: unknown field "unknownfieldinside"'
|
||||
|
||||
|
||||
def test_perses_fixture_creates_dashboard(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""The perses.json fixture is the kitchen-sink dashboard the schema tests
|
||||
use; round-tripping it through the create API exercises the full plugin
|
||||
surface end-to-end."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
data = json.loads(_PERSES_FIXTURE.read_text())
|
||||
response = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{"metadata": {"schemaVersion": "v6"}, "data": data},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
body = response.json()
|
||||
assert body["status"] == "success"
|
||||
assert body["data"]["info"]["data"]["display"]["name"] == data["display"]["name"]
|
||||
|
||||
|
||||
def test_tag_casing_is_inherited_from_existing_parent(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""A second dashboard tagged with a sibling under a casing-variant parent
|
||||
path should adopt the existing parent's casing while keeping the
|
||||
user-supplied casing for the new leaf segment."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
first = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {"display": {"name": "dac"}},
|
||||
"tags": [{"name": "engineering/US/NYC"}],
|
||||
},
|
||||
)
|
||||
assert first.status_code == HTTPStatus.CREATED
|
||||
first_tags = first.json()["data"]["info"]["tags"]
|
||||
assert first_tags == [{"name": "engineering/US/NYC"}]
|
||||
|
||||
second = _post_dashboard(
|
||||
signoz,
|
||||
admin_token,
|
||||
{
|
||||
"metadata": {"schemaVersion": "v6"},
|
||||
"data": {"display": {"name": "dac"}},
|
||||
"tags": [{"name": "engineering/us/SF"}],
|
||||
},
|
||||
)
|
||||
assert second.status_code == HTTPStatus.CREATED
|
||||
second_tags = second.json()["data"]["info"]["tags"]
|
||||
assert second_tags == [{"name": "engineering/US/SF"}]
|
||||
192
tests/seeder/golden/otel-demo-logs-golden.jsonl
Normal file
192
tests/seeder/golden/otel-demo-logs-golden.jsonl
Normal file
@@ -0,0 +1,192 @@
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Database query timed out","severity":"ERROR","minutes_ago":270,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Slow response detected","severity":"WARN","minutes_ago":240,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Handled request","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache miss","severity":"WARN","minutes_ago":180,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Upstream call failed","severity":"ERROR","minutes_ago":120,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Connection established","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Background job completed","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Retrying upstream call","severity":"WARN","minutes_ago":30,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[adservice] Cache hit","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"logger.name":"adservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Upstream call failed","severity":"ERROR","minutes_ago":300,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Retrying upstream call","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Retrying upstream call","severity":"WARN","minutes_ago":120,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Handled request","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Connection established","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[cartservice] Handled request","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"logger.name":"cartservice.app"}}
|
||||
{"body":"[checkoutservice] Retrying upstream call","severity":"WARN","minutes_ago":360,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Retrying upstream call","severity":"WARN","minutes_ago":210,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Handled request","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Authorization failed","severity":"ERROR","minutes_ago":150,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache miss","severity":"WARN","minutes_ago":120,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Connection established","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[checkoutservice] Slow response detected","severity":"WARN","minutes_ago":15,"resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"logger.name":"checkoutservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Connection established","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache miss","severity":"WARN","minutes_ago":135,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Handled request","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[currencyservice] Cache hit","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"logger.name":"currencyservice.app"}}
|
||||
{"body":"[frontend] Slow response detected","severity":"WARN","minutes_ago":360,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Slow response detected","severity":"WARN","minutes_ago":225,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Cache miss","severity":"WARN","minutes_ago":165,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Authorization failed","severity":"ERROR","minutes_ago":150,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Cache hit","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Connection established","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[frontend] Background job completed","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"logger.name":"frontend.app"}}
|
||||
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Slow response detected","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":255,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":225,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Background job completed","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":195,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Retrying upstream call","severity":"WARN","minutes_ago":150,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Authorization failed","severity":"ERROR","minutes_ago":90,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Connection established","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Cache hit","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Handled request","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[paymentservice] Upstream call failed","severity":"ERROR","minutes_ago":15,"resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"logger.name":"paymentservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":360,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache miss","severity":"WARN","minutes_ago":270,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":225,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":135,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Handled request","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Connection established","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Cache hit","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[productcatalogservice] Background job completed","severity":"INFO","minutes_ago":15,"resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"logger.name":"productcatalogservice.app"}}
|
||||
{"body":"[shippingservice] Database query timed out","severity":"ERROR","minutes_ago":360,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":345,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":330,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":315,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":300,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":285,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":270,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":255,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":240,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache miss","severity":"WARN","minutes_ago":225,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":210,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":195,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":180,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":165,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":150,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Database query timed out","severity":"ERROR","minutes_ago":135,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":120,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":105,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":90,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Handled request","severity":"INFO","minutes_ago":75,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache hit","severity":"INFO","minutes_ago":60,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Connection established","severity":"INFO","minutes_ago":45,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Background job completed","severity":"INFO","minutes_ago":30,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
{"body":"[shippingservice] Cache miss","severity":"WARN","minutes_ago":15,"resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"logger.name":"shippingservice.app"}}
|
||||
6624
tests/seeder/golden/otel-demo-metrics-golden.jsonl
Normal file
6624
tests/seeder/golden/otel-demo-metrics-golden.jsonl
Normal file
File diff suppressed because it is too large
Load Diff
180
tests/seeder/golden/otel-demo-traces-golden.jsonl
Normal file
180
tests/seeder/golden/otel-demo-traces-golden.jsonl
Normal file
@@ -0,0 +1,180 @@
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":360,"duration_ms":380,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":330,"duration_ms":141,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":300,"duration_ms":419,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":270,"duration_ms":421,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":240,"duration_ms":245,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":210,"duration_ms":341,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":180,"duration_ms":403,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":150,"duration_ms":334,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":120,"duration_ms":402,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":90,"duration_ms":140,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":60,"duration_ms":350,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/get","kind":"SERVER","minutes_ago":30,"duration_ms":229,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"GET","http.route":"/ads/get","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":360,"duration_ms":421,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":330,"duration_ms":332,"status":"ERROR","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"500"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":300,"duration_ms":70,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":270,"duration_ms":464,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":240,"duration_ms":77,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":210,"duration_ms":187,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":180,"duration_ms":278,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":150,"duration_ms":191,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":120,"duration_ms":433,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":90,"duration_ms":191,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":60,"duration_ms":104,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"adservice /ads/list","kind":"SERVER","minutes_ago":30,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"adservice","deployment.environment":"production","k8s.namespace.name":"signoz-adservice"},"attributes":{"http.method":"POST","http.route":"/ads/list","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":360,"duration_ms":303,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":330,"duration_ms":492,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":300,"duration_ms":271,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":270,"duration_ms":114,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":240,"duration_ms":245,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":210,"duration_ms":171,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":180,"duration_ms":145,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":150,"duration_ms":190,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":120,"duration_ms":166,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":90,"duration_ms":391,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":60,"duration_ms":361,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/add","kind":"SERVER","minutes_ago":30,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/add","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":360,"duration_ms":173,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":330,"duration_ms":127,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":300,"duration_ms":342,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":270,"duration_ms":124,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":240,"duration_ms":499,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":210,"duration_ms":168,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":180,"duration_ms":292,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":150,"duration_ms":417,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":120,"duration_ms":107,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":90,"duration_ms":439,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":60,"duration_ms":441,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/get","kind":"SERVER","minutes_ago":30,"duration_ms":223,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"GET","http.route":"/cart/get","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":360,"duration_ms":420,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":330,"duration_ms":244,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":300,"duration_ms":485,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":270,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":240,"duration_ms":134,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":210,"duration_ms":420,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":180,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":150,"duration_ms":485,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":120,"duration_ms":80,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":90,"duration_ms":291,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":60,"duration_ms":158,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"cartservice /cart/empty","kind":"SERVER","minutes_ago":30,"duration_ms":56,"status":"OK","resource_attributes":{"service.name":"cartservice","deployment.environment":"production","k8s.namespace.name":"signoz-cartservice"},"attributes":{"http.method":"POST","http.route":"/cart/empty","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":360,"duration_ms":378,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":330,"duration_ms":82,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":300,"duration_ms":102,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":270,"duration_ms":249,"status":"ERROR","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"500"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":240,"duration_ms":215,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":210,"duration_ms":234,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":180,"duration_ms":301,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":150,"duration_ms":284,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":120,"duration_ms":290,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":90,"duration_ms":212,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":60,"duration_ms":400,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"checkoutservice /checkout","kind":"SERVER","minutes_ago":30,"duration_ms":339,"status":"OK","resource_attributes":{"service.name":"checkoutservice","deployment.environment":"production","k8s.namespace.name":"signoz-checkoutservice"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":360,"duration_ms":205,"status":"ERROR","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"500"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":330,"duration_ms":77,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":300,"duration_ms":217,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":270,"duration_ms":348,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":240,"duration_ms":351,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":210,"duration_ms":288,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":180,"duration_ms":83,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":150,"duration_ms":264,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":120,"duration_ms":111,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":90,"duration_ms":349,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":60,"duration_ms":464,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"currencyservice /currency/convert","kind":"SERVER","minutes_ago":30,"duration_ms":201,"status":"OK","resource_attributes":{"service.name":"currencyservice","deployment.environment":"production","k8s.namespace.name":"signoz-currencyservice"},"attributes":{"http.method":"POST","http.route":"/currency/convert","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":360,"duration_ms":333,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":330,"duration_ms":227,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":300,"duration_ms":487,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":270,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":240,"duration_ms":137,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":210,"duration_ms":215,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":180,"duration_ms":445,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":150,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":120,"duration_ms":254,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":90,"duration_ms":197,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":60,"duration_ms":52,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /","kind":"SERVER","minutes_ago":30,"duration_ms":221,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"GET","http.route":"/","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":360,"duration_ms":72,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":330,"duration_ms":335,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":300,"duration_ms":292,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":270,"duration_ms":286,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":240,"duration_ms":444,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":210,"duration_ms":183,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":180,"duration_ms":123,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":150,"duration_ms":337,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":120,"duration_ms":373,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":90,"duration_ms":248,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":60,"duration_ms":459,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /product","kind":"SERVER","minutes_ago":30,"duration_ms":90,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/product","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":360,"duration_ms":304,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":330,"duration_ms":427,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":300,"duration_ms":130,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":270,"duration_ms":152,"status":"ERROR","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"500"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":240,"duration_ms":163,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":210,"duration_ms":73,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":180,"duration_ms":177,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":150,"duration_ms":80,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":120,"duration_ms":440,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":90,"duration_ms":450,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":60,"duration_ms":481,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"frontend /checkout","kind":"SERVER","minutes_ago":30,"duration_ms":219,"status":"OK","resource_attributes":{"service.name":"frontend","deployment.environment":"production","k8s.namespace.name":"signoz-frontend"},"attributes":{"http.method":"POST","http.route":"/checkout","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":360,"duration_ms":381,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":330,"duration_ms":307,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":300,"duration_ms":351,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":270,"duration_ms":384,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":240,"duration_ms":273,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":210,"duration_ms":499,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":180,"duration_ms":80,"status":"ERROR","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"500"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":150,"duration_ms":186,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":120,"duration_ms":423,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":90,"duration_ms":121,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":60,"duration_ms":451,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"paymentservice /payment/charge","kind":"SERVER","minutes_ago":30,"duration_ms":402,"status":"OK","resource_attributes":{"service.name":"paymentservice","deployment.environment":"production","k8s.namespace.name":"signoz-paymentservice"},"attributes":{"http.method":"POST","http.route":"/payment/charge","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":360,"duration_ms":189,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":330,"duration_ms":229,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":300,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":270,"duration_ms":300,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":240,"duration_ms":439,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":210,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":180,"duration_ms":104,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":150,"duration_ms":336,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":120,"duration_ms":335,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":90,"duration_ms":430,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":60,"duration_ms":116,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/list","kind":"SERVER","minutes_ago":30,"duration_ms":75,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"POST","http.route":"/products/list","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":360,"duration_ms":314,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":330,"duration_ms":303,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":300,"duration_ms":174,"status":"ERROR","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"500"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":270,"duration_ms":238,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":240,"duration_ms":494,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":210,"duration_ms":394,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":180,"duration_ms":71,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":150,"duration_ms":222,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":120,"duration_ms":386,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":90,"duration_ms":227,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":60,"duration_ms":54,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"productcatalogservice /products/get","kind":"SERVER","minutes_ago":30,"duration_ms":456,"status":"OK","resource_attributes":{"service.name":"productcatalogservice","deployment.environment":"production","k8s.namespace.name":"signoz-productcatalogservice"},"attributes":{"http.method":"GET","http.route":"/products/get","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":360,"duration_ms":317,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":330,"duration_ms":111,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":300,"duration_ms":478,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":270,"duration_ms":75,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":240,"duration_ms":413,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":210,"duration_ms":217,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":180,"duration_ms":160,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":150,"duration_ms":170,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":120,"duration_ms":415,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":90,"duration_ms":448,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":60,"duration_ms":340,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/quote","kind":"SERVER","minutes_ago":30,"duration_ms":390,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/quote","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":360,"duration_ms":96,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":330,"duration_ms":414,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":300,"duration_ms":182,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":270,"duration_ms":116,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":240,"duration_ms":489,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":210,"duration_ms":130,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":180,"duration_ms":394,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":150,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":120,"duration_ms":159,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":90,"duration_ms":432,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":60,"duration_ms":87,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
{"name":"shippingservice /shipping/ship","kind":"SERVER","minutes_ago":30,"duration_ms":59,"status":"OK","resource_attributes":{"service.name":"shippingservice","deployment.environment":"production","k8s.namespace.name":"signoz-shippingservice"},"attributes":{"http.method":"POST","http.route":"/shipping/ship","http.status_code":"200"}}
|
||||
@@ -1,5 +1,12 @@
|
||||
"""HTTP seeder — wraps fixtures.{traces,logs,metrics} so Playwright specs
|
||||
can POST per-test telemetry (tagged `seeder=true`) and DELETE to clear."""
|
||||
"""HTTP seeder — single entrypoint for e2e/integration telemetry.
|
||||
|
||||
POST /telemetry/{metrics,logs,traces} insert into ClickHouse via
|
||||
fixtures.{metrics,logs,traces}. DELETE truncates the signal tables.
|
||||
|
||||
Parallel-safe: every seeded row is tagged `seeder=true`. Tests share
|
||||
the seeded baseline; per-test mutations live in their own dashboards.
|
||||
Only test_teardown should call DELETE — workers must finish first.
|
||||
"""
|
||||
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -10,11 +17,7 @@ import clickhouse_connect
|
||||
from fastapi import FastAPI, HTTPException, Response, status
|
||||
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.logs import (
|
||||
Logs,
|
||||
insert_logs_to_clickhouse,
|
||||
truncate_logs_tables,
|
||||
)
|
||||
from fixtures.logs import Logs, insert_logs_to_clickhouse, truncate_logs_tables
|
||||
from fixtures.metrics import (
|
||||
Metrics,
|
||||
insert_metrics_to_clickhouse,
|
||||
@@ -64,12 +67,19 @@ def _tag(item: dict[str, Any]) -> dict[str, Any]:
|
||||
return {**item, "resources": resources}
|
||||
|
||||
|
||||
# Metrics payload carries label dicts at the top level, not a `resources`
|
||||
# key — tagging goes on the `resource_attrs` wrapper that Metrics.from_dict
|
||||
# unpacks. Same effect, different key.
|
||||
def _tag_metrics(item: dict[str, Any]) -> dict[str, Any]:
|
||||
resource_attrs = {**(item.get("resource_attrs") or {}), **SEEDER_MARKER}
|
||||
return {**item, "resource_attrs": resource_attrs}
|
||||
# Accept OTLP-style `resource_attributes` / `attributes` or legacy
|
||||
# `resource_attrs` / `labels` interchangeably.
|
||||
resource_attrs = {
|
||||
**(item.get("resource_attrs") or {}),
|
||||
**(item.get("resource_attributes") or {}),
|
||||
**SEEDER_MARKER,
|
||||
}
|
||||
labels = {**(item.get("labels") or {}), **(item.get("attributes") or {})}
|
||||
out = {**item, "resource_attrs": resource_attrs, "labels": labels}
|
||||
out.pop("resource_attributes", None)
|
||||
out.pop("attributes", None)
|
||||
return out
|
||||
|
||||
|
||||
@app.post("/telemetry/traces", status_code=status.HTTP_201_CREATED)
|
||||
@@ -145,3 +155,17 @@ def delete_metrics() -> Response:
|
||||
except Exception as e:
|
||||
logger.exception("truncate failed")
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@app.post("/seed/golden", status_code=status.HTTP_200_OK)
|
||||
def seed_golden() -> dict[str, int]:
|
||||
"""Re-seed the golden dataset with timestamps rebased to `now`.
|
||||
Called by Playwright globalSetup before every test session so chart
|
||||
assertions land within default panel time windows."""
|
||||
from fixtures import seed_golden_dataset # noqa: PLC0415 — local import keeps cold-start fast
|
||||
|
||||
try:
|
||||
return seed_golden_dataset.seed("http://localhost:8080")
|
||||
except Exception as e:
|
||||
logger.exception("golden seed failed")
|
||||
raise HTTPException(500, str(e)) from e
|
||||
|
||||
Reference in New Issue
Block a user