Compare commits

..

8 Commits

Author SHA1 Message Date
vikrantgupta25
0d5c288dd6 feat(licensing): add resource authz to license endpoints 2026-08-26 16:32:14 +05:30
vikrantgupta25
35abee54b1 chore(licensing): regenerate frontend api clients 2026-08-26 16:17:32 +05:30
vikrantgupta25
6a21345203 fix(licensing): advertise api key auth on get active license 2026-08-26 16:11:22 +05:30
vikrantgupta25
9f128ee00b refactor(licensing): rename api interface to handler 2026-08-26 16:03:12 +05:30
vikrantgupta25
ad06c14557 refactor(licensing): rename licensing api wiring to licensing handler 2026-08-26 16:01:50 +05:30
vikrantgupta25
92734c0c2b chore(licensing): remove unused community licenses list stub 2026-08-26 15:58:53 +05:30
vikrantgupta25
4c752655f3 feat(licensing): serve license endpoints from apiserver 2026-08-26 15:56:21 +05:30
vikrantgupta25
76211b3233 feat(zeus): add api/v2/zeus/licenses endpoints 2026-08-26 14:00:39 +05:30
57 changed files with 1124 additions and 2072 deletions

View File

@@ -5641,6 +5641,15 @@ components:
- total
- endTimeBeforeRetention
type: object
LicensetypesGettableLicense:
additionalProperties: {}
nullable: true
type: object
LicensetypesPostableLicense:
properties:
key:
type: string
type: object
LlmpricingruletypesGettablePricingRules:
properties:
items:
@@ -8010,7 +8019,6 @@ components:
- logs
- metrics
- meter
- ai_observability
type: string
SavedviewtypesUpdatableSavedView:
properties:
@@ -22950,73 +22958,6 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v2/system/dashboards/{name}:
get:
deprecated: false
description: Returns a dashboard SigNoz ships and owns, addressed by its stable
definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards
are read-only and upgraded through releases. The dashboard's own `name` field
carries a reserved prefix that the path segment must not include.
operationId: GetSystemDashboard
parameters:
- in: path
name: name
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- dashboard:read
- tokenizer:
- dashboard:read
summary: Get system dashboard
tags:
- dashboard
/api/v2/user_roles:
post:
deprecated: false
@@ -24129,6 +24070,166 @@ paths:
summary: Put profile in Zeus for a deployment.
tags:
- zeus
/api/v3/licenses:
post:
deprecated: false
description: This endpoint validates the license key with upstream and activates
the license for the organization.
operationId: ActivateLicense
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LicensetypesPostableLicense'
responses:
"202":
description: Accepted
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:create
- tokenizer:
- license:create
summary: Activate a license.
tags:
- licenses
put:
deprecated: false
description: This endpoint refreshes the active license of the organization
from upstream.
operationId: RefreshLicense
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- license:update
- tokenizer:
- license:update
summary: Refresh the active license.
tags:
- licenses
/api/v3/licenses/active:
get:
deprecated: false
description: This endpoint gets the active license of the organization.
operationId: GetActiveLicense
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LicensetypesGettableLicense'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key: []
- tokenizer: []
summary: Get the active license.
tags:
- licenses
/api/v3/metrics/dashboards:
get:
deprecated: false

View File

@@ -276,10 +276,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) GetByNameV2(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetByNameV2(ctx, orgID, name)
}
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.MigrateV2(ctx, orgID, id)
}
@@ -288,10 +284,6 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
}
func (module *module) UpdateUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.UpdateUnsafeV2(ctx, orgID, id, updatedBy, updatable)
}
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)
}

View File

@@ -4,10 +4,10 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
@@ -42,7 +42,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
IntegrationsController: opts.IntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
LicensingHandler: licensing.NewHandler(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
}, config)
@@ -72,14 +72,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// base overrides
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingHandler.Checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingHandler.Portal)).Methods(http.MethodPost)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)

View File

@@ -46,8 +46,6 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
ListDashboardViews200,
ListDashboardsForUserV2200,
ListDashboardsForUserV2Params,
@@ -2113,108 +2111,6 @@ export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
return queryClient;
};
/**
* Returns a dashboard SigNoz ships and owns, addressed by its stable definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards are read-only and upgraded through releases. The dashboard's own `name` field carries a reserved prefix that the path segment must not include.
* @summary Get system dashboard
*/
export const getSystemDashboard = (
{ name }: GetSystemDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSystemDashboard200>({
url: `/api/v2/system/dashboards/${name}`,
method: 'GET',
signal,
});
};
export const getGetSystemDashboardQueryKey = ({
name,
}: GetSystemDashboardPathParameters) => {
return [`/api/v2/system/dashboards/${name}`] as const;
};
export const getGetSystemDashboardQueryOptions = <
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ name }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetSystemDashboardQueryKey({ name });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getSystemDashboard>>
> = ({ signal }) => getSystemDashboard({ name }, signal);
return {
queryKey,
queryFn,
enabled: !!name,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSystemDashboardQueryResult = NonNullable<
Awaited<ReturnType<typeof getSystemDashboard>>
>;
export type GetSystemDashboardQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get system dashboard
*/
export function useGetSystemDashboard<
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ name }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSystemDashboardQueryOptions({ name }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get system dashboard
*/
export const invalidateGetSystemDashboard = async (
queryClient: QueryClient,
{ name }: GetSystemDashboardPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSystemDashboardQueryKey({ name }) },
options,
);
return queryClient;
};
/**
* Same as ListDashboardsV2 but personalized for the calling user: each dashboard carries the caller's `pinned` state, and pinned dashboards float to the top of the requested ordering. Supports the same filter DSL, sort, order, and pagination.
* @summary List dashboards for the current user (v2)

View File

@@ -0,0 +1,268 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetActiveLicense200,
LicensetypesPostableLicenseDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint validates the license key with upstream and activates the license for the organization.
* @summary Activate a license.
*/
export const activateLicense = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicense'];
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 activateLicense>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicense(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicense>>
>;
export type ActivateLicenseMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Activate a license.
*/
export const useActivateLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseMutationOptions(options));
};
/**
* This endpoint refreshes the active license of the organization from upstream.
* @summary Refresh the active license.
*/
export const refreshLicense = (signal?: AbortSignal) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
void,
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
void,
TContext
> => {
const mutationKey = ['refreshLicense'];
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 refreshLicense>>,
void
> = () => {
return refreshLicense();
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicense>>
>;
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Refresh the active license.
*/
export const useRefreshLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
void,
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
void,
TContext
> => {
return useMutation(getRefreshLicenseMutationOptions(options));
};
/**
* This endpoint gets the active license of the organization.
* @summary Get the active license.
*/
export const getActiveLicense = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetActiveLicense200>({
url: `/api/v3/licenses/active`,
method: 'GET',
signal,
});
};
export const getGetActiveLicenseQueryKey = () => {
return [`/api/v3/licenses/active`] as const;
};
export const getGetActiveLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetActiveLicenseQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getActiveLicense>>> = ({
signal,
}) => getActiveLicense(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetActiveLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getActiveLicense>>
>;
export type GetActiveLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get the active license.
*/
export function useGetActiveLicense<
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetActiveLicenseQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get the active license.
*/
export const invalidateGetActiveLicense = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetActiveLicenseQueryKey() },
options,
);
return queryClient;
};

View File

@@ -7171,6 +7171,21 @@ export interface InframonitoringtypesVolumesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type LicensetypesGettableLicenseDTOAnyOf = { [key: string]: unknown };
/**
* @nullable
*/
export type LicensetypesGettableLicenseDTO =
LicensetypesGettableLicenseDTOAnyOf | null;
export interface LicensetypesPostableLicenseDTO {
/**
* @type string
*/
key?: string;
}
/**
* @nullable
*/
@@ -9021,7 +9036,6 @@ export enum SavedviewtypesSourceDTO {
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
ai_observability = 'ai_observability',
}
export interface SavedviewtypesSavedViewSpecDTO {
display?: SavedviewtypesDisplayDTO;
@@ -12250,17 +12264,6 @@ export type RotateSession200 = {
status: string;
};
export type GetSystemDashboardPathParameters = {
name: string;
};
export type GetSystemDashboard200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type CreateUserRole201 = {
data: TypesIdentifiableDTO;
/**
@@ -12411,6 +12414,14 @@ export type GetHosts200 = {
status: string;
};
export type GetActiveLicense200 = {
data: LicensetypesGettableLicenseDTO | null;
/**
* @type string
*/
status: string;
};
export type GetMetricDashboardsV2Params = {
/**
* @type string

View File

@@ -0,0 +1,84 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/gorilla/mux"
)
func (provider *provider) addLicensingRoutes(router *mux.Router) error {
if err := router.Handle("/api/v3/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Activate, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ActivateLicense",
Tags: []string{"licenses"},
Summary: "Activate a license.",
Description: "This endpoint validates the license key with upstream and activates the license for the organization.",
Request: new(licensetypes.PostableLicense),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusAccepted,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v3/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Refresh, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "RefreshLicense",
Tags: []string{"licenses"},
Summary: "Refresh the active license.",
Description: "This endpoint refreshes the active license of the organization from upstream.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v3/licenses/active", handler.New(provider.authzMiddleware.OpenAccess(provider.licensingHandler.GetActive), handler.OpenAPIDef{
ID: "GetActiveLicense",
Tags: []string{"licenses"},
Summary: "Get the active license.",
Description: "This endpoint gets the active license of the organization.",
Request: nil,
RequestContentType: "",
Response: new(licensetypes.GettableLicense),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes(nil),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -30,7 +31,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -68,6 +68,7 @@ type provider struct {
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
licensingHandler licensing.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
serviceAccountGetter serviceaccount.Getter
@@ -81,8 +82,6 @@ type provider struct {
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
systemDashboardModule systemdashboard.Module
systemDashboardHandler systemdashboard.Handler
}
func NewFactory(
@@ -108,6 +107,7 @@ func NewFactory(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -121,8 +121,6 @@ func NewFactory(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
systemDashboardModule systemdashboard.Module,
systemDashboardHandler systemdashboard.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -151,6 +149,7 @@ func NewFactory(
authzHandler,
rawDataExportHandler,
zeusHandler,
licensingHandler,
querierHandler,
serviceAccountHandler,
serviceAccountGetter,
@@ -164,8 +163,6 @@ func NewFactory(
rulerHandler,
statsHandler,
savedViewHandler,
systemDashboardModule,
systemDashboardHandler,
)
})
}
@@ -196,6 +193,7 @@ func newProvider(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -209,8 +207,6 @@ func newProvider(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
systemDashboardModule systemdashboard.Module,
systemDashboardHandler systemdashboard.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -240,6 +236,7 @@ func newProvider(
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
licensingHandler: licensingHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
serviceAccountGetter: serviceAccountGetter,
@@ -253,8 +250,6 @@ func newProvider(
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,
systemDashboardModule: systemDashboardModule,
systemDashboardHandler: systemDashboardHandler,
}
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
@@ -307,10 +302,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSystemDashboardRoutes(router); err != nil {
return err
}
if err := provider.addMetricsExplorerRoutes(router); err != nil {
return err
}
@@ -347,6 +338,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addLicensingRoutes(router); err != nil {
return err
}
if err := provider.addZeusRoutes(router); err != nil {
return err
}

View File

@@ -1,63 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"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 (provider *provider) addSystemDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/system/dashboards/{name}", handler.New(
provider.authzMiddleware.CheckResources(provider.systemDashboardHandler.Get, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetSystemDashboard",
Tags: []string{"dashboard"},
Summary: "Get system dashboard",
Description: "Returns a dashboard SigNoz ships and owns, addressed by its stable definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards are read-only and upgraded through releases. The dashboard's own `name` field carries a reserved prefix that the path segment must not include.",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettableDashboardV2),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDashboard.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceDashboard,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: provider.systemDashboardID(),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}
// systemDashboardID resolves the {name} path param to the dashboard's id. Authz
// tuples and audit records are written against ids, so the name has to be
// resolved before either runs.
func (provider *provider) systemDashboardID() coretypes.ResourceIDExtractor {
return coretypes.NewResourceIDExtractor(coretypes.PhaseRequest, func(ec coretypes.ExtractorContext) (string, error) {
ctx := ec.Request.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return "", err
}
id, err := provider.systemDashboardModule.ResolveID(ctx, valuer.MustNewUUID(claims.OrgID), mux.Vars(ec.Request)["name"])
if err != nil {
return "", err
}
return id.StringValue(), nil
})
}

View File

@@ -1,4 +1,4 @@
package httplicensing
package licensing
import (
"context"
@@ -8,21 +8,20 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type licensingAPI struct {
licensing licensing.Licensing
type handler struct {
licensing Licensing
}
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
return &licensingAPI{licensing: licensing}
func NewHandler(licensing Licensing) Handler {
return &handler{licensing: licensing}
}
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) Activate(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -45,7 +44,7 @@ func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
return
}
err = api.licensing.Activate(r.Context(), orgID, req.Key)
err = handler.licensing.Activate(r.Context(), orgID, req.Key)
if err != nil {
render.Error(rw, err)
return
@@ -54,7 +53,7 @@ func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusAccepted, nil)
}
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -70,7 +69,7 @@ func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
return
}
license, err := api.licensing.GetActive(r.Context(), orgID)
license, err := handler.licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
@@ -80,7 +79,7 @@ func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, gettableLicense)
}
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -96,7 +95,7 @@ func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
return
}
err = api.licensing.Refresh(r.Context(), orgID)
err = handler.licensing.Refresh(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
@@ -105,7 +104,7 @@ func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) Checkout(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -127,7 +126,7 @@ func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
return
}
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
gettableSubscription, err := handler.licensing.Checkout(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
@@ -136,7 +135,7 @@ func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, gettableSubscription)
}
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) Portal(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
@@ -158,7 +157,7 @@ func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
return
}
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
gettableSubscription, err := handler.licensing.Portal(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return

View File

@@ -37,7 +37,7 @@ type Licensing interface {
statsreporter.StatsCollector
}
type API interface {
type Handler interface {
Activate(http.ResponseWriter, *http.Request)
Refresh(http.ResponseWriter, *http.Request)
GetActive(http.ResponseWriter, *http.Request)

View File

@@ -1,35 +0,0 @@
package nooplicensing
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
)
type noopLicensingAPI struct{}
func NewLicenseAPI() licensing.API {
return &noopLicensingAPI{}
}
func (api *noopLicensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}
func (api *noopLicensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
}

View File

@@ -63,8 +63,6 @@ type Module interface {
GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
GetByNameV2(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error)
// MigrateV2 retries the v1→v2 migration on a dashboard still stored in the v1 schema.
MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
@@ -74,9 +72,6 @@ type Module interface {
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error)
// UpdateUnsafeV2 updates a dashboard bypassing the guards. Intended for internal system callers.
UpdateUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error)
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error
PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error)

View File

@@ -64,23 +64,6 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
return storableDashboard, nil
}
func (store *store) GetByName(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.StorableDashboard, error) {
storableDashboard := new(dashboardtypes.StorableDashboard)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableDashboard).
Where("name = ?", name).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with name %s doesn't exist", name)
}
return storableDashboard, nil
}
// ListForUser emits the joined dashboard ⨝ user_dashboard_preference query the
// spec calls for. Aliases:
//

View File

@@ -19,12 +19,9 @@ func (m *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy stri
return nil, err
}
dashboard, err := postable.NewDashboardV2(orgID, createdBy, source)
if err != nil {
return nil, err
}
dashboard := postable.NewDashboardV2(orgID, createdBy, source)
err = m.store.RunInTx(ctx, func(ctx context.Context) error {
err := m.store.RunInTx(ctx, func(ctx context.Context) error {
resolvedTags, err := m.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, dashboard.ID, postable.Tags)
if err != nil {
return err
@@ -123,20 +120,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
return storable.ToDashboardV2(tags)
}
func (module *module) GetByNameV2(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
storable, err := module.store.GetByName(ctx, orgID, name)
if err != nil {
return nil, err
}
tags, err := module.tagModule.ListForResource(ctx, orgID, coretypes.KindDashboard, storable.ID)
if err != nil {
return nil, err
}
return storable.ToDashboardV2(tags)
}
// MigrateV2 retries the v1→v2 migration on a dashboard still stored as v1 (one the
// bulk 103 migration skipped or failed). Idempotent: an already-v2 one is unchanged.
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
@@ -196,32 +179,13 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer
return nil, err
}
return module.updateV2(ctx, orgID, existing, updatedBy, updatable, existing.Update)
}
func (module *module) UpdateUnsafeV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
if err := updatable.Validate(); err != nil {
return nil, err
}
existing, err := module.GetV2(ctx, orgID, id)
if err != nil {
return nil, err
}
return module.updateV2(ctx, orgID, existing, updatedBy, updatable, existing.UpdateUnsafe)
}
// apply is existing.Update or existing.UpdateUnsafe, so the gated path keeps its
// in-transaction checks and only UpdateUnsafeV2 skips them.
func (module *module) updateV2(ctx context.Context, orgID valuer.UUID, existing *dashboardtypes.DashboardV2, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2, apply func(dashboardtypes.UpdatableDashboardV2, string, []*tagtypes.Tag) error) (*dashboardtypes.DashboardV2, error) {
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, existing.ID, updatable.Tags)
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
resolvedTags, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, updatable.Tags)
if err != nil {
return err
}
err = apply(updatable, updatedBy, resolvedTags)
err = existing.Update(updatable, updatedBy, resolvedTags)
if err != nil {
return err
}

View File

@@ -6,20 +6,18 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type setter struct {
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
systemDashboard systemdashboard.Module
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, systemDashboard systemdashboard.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, systemDashboard: systemDashboard}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
}
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
@@ -39,10 +37,6 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
return err
}
if err := module.systemDashboard.Reconcile(ctx, organization.ID); err != nil {
return err
}
return nil
}

View File

@@ -1,45 +0,0 @@
package implsystemdashboard
import (
"embed"
"io/fs"
"path"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
)
const definitionsRoot = "fs/definitions"
//go:embed fs/definitions/*.json
var definitionFiles embed.FS
// NewRegistry parses every embedded definition. Definitions are build-time assets
// validated by a test, so a failure here means the binary shipped broken JSON.
func NewRegistry() (systemdashboardtypes.Registry, error) {
entries, err := fs.ReadDir(definitionFiles, definitionsRoot)
if err != nil {
return systemdashboardtypes.Registry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read system dashboard definitions")
}
definitions := make([]systemdashboardtypes.Definition, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
file := path.Join(definitionsRoot, entry.Name())
raw, err := definitionFiles.ReadFile(file)
if err != nil {
return systemdashboardtypes.Registry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read %s", file)
}
definition, err := systemdashboardtypes.NewDefinition(raw)
if err != nil {
return systemdashboardtypes.Registry{}, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "couldn't parse %s", file)
}
definitions = append(definitions, definition)
}
return systemdashboardtypes.NewRegistry(definitions)
}

View File

@@ -1,20 +0,0 @@
package implsystemdashboard
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// A schema migration cannot ship without updating the definitions: parsing them
// runs the same validation a create goes through, at the current schemaVersion.
func TestEmbeddedDefinitionsParseAtCurrentSchemaVersion(t *testing.T) {
registry, err := NewRegistry()
require.NoError(t, err)
// The frontend addresses the overview dashboard by this name.
_, ok := registry.Get(dashboardtypes.SystemDashboardNamePrefix + "ai-o11y-overview")
assert.True(t, ok)
}

View File

@@ -1,17 +0,0 @@
{
"version": 1,
"definition": {
"schemaVersion": "v6",
"name": "signoz---ai-o11y-overview",
"tags": [],
"spec": {
"display": {
"name": "AI Observability Overview",
"description": "Overview of LLM traffic. Panels ship in an upcoming release."
},
"variables": [],
"panels": {},
"layouts": []
}
}
}

View File

@@ -1,48 +0,0 @@
package implsystemdashboard
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module systemdashboard.Module
}
func NewHandler(module systemdashboard.Module) systemdashboard.Handler {
return &handler{module: module}
}
func (handler *handler) Get(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
}
name := mux.Vars(r)["name"]
if name == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "name is missing in the path"))
return
}
systemDashboard, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), name)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, systemDashboard.ToGettableDashboardV2())
}

View File

@@ -1,151 +0,0 @@
package implsystemdashboard
import (
"context"
"log/slog"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
settings factory.ScopedProviderSettings
store systemdashboardtypes.Store
registry systemdashboardtypes.Registry
dashboardModule dashboard.Module
}
func NewModule(
providerSettings factory.ProviderSettings,
store systemdashboardtypes.Store,
registry systemdashboardtypes.Registry,
dashboardModule dashboard.Module,
) systemdashboard.Module {
return &module{
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"),
store: store,
registry: registry,
dashboardModule: dashboardModule,
}
}
func (module *module) Reconcile(ctx context.Context, orgID valuer.UUID) error {
for _, definition := range module.registry.List() {
if err := module.reconcile(ctx, orgID, definition); err != nil {
return err
}
}
return nil
}
func (module *module) reconcile(ctx context.Context, orgID valuer.UUID, definition systemdashboardtypes.Definition) error {
existing, err := module.dashboardModule.GetByNameV2(ctx, orgID, definition.Name())
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return err
}
return module.provision(ctx, orgID, definition)
}
// Anything but the provisioner in updated_by means a foreign write. Leave the
// row alone — never overwriting is the safe direction.
if existing.UpdatedBy != systemdashboardtypes.ProvisionerIdentity {
module.settings.Logger().WarnContext(ctx, "skipping system dashboard reconcile: last write was not by the provisioner", slog.String("name", definition.Name()), slog.String("org_id", orgID.StringValue()), slog.String("updated_by", existing.UpdatedBy))
return nil
}
state, err := module.store.Get(ctx, orgID, definition.Name())
if err != nil {
return err
}
// Only ever move forward: a downgrade must not rewrite the newer content.
if state.Version >= definition.Version {
return nil
}
return module.upgrade(ctx, orgID, existing.ID, definition)
}
// provision creates the dashboard and its state row in one transaction, so a
// system dashboard can never exist without the version it was provisioned at.
// A concurrent provisioner (another replica, or the org-creation hook racing the
// startup sweep) loses on the state row's unique (org_id, name) index and rolls back.
func (module *module) provision(ctx context.Context, orgID valuer.UUID, definition systemdashboardtypes.Definition) error {
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
created, err := module.dashboardModule.CreateV2(
ctx,
orgID,
systemdashboardtypes.ProvisionerIdentity,
valuer.UUID{},
dashboardtypes.SourceSystem,
definition.Dashboard,
)
if err != nil {
return err
}
return module.store.Create(ctx, systemdashboardtypes.NewStorableSystemDashboard(orgID, created.ID, definition.Name(), definition.Version))
})
if err != nil {
if errors.Ast(err, errors.TypeAlreadyExists) {
module.settings.Logger().DebugContext(ctx, "system dashboard already provisioned concurrently", slog.String("name", definition.Name()), slog.String("org_id", orgID.StringValue()))
return nil
}
return err
}
module.settings.Logger().InfoContext(ctx, "provisioned system dashboard", slog.String("name", definition.Name()), slog.Int("version", definition.Version), slog.String("org_id", orgID.StringValue()))
return nil
}
func (module *module) upgrade(ctx context.Context, orgID valuer.UUID, id valuer.UUID, definition systemdashboardtypes.Definition) error {
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
if _, err := module.dashboardModule.UpdateUnsafeV2(ctx, orgID, id, systemdashboardtypes.ProvisionerIdentity, definition.ToUpdatable()); err != nil {
return err
}
return module.store.UpdateVersion(ctx, orgID, definition.Name(), definition.Version)
})
if err != nil {
return err
}
module.settings.Logger().InfoContext(ctx, "upgraded system dashboard", slog.String("name", definition.Name()), slog.Int("version", definition.Version), slog.String("org_id", orgID.StringValue()))
return nil
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
return module.get(ctx, orgID, name)
}
func (module *module) ResolveID(ctx context.Context, orgID valuer.UUID, name string) (valuer.UUID, error) {
existing, err := module.get(ctx, orgID, name)
if err != nil {
return valuer.UUID{}, err
}
return existing.ID, nil
}
func (module *module) get(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
if strings.HasPrefix(name, dashboardtypes.SystemDashboardNamePrefix) {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "name must not carry the %q prefix", dashboardtypes.SystemDashboardNamePrefix)
}
existing, err := module.dashboardModule.GetByNameV2(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+name)
if err != nil {
return nil, err
}
if err := existing.ErrIfNotSystem(); err != nil {
return nil, err
}
return existing, nil
}

View File

@@ -1,209 +0,0 @@
package implsystemdashboard
import (
"context"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testDashboardName = "test-overview"
func newTestSQLStore(t *testing.T) sqlstore.SQLStore {
t.Helper()
store, err := sqlitesqlstore.New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
Provider: "sqlite",
Connection: sqlstore.ConnectionConfig{MaxOpenConns: 10},
Sqlite: sqlstore.SqliteConfig{
Path: filepath.Join(t.TempDir(), "test.db"),
Mode: "wal",
BusyTimeout: 5 * time.Second,
TransactionMode: "deferred",
},
})
require.NoError(t, err)
for _, model := range []any{
(*dashboardtypes.StorableDashboard)(nil),
(*tagtypes.Tag)(nil),
(*tagtypes.TagRelation)(nil),
(*systemdashboardtypes.StorableSystemDashboard)(nil),
} {
_, err := store.BunDB().NewCreateTable().Model(model).IfNotExists().Exec(context.Background())
require.NoError(t, err)
}
_, err = store.BunDB().Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_system_dashboard_org_name ON system_dashboard (org_id, name)`)
require.NoError(t, err)
return store
}
func newTestModule(t *testing.T, sqlStore sqlstore.SQLStore, definitions ...systemdashboardtypes.Definition) (*module, dashboard.Module) {
t.Helper()
providerSettings := factorytest.NewSettings()
dashboardModule := impldashboard.NewModule(
impldashboard.NewStore(sqlStore),
providerSettings,
analyticstest.New(),
nil,
queryparser.New(providerSettings),
impltag.NewModule(impltag.NewStore(sqlStore)),
)
registry, err := systemdashboardtypes.NewRegistry(definitions)
require.NoError(t, err)
return NewModule(providerSettings, NewStore(sqlStore), registry, dashboardModule).(*module), dashboardModule
}
func newTestDefinition(t *testing.T, version int, displayName string) systemdashboardtypes.Definition {
t.Helper()
raw := `{
"version": ` + strconv.Itoa(version) + `,
"definition": {
"schemaVersion": "` + dashboardtypes.SchemaVersion + `",
"name": "` + dashboardtypes.SystemDashboardNamePrefix + testDashboardName + `",
"tags": [],
"spec": {"display": {"name": "` + displayName + `"}, "variables": [], "panels": {}, "layouts": []}
}
}`
definition, err := systemdashboardtypes.NewDefinition([]byte(raw))
require.NoError(t, err)
return definition
}
func TestReconcileProvisionsThenUpgradesUntilTheRowIsModified(t *testing.T) {
ctx := context.Background()
sqlStore := newTestSQLStore(t)
orgID := valuer.GenerateUUID()
systemDashboardModule, _ := newTestModule(t, sqlStore, newTestDefinition(t, 1, "v1"))
require.NoError(t, systemDashboardModule.Reconcile(ctx, orgID))
provisioned, err := systemDashboardModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
assert.Equal(t, dashboardtypes.SourceSystem, provisioned.Source)
assert.Equal(t, systemdashboardtypes.ProvisionerIdentity, provisioned.CreatedBy)
assert.Equal(t, "v1", provisioned.Spec.Display.Name)
assert.Equal(t, 1, stateVersion(t, systemDashboardModule, ctx, orgID))
// Reconciling the same version again is a no-op.
require.NoError(t, systemDashboardModule.Reconcile(ctx, orgID))
unchanged, err := systemDashboardModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
assert.Equal(t, provisioned.UpdatedAt, unchanged.UpdatedAt)
// An unmodified copy is upgraded in place, keeping its id.
upgradingModule, dashboardModule := newTestModule(t, sqlStore, newTestDefinition(t, 2, "v2"))
require.NoError(t, upgradingModule.Reconcile(ctx, orgID))
upgraded, err := upgradingModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
assert.Equal(t, provisioned.ID, upgraded.ID)
assert.Equal(t, "v2", upgraded.Spec.Display.Name)
assert.Equal(t, 2, stateVersion(t, upgradingModule, ctx, orgID))
// Once anything but the provisioner writes the row, later releases leave it alone.
updatable := newTestDefinition(t, 2, "edited out of band").ToUpdatable()
_, err = dashboardModule.UpdateUnsafeV2(ctx, orgID, upgraded.ID, "user@signoz.io", updatable)
require.NoError(t, err)
shippingModule, _ := newTestModule(t, sqlStore, newTestDefinition(t, 3, "v3"))
require.NoError(t, shippingModule.Reconcile(ctx, orgID))
untouched, err := shippingModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
assert.Equal(t, "user@signoz.io", untouched.UpdatedBy)
assert.Equal(t, "edited out of band", untouched.Spec.Display.Name)
assert.Equal(t, 2, stateVersion(t, shippingModule, ctx, orgID))
}
func stateVersion(t *testing.T, module *module, ctx context.Context, orgID valuer.UUID) int {
t.Helper()
state, err := module.store.Get(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+testDashboardName)
require.NoError(t, err)
return state.Version
}
func TestSystemDashboardsAreImmutableToUsers(t *testing.T) {
ctx := context.Background()
sqlStore := newTestSQLStore(t)
orgID := valuer.GenerateUUID()
systemDashboardModule, dashboardModule := newTestModule(t, sqlStore, newTestDefinition(t, 1, "v1"))
require.NoError(t, systemDashboardModule.Reconcile(ctx, orgID))
provisioned, err := systemDashboardModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
_, err = dashboardModule.UpdateV2(ctx, orgID, provisioned.ID, "user@signoz.io", newTestDefinition(t, 1, "edited").ToUpdatable())
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be modified")
}
func TestReconcileDoesNotDowngrade(t *testing.T) {
ctx := context.Background()
sqlStore := newTestSQLStore(t)
orgID := valuer.GenerateUUID()
newerModule, _ := newTestModule(t, sqlStore, newTestDefinition(t, 3, "v3"))
require.NoError(t, newerModule.Reconcile(ctx, orgID))
olderModule, _ := newTestModule(t, sqlStore, newTestDefinition(t, 2, "v2"))
require.NoError(t, olderModule.Reconcile(ctx, orgID))
got, err := newerModule.Get(ctx, orgID, testDashboardName)
require.NoError(t, err)
assert.Equal(t, "v3", got.Spec.Display.Name)
assert.Equal(t, 3, stateVersion(t, newerModule, ctx, orgID))
}
func TestGetRejectsANonSystemDashboard(t *testing.T) {
ctx := context.Background()
sqlStore := newTestSQLStore(t)
orgID := valuer.GenerateUUID()
systemDashboardModule, dashboardModule := newTestModule(t, sqlStore)
var postable dashboardtypes.PostableDashboardV2
require.NoError(t, postable.UnmarshalJSON([]byte(`{
"schemaVersion": "`+dashboardtypes.SchemaVersion+`",
"name": "a-user-dashboard",
"tags": [],
"spec": {"display": {"name": "user"}, "variables": [], "panels": {}, "layouts": []}
}`)))
_, err := dashboardModule.CreateV2(ctx, orgID, "user@signoz.io", valuer.GenerateUUID(), dashboardtypes.SourceUser, postable)
require.NoError(t, err)
// The server-side prefix makes user names structurally unreachable here.
_, err = systemDashboardModule.Get(ctx, orgID, "a-user-dashboard")
require.Error(t, err)
_, err = systemDashboardModule.Get(ctx, orgID, dashboardtypes.SystemDashboardNamePrefix+testDashboardName)
require.Error(t, err)
assert.Contains(t, err.Error(), "must not carry")
}

View File

@@ -1,81 +0,0 @@
package implsystemdashboard
import (
"context"
"log/slog"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
)
const reconcileRetryInterval = 30 * time.Second
type service struct {
settings factory.ScopedProviderSettings
module systemdashboard.Module
orgGetter organization.Getter
stopC chan struct{}
healthyC chan struct{}
}
// NewService reconciles every org's system dashboards once at startup. Orgs
// created later are reconciled by the organization setter instead.
func NewService(providerSettings factory.ProviderSettings, module systemdashboard.Module, orgGetter organization.Getter) factory.Service {
return &service{
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"),
module: module,
orgGetter: orgGetter,
stopC: make(chan struct{}),
healthyC: make(chan struct{}),
}
}
func (service *service) Start(ctx context.Context) error {
ticker := time.NewTicker(reconcileRetryInterval)
defer ticker.Stop()
for {
err := service.reconcile(ctx)
if err == nil {
close(service.healthyC)
<-service.stopC
return nil
}
service.settings.Logger().WarnContext(ctx, "system dashboard reconciliation failed, retrying", errors.Attr(err))
select {
case <-service.stopC:
return nil
case <-ticker.C:
}
}
}
func (service *service) Healthy() <-chan struct{} {
return service.healthyC
}
func (service *service) Stop(_ context.Context) error {
close(service.stopC)
return nil
}
func (service *service) reconcile(ctx context.Context) error {
orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
}
for _, org := range orgs {
if err := service.module.Reconcile(ctx, org.ID); err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "couldn't reconcile system dashboards for org %s", org.ID.StringValue())
}
}
service.settings.Logger().InfoContext(ctx, "system dashboard reconciliation completed", slog.Int("orgs", len(orgs)))
return nil
}

View File

@@ -1,80 +0,0 @@
package implsystemdashboard
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) systemdashboardtypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, storable *systemdashboardtypes.StorableSystemDashboard) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(storable).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, systemdashboardtypes.ErrCodeSystemDashboardAlreadyProvisioned, "system dashboard %s is already provisioned", storable.Name)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, name string) (*systemdashboardtypes.StorableSystemDashboard, error) {
storable := new(systemdashboardtypes.StorableSystemDashboard)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("org_id = ?", orgID).
Where("name = ?", name).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, systemdashboardtypes.ErrCodeSystemDashboardNotFound, "system dashboard %s is not provisioned", name)
}
return storable, nil
}
func (store *store) UpdateVersion(ctx context.Context, orgID valuer.UUID, name string, version int) error {
result, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(new(systemdashboardtypes.StorableSystemDashboard)).
Set("version = ?", version).
Set("updated_at = ?", time.Now()).
Where("org_id = ?", orgID).
Where("name = ?", name).
Exec(ctx)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, systemdashboardtypes.ErrCodeSystemDashboardNotFound, "system dashboard %s is not provisioned", name)
}
return nil
}
func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return store.sqlstore.RunInTxCtx(ctx, nil, cb)
}

View File

@@ -1,28 +0,0 @@
package systemdashboard
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// Reconcile provisions the org's missing system dashboards and upgrades the
// unmodified ones to the shipped version. It never touches a dashboard whose
// row carries a foreign write and it never deletes.
Reconcile(ctx context.Context, orgID valuer.UUID) error
// Get addresses the dashboard by its bare definition name; the reserved
// prefix is a storage concern the API never exposes.
Get(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error)
// ResolveID maps a system dashboard's name to its id, so routes addressed by
// name can be authz-checked and audited against the id tuples carry.
ResolveID(ctx context.Context, orgID valuer.UUID, name string) (valuer.UUID, error)
}
type Handler interface {
Get(http.ResponseWriter, *http.Request)
}

View File

@@ -470,7 +470,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
continue
}
// Type is resolved now; validate aggregation compatibility against it.
if err := spec.Aggregations[i].ValidateForTypeAndTemporality(); err != nil {
if err := spec.Aggregations[i].ValidateForType(); err != nil {
return nil, nil, err
}
if reducedMetricsSet[spec.Aggregations[i].MetricName] {

View File

@@ -119,7 +119,7 @@ type APIHandler struct {
// Websocket connection upgrader
Upgrader *websocket.Upgrader
LicensingAPI licensing.API
LicensingHandler licensing.Handler
QueryParserAPI *queryparser.API
@@ -139,7 +139,7 @@ type APIHandlerOpts struct {
// Flux Interval
FluxInterval time.Duration
LicensingAPI licensing.API
LicensingHandler licensing.Handler
QueryParserAPI *queryparser.API
@@ -176,7 +176,7 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
LogsParsingPipelineController: opts.LogsParsingPipelineController,
querier: querier,
querierV2: querierv2,
LicensingAPI: opts.LicensingAPI,
LicensingHandler: opts.LicensingHandler,
Signoz: opts.Signoz,
QueryParserAPI: opts.QueryParserAPI,
}
@@ -457,13 +457,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/register", am.OpenAccess(aH.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, []any{})
})).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
aH.LicensingAPI.Activate(rw, req)
})).Methods(http.MethodGet)
router.HandleFunc("/api/v1/span_percentile", am.ViewAccess(aH.Signoz.Handlers.SpanPercentile.GetSpanPercentileDetails)).Methods(http.MethodPost)
// Query Filter Analyzer api used to extract metric names and grouping columns from a query

View File

@@ -16,7 +16,7 @@ import (
"github.com/soheilhy/cmux"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@@ -84,7 +84,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
IntegrationsController: integrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
LicensingAPI: nooplicensing.NewLicenseAPI(),
LicensingHandler: licensing.NewHandler(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
}, config)

View File

@@ -46,8 +46,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/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
@@ -79,6 +77,7 @@ type Handlers struct {
AIObservability aiobservability.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
LicensingHandler licensing.Handler
QuerierHandler querier.Handler
ServiceAccountHandler serviceaccount.Handler
RegistryHandler factory.Handler
@@ -90,7 +89,6 @@ type Handlers struct {
RulerHandler ruler.Handler
LLMPricingRuleHandler llmpricingrule.Handler
StatsHandler statsreporter.Handler
SystemDashboard systemdashboard.Handler
}
func NewHandlers(
@@ -98,7 +96,7 @@ func NewHandlers(
providerSettings factory.ProviderSettings,
analytics analytics.Analytics,
querierHandler querier.Handler,
licensing licensing.Licensing,
licensingService licensing.Licensing,
global global.Global,
flaggerService flagger.Flagger,
gatewayService gateway.Gateway,
@@ -128,7 +126,8 @@ func NewHandlers(
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
AIObservability: implaiobservability.NewHandler(telemetryMetadataStore),
AuthzHandler: signozauthzapi.NewHandler(authz),
ZeusHandler: zeus.NewHandler(zeusService, licensing),
ZeusHandler: zeus.NewHandler(zeusService, licensingService),
LicensingHandler: licensing.NewHandler(licensingService),
QuerierHandler: querierHandler,
ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount, modules.ServiceAccountGetter),
RegistryHandler: registryHandler,
@@ -140,6 +139,5 @@ func NewHandlers(
RulerHandler: signozruler.NewHandler(rulerService),
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),
StatsHandler: statsreporter.NewHandler(statsAggregator),
SystemDashboard: implsystemdashboard.NewHandler(modules.SystemDashboard),
}
}

View File

@@ -59,7 +59,7 @@ func TestNewHandlers(t *testing.T) {
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, nil, retentionGetter, flagger, tagModule, nil, nil)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, nil, retentionGetter, flagger, tagModule, nil)
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)

View File

@@ -48,7 +48,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/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
@@ -68,36 +67,35 @@ import (
)
type Modules struct {
OrgGetter organization.Getter
OrgSetter organization.Setter
Preference preference.Module
UserSetter user.Setter
UserGetter user.Getter
RetentionGetter retention.Getter
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module
AuthDomain authdomain.Module
Session session.Module
Services services.Module
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
MetricReductionRule metricreductionrule.Module
InfraMonitoring inframonitoring.Module
OrgGetter organization.Getter
OrgSetter organization.Setter
Preference preference.Module
UserSetter user.Setter
UserGetter user.Getter
RetentionGetter retention.Getter
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module
AuthDomain authdomain.Module
Session session.Module
Services services.Module
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
MetricReductionRule metricreductionrule.Module
InfraMonitoring inframonitoring.Module
Promote promote.Module
ServiceAccount serviceaccount.Module
ServiceAccountGetter serviceaccount.Getter
CloudIntegration cloudintegration.Module
LogsPipeline logspipeline.Module
RuleStateHistory rulestatehistory.Module
TraceDetail tracedetail.Module
SpanMapper spanmapper.Module
LLMPricingRule llmpricingrule.Module
Tag tag.Module
SystemDashboard systemdashboard.Module
LogsPipeline logspipeline.Module
RuleStateHistory rulestatehistory.Module
TraceDetail tracedetail.Module
SpanMapper spanmapper.Module
LLMPricingRule llmpricingrule.Module
Tag tag.Module
}
func NewModules(
@@ -126,10 +124,9 @@ func NewModules(
fl flagger.Flagger,
tagModule tag.Module,
metricReductionRule metricreductionrule.Module,
systemDashboard systemdashboard.Module,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, systemDashboard)
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
// Cleanup callbacks from other modules, invoked when a user is deleted.
onDeleteUser := []user.OnDeleteUser{
dashboard.DeletePreferencesForUser,
@@ -139,35 +136,34 @@ func NewModules(
authDomainModule := implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs, authz)
return Modules{
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(implsavedview.NewStore(sqlstore)),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
UserSetter: userSetter,
UserGetter: userGetter,
RetentionGetter: retentionGetter,
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: authDomainModule,
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
MetricReductionRule: metricReductionRule,
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(implsavedview.NewStore(sqlstore)),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard,
UserSetter: userSetter,
UserGetter: userGetter,
RetentionGetter: retentionGetter,
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: authDomainModule,
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
MetricReductionRule: metricReductionRule,
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: serviceAccount,
ServiceAccountGetter: serviceAccountGetter,
LogsPipeline: impllogspipeline.NewModule(sqlstore),
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl, querier),
Tag: tagModule,
SystemDashboard: systemDashboard,
LogsPipeline: impllogspipeline.NewModule(sqlstore),
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl, querier),
Tag: tagModule,
}
}

View File

@@ -21,7 +21,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/systemdashboard/implsystemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -67,12 +66,7 @@ func TestNewModules(t *testing.T) {
retentionGetter := implretention.NewGetter(implretention.NewStore(sqlstore))
systemDashboardRegistry, err := implsystemdashboard.NewRegistry()
require.NoError(t, err)
systemDashboard := implsystemdashboard.NewModule(providerSettings, implsystemdashboard.NewStore(sqlstore), systemDashboardRegistry, dashboardModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule, implmetricreductionrule.NewModule(), systemDashboard)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule, implmetricreductionrule.NewModule())
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -35,7 +36,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -81,6 +81,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ authz.Handler }{},
struct{ rawdataexport.Handler }{},
struct{ zeus.Handler }{},
struct{ licensing.Handler }{},
struct{ querier.Handler }{},
struct{ serviceaccount.Handler }{},
struct{ serviceaccount.Getter }{},
@@ -94,8 +95,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ ruler.Handler }{},
struct{ statsreporter.Handler }{},
struct{ savedview.Handler }{},
struct{ systemdashboard.Module }{},
struct{ systemdashboard.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -244,7 +244,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewDeleteOrphanUserRolesFactory(),
sqlmigration.NewMigrateLambdaDashboardsFactory(),
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
sqlmigration.NewAddSystemDashboardFactory(sqlstore, sqlschema),
sqlmigration.NewAddLicenseTuplesFactory(sqlstore),
)
}
@@ -335,6 +335,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.AuthzHandler,
handlers.RawDataExport,
handlers.ZeusHandler,
handlers.LicensingHandler,
handlers.QuerierHandler,
handlers.ServiceAccountHandler,
modules.ServiceAccountGetter,
@@ -348,8 +349,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RulerHandler,
handlers.StatsHandler,
handlers.SavedView,
modules.SystemDashboard,
handlers.SystemDashboard,
),
)
}

View File

@@ -36,7 +36,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/systemdashboard/implsystemdashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -541,16 +540,8 @@ func New(
metricReductionRuleModule := metricReductionRuleModuleCallback(sqlstore, telemetrystore, dashboard, queryParser, licensing, flagger, telemetryMetadataStore, providerSettings, config.MetricsExplorer.TelemetryStore.Threads)
// Initialize the system dashboard module. The registry is parsed here so a
// malformed embedded definition fails startup instead of a request.
systemDashboardRegistry, err := implsystemdashboard.NewRegistry()
if err != nil {
return nil, err
}
systemDashboardModule := implsystemdashboard.NewModule(providerSettings, implsystemdashboard.NewStore(sqlstore), systemDashboardRegistry, dashboard)
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, cloudIntegrationModule, retentionGetter, flagger, tagModule, metricReductionRuleModule, systemDashboardModule)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, cloudIntegrationModule, retentionGetter, flagger, tagModule, metricReductionRuleModule)
// 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")
@@ -619,7 +610,6 @@ func New(
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
factory.NewNamedService(factory.MustNewName("meterreporter"), meterReporter, factory.MustNewName("licensing")),
factory.NewNamedService(factory.MustNewName("ruler"), rulerInstance),
factory.NewNamedService(factory.MustNewName("systemdashboard"), implsystemdashboard.NewService(providerSettings, systemDashboardModule, orgGetter)),
)
if err != nil {
return nil, err

View File

@@ -0,0 +1,159 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addLicenseTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddLicenseTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_license_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addLicenseTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addLicenseTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "metaresource", "license", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "delete"},
{authtypes.SigNozAdminRoleName, "metaresource", "license", "list"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
if err != nil {
return err
}
managedRoleGroups[roleName] = string(data)
}
for _, orgID := range orgIDs {
for roleName, data := range managedRoleGroups {
if _, err := tx.NewUpdate().
Model(new(roles)).
Set("transaction_groups = ?", data).
Where("org_id = ?", orgID).
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
Where("name = ?", roleName).
Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (migration *addLicenseTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -1,93 +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 addSystemDashboard struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddSystemDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("add_system_dashboard"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addSystemDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
},
)
}
func (migration *addSystemDashboard) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addSystemDashboard) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "system_dashboard",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "version", DataType: sqlschema.DataTypeBigInt, 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"),
},
{
ReferencingColumnName: sqlschema.ColumnName("dashboard_id"),
ReferencedTableName: sqlschema.TableName("dashboard"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
// (org_id, name) is what makes provisioning safe across replicas: the state
// row is written in the same transaction as the dashboard, so a losing racer
// rolls back its dashboard too.
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "system_dashboard",
ColumnNames: []sqlschema.ColumnName{"org_id", "name"},
},
)...)
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(
&sqlschema.UniqueIndex{
TableName: "system_dashboard",
ColumnNames: []sqlschema.ColumnName{"dashboard_id"},
},
)...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *addSystemDashboard) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -428,24 +428,20 @@ func (b *StatementBuilder) buildTemporalAggDeltaFastPath(
sb.SelectMore(fmt.Sprintf("`%s`", GroupByColumnAlias(i, g.Name)))
}
var aggCol string
aggCol, err := metricstelemetryschema.AggregationColumnForSamplesTable(
samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation,
)
if err != nil {
return "", nil, err
}
if query.Aggregations[0].TimeAggregation == metrictypes.TimeAggregationRate {
// TODO(srikanthccv): should it be step interval or use [start_time_unix_nano](https://github.com/open-telemetry/opentelemetry-proto/blob/d3fb76d70deb0874692bd0ebe03148580d85f3bb/opentelemetry/proto/metrics/v1/metrics.proto#L400C11-L400C31)?
aggCol = fmt.Sprintf("%s/%d", aggCol, stepSec)
}
if query.Aggregations[0].SpaceAggregation.IsPercentile() &&
query.Aggregations[0].Type == metrictypes.ExpHistogramType {
// merging sketches already spans every series in the step, so neither a
// samples-table value column nor the rate divisor applies
aggCol = fmt.Sprintf("quantilesDDMerge(0.01, %f)(sketch)[1]", query.Aggregations[0].SpaceAggregation.Percentile())
} else {
col, err := metricstelemetryschema.AggregationColumnForSamplesTable(
samplesTable, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation,
)
if err != nil {
return "", nil, err
}
aggCol = col
if query.Aggregations[0].TimeAggregation == metrictypes.TimeAggregationRate {
// TODO(srikanthccv): should it be step interval or use [start_time_unix_nano](https://github.com/open-telemetry/opentelemetry-proto/blob/d3fb76d70deb0874692bd0ebe03148580d85f3bb/opentelemetry/proto/metrics/v1/metrics.proto#L400C11-L400C31)?
aggCol = fmt.Sprintf("%s/%d", aggCol, stepSec)
}
}
sb.SelectMore(fmt.Sprintf("%s AS value", aggCol))

View File

@@ -126,64 +126,6 @@ func TestStatementBuilder(t *testing.T) {
},
expectedErr: nil,
},
{
name: "test_exp_histogram_percentile_delta",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Signal: telemetrytypes.SignalMetrics,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency",
Type: metrictypes.ExpHistogramType,
Temporality: metrictypes.Delta,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
},
},
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH __spatial_aggregation_cte AS (SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(30)) AS ts, `__GROUP_BY_KEY_0_service.name`, quantilesDDMerge(0.01, 0.950000)(sketch)[1] AS value FROM signoz_metrics.distributed_exp_hist AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'service.name') AS `__GROUP_BY_KEY_0_service.name` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) GROUP BY fingerprint, `__GROUP_BY_KEY_0_service.name`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY ts, `__GROUP_BY_KEY_0_service.name`) SELECT * FROM __spatial_aggregation_cte ORDER BY `__GROUP_BY_KEY_0_service.name`, ts",
Args: []any{"signoz_latency", uint64(1747936800000), uint64(1747983420000), "delta", "signoz_latency", uint64(1747947390000), uint64(1747983420000)},
},
expectedErr: nil,
},
{
// the sketch merge spans the whole step, so `rate` must not add a /step divisor
name: "test_exp_histogram_percentile_delta_rate_time_aggregation",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Signal: telemetrytypes.SignalMetrics,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_latency",
Type: metrictypes.ExpHistogramType,
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationPercentile95,
},
},
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH __spatial_aggregation_cte AS (SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(30)) AS ts, `__GROUP_BY_KEY_0_service.name`, quantilesDDMerge(0.01, 0.950000)(sketch)[1] AS value FROM signoz_metrics.distributed_exp_hist AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'service.name') AS `__GROUP_BY_KEY_0_service.name` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) GROUP BY fingerprint, `__GROUP_BY_KEY_0_service.name`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY ts, `__GROUP_BY_KEY_0_service.name`) SELECT * FROM __spatial_aggregation_cte ORDER BY `__GROUP_BY_KEY_0_service.name`, ts",
Args: []any{"signoz_latency", uint64(1747936800000), uint64(1747983420000), "delta", "signoz_latency", uint64(1747947390000), uint64(1747983420000)},
},
expectedErr: nil,
},
{
name: "test_histogram_percentile1",
requestType: qbtypes.RequestTypeTimeSeries,

View File

@@ -64,10 +64,10 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
// license — admin only.
// Uniform LCRUD shape; actual ee routes are POST /api/v3/licenses (create
// = Activate), PUT /api/v3/licenses (update = Refresh), GET
// /api/v3/licenses/active (read; currently exposed as ViewAccess on the
// route side). delete and list are placeholders for shape parity, no
// Uniform LCRUD shape; routes are POST /api/v3/licenses (create =
// Activate) and PUT /api/v3/licenses (update = Refresh). GET
// /api/v3/licenses/active is OpenAccess, so the read grant is not
// route-enforced. delete and list are placeholders for shape parity, no
// route serves them today.
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},

View File

@@ -25,10 +25,6 @@ const (
dashboardNameSuffixLen = 8
)
// SystemDashboardNamePrefix is reserved for dashboards SigNoz ships and owns. Generated
// names never contain consecutive hyphens, so only a typed name can carry it — create rejects that.
const SystemDashboardNamePrefix = "signoz---"
const (
dashboardIconPathPrefix = "/assets/Icons/"
dashboardLogoPathPrefix = "/assets/Logos/"
@@ -79,8 +75,8 @@ type DashboardV2 struct {
}
func (d *DashboardV2) ErrIfNotMutable() error {
if d.Source != SourceUser {
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "%s dashboards cannot be modified", d.Source)
if d.Source == SourceIntegration {
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "integration dashboards cannot be modified")
}
return nil
}
@@ -99,11 +95,6 @@ func (d *DashboardV2) Update(updatable UpdatableDashboardV2, updatedBy string, r
if err := d.ErrIfNotUpdatable(); err != nil {
return err
}
return d.UpdateUnsafe(updatable, updatedBy, resolvedTags)
}
// UpdateUnsafe applies the update without the source/lock gate. Intended for internal system callers.
func (d *DashboardV2) UpdateUnsafe(updatable UpdatableDashboardV2, updatedBy string, resolvedTags []*tagtypes.Tag) error {
if updatable.Name != d.Name {
return errors.NewInvalidInputf(ErrCodeDashboardImmutable, "name is immutable; cannot change from %q to %q", d.Name, updatable.Name)
}
@@ -138,13 +129,6 @@ func (d *DashboardV2) LockUnlock(lock bool, isAdmin bool, updatedBy string) erro
return nil
}
func (d *DashboardV2) ErrIfNotSystem() error {
if d.Source != SourceSystem {
return errors.Newf(errors.TypeNotFound, ErrCodeDashboardNotFound, "dashboard %q is not a system dashboard", d.Name)
}
return nil
}
func (d *DashboardV2) ErrIfNotClonable() error {
if !d.Source.isClonable() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardImmutable, "%s dashboards cannot be cloned", d.Source)
@@ -221,21 +205,13 @@ type PostableDashboardV2 struct {
Spec DashboardSpec `json:"spec" required:"true"`
}
func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy string, source Source) (*DashboardV2, error) {
func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy string, source Source) *DashboardV2 {
now := time.Now()
name := postable.Name
if postable.GenerateName {
name = generateDashboardName(postable.Spec.Display.Name)
}
// Checked on the final name, here rather than in validateName, because only
// the constructor knows the source.
if source != SourceSystem && strings.HasPrefix(name, SystemDashboardNamePrefix) {
return nil, errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "name %q is invalid: the %q prefix is reserved for system dashboards", name, SystemDashboardNamePrefix)
}
if source == SourceSystem && !strings.HasPrefix(name, SystemDashboardNamePrefix) {
return nil, errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "name %q is invalid: system dashboard names must start with the %q prefix", name, SystemDashboardNamePrefix)
}
return &DashboardV2{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
@@ -248,7 +224,7 @@ func (postable PostableDashboardV2) NewDashboardV2(orgID valuer.UUID, createdBy
Name: name,
Tags: tagtypes.NewTagsFromPostableTags(orgID, coretypes.KindDashboard, postable.Tags),
Spec: postable.Spec,
}, nil
}
}
func (p *PostableDashboardV2) UnmarshalJSON(data []byte) error {

View File

@@ -89,25 +89,21 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
cases := []struct {
scenario string
source Source
name string
expectedLocked bool
}{
{
scenario: "user source is not locked",
source: SourceUser,
name: "my-dashboard",
expectedLocked: false,
},
{
scenario: "system source is not locked",
source: SourceSystem,
name: SystemDashboardNamePrefix + "my-dashboard",
expectedLocked: false,
},
{
scenario: "integration source is locked",
source: SourceIntegration,
name: "my-dashboard",
expectedLocked: true,
},
}
@@ -119,7 +115,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
SchemaVersion: SchemaVersion,
Image: "img",
},
Name: tc.name,
Name: "my-dashboard",
Tags: []tagtypes.PostableTag{
{Key: "team", Value: "platform"},
{Key: "env", Value: "prod"},
@@ -128,8 +124,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
}
before := time.Now()
dashboard, err := postable.NewDashboardV2(orgID, "alice", tc.source)
require.NoError(t, err)
dashboard := postable.NewDashboardV2(orgID, "alice", tc.source)
after := time.Now()
require.NotNil(t, dashboard)
@@ -165,10 +160,8 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
Spec: DashboardSpec{},
}
first, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
require.NoError(t, err)
second, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
require.NoError(t, err)
first := postable.NewDashboardV2(orgID, "alice", SourceUser)
second := postable.NewDashboardV2(orgID, "alice", SourceUser)
assert.NotEqual(t, first.ID, second.ID, "expected distinct UUIDs across invocations")
})
@@ -181,8 +174,7 @@ func TestPostableDashboardV2NewDashboardV2(t *testing.T) {
},
}
dashboard, err := postable.NewDashboardV2(orgID, "alice", SourceUser)
require.NoError(t, err)
dashboard := postable.NewDashboardV2(orgID, "alice", SourceUser)
assert.True(t, strings.HasPrefix(dashboard.Name, "my-dashboard-"), "expected slug prefix, got %q", dashboard.Name)
assert.Len(t, dashboard.Name, len("my-dashboard-")+dashboardNameSuffixLen)
})

View File

@@ -109,8 +109,7 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
var p PostableDashboardV2
require.NoError(t, json.Unmarshal([]byte(basePostableJSON), &p), "base postable JSON must validate")
testOrgID := valuer.GenerateUUID()
base, err := p.NewDashboardV2(testOrgID, "somecreatedthisiguess@signoz.io", SourceUser)
require.NoError(t, err)
base := p.NewDashboardV2(testOrgID, "somecreatedthisiguess@signoz.io", SourceUser)
base.Tags = []*tagtypes.Tag{
{Key: "team", Value: "alpha"},
{Key: "env", Value: "prod"},

View File

@@ -8,7 +8,6 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/perses/spec/go/dashboard"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -1929,37 +1928,3 @@ func TestEnsureSingleExpressionAggregation(t *testing.T) {
})
}
}
// Guards the constant: a prefixed name must stay a valid DNS-1123 label.
func TestSystemDashboardNamePrefix(t *testing.T) {
require.NoError(t, validateDashboardName(SystemDashboardNamePrefix+"ai-o11y-overview"))
}
func TestNewDashboardV2RejectsReservedName(t *testing.T) {
testCases := []struct {
description string
name string
source Source
errContains string
}{
{description: "reserved name for a system dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceSystem},
{description: "reserved name for a user dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceUser, errContains: "reserved for system dashboards"},
{description: "reserved name for an integration dashboard", name: SystemDashboardNamePrefix + "overview", source: SourceIntegration, errContains: "reserved for system dashboards"},
{description: "unprefixed name for a system dashboard", name: "overview", source: SourceSystem, errContains: "must start with"},
{description: "ordinary name for a user dashboard", name: "overview", source: SourceUser},
{description: "fewer hyphens than the prefix for a user dashboard", name: "signoz--overview", source: SourceUser},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
postable := PostableDashboardV2{Name: testCase.name}
_, err := postable.NewDashboardV2(valuer.GenerateUUID(), "user@signoz.io", testCase.source)
if testCase.errContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), testCase.errContains)
return
}
require.NoError(t, err)
})
}
}

View File

@@ -13,9 +13,6 @@ type Store interface {
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error)
// GetByName resolves a dashboard by its per-org unique name.
GetByName(ctx context.Context, orgID valuer.UUID, name string) (*StorableDashboard, error)
GetPublic(context.Context, string) (*StorablePublicDashboard, error)
GetDashboardByOrgsAndPublicID(context.Context, []string, string) (*StorableDashboard, error)

View File

@@ -374,7 +374,7 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
return nil
}
func (m MetricAggregation) ValidateForTypeAndTemporality() error {
func (m MetricAggregation) ValidateForType() error {
if m.SpaceAggregation.IsPercentile() && !m.Type.IsPercentileSpaceAggregationAllowed() {
return errors.Newf(
errors.TypeInvalidInput,
@@ -384,17 +384,6 @@ func (m MetricAggregation) ValidateForTypeAndTemporality() error {
m.Type.StringValue(),
)
}
// reading a step's distribution out of a cumulative sketch would mean
// subtracting the previous point's sketch, which ClickHouse cannot do
if m.Type == metrictypes.ExpHistogramType && m.Temporality != metrictypes.Delta {
return errors.Newf(
errors.TypeUnsupported,
errors.CodeUnsupported,
"metric `%s` is an exponential histogram recorded with `%s` temporality, which cannot be queried; only `delta` exponential histograms are supported",
m.MetricName,
m.Temporality.StringValue(),
)
}
return nil
}

View File

@@ -1517,11 +1517,10 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
})
}
func TestMetricAggregationValidateForTypeAndTemporality(t *testing.T) {
func TestMetricAggregationValidateForType(t *testing.T) {
cases := []struct {
name string
metricType metrictypes.Type
temporality metrictypes.Temporality
spaceAggregation metrictypes.SpaceAggregation
comparisonParam *metrictypes.ComparisonSpaceAggregationParam
wantErr bool
@@ -1533,33 +1532,11 @@ func TestMetricAggregationValidateForTypeAndTemporality(t *testing.T) {
wantErr: false,
},
{
name: "percentile on delta exponential histogram is allowed",
name: "percentile on exponential histogram is allowed",
metricType: metrictypes.ExpHistogramType,
temporality: metrictypes.Delta,
spaceAggregation: metrictypes.SpaceAggregationPercentile99,
wantErr: false,
},
{
name: "cumulative exponential histogram is not allowed",
metricType: metrictypes.ExpHistogramType,
temporality: metrictypes.Cumulative,
spaceAggregation: metrictypes.SpaceAggregationPercentile99,
wantErr: true,
},
{
name: "exponential histogram with unresolved temporality is not allowed",
metricType: metrictypes.ExpHistogramType,
temporality: metrictypes.Unknown,
spaceAggregation: metrictypes.SpaceAggregationPercentile99,
wantErr: true,
},
{
name: "cumulative histogram is unaffected by the exponential histogram rule",
metricType: metrictypes.HistogramType,
temporality: metrictypes.Cumulative,
spaceAggregation: metrictypes.SpaceAggregationPercentile95,
wantErr: false,
},
{
name: "percentile on summary is not allowed",
metricType: metrictypes.SummaryType,
@@ -1585,11 +1562,10 @@ func TestMetricAggregationValidateForTypeAndTemporality(t *testing.T) {
agg := MetricAggregation{
MetricName: "test_metric",
Type: tc.metricType,
Temporality: tc.temporality,
SpaceAggregation: tc.spaceAggregation,
ComparisonSpaceAggregationParam: tc.comparisonParam,
}
err := agg.ValidateForTypeAndTemporality()
err := agg.ValidateForType()
if tc.wantErr && err == nil {
t.Errorf("expected error, got nil")
}

View File

@@ -23,11 +23,10 @@ var (
const savedViewNameSuffixLen = 8
var (
SourceTraces = Source{valuer.NewString("traces")}
SourceLogs = Source{valuer.NewString("logs")}
SourceMetrics = Source{valuer.NewString("metrics")}
SourceMeter = Source{valuer.NewString("meter")}
SourceAIObservability = Source{valuer.NewString("ai_observability")}
SourceTraces = Source{valuer.NewString("traces")}
SourceLogs = Source{valuer.NewString("logs")}
SourceMetrics = Source{valuer.NewString("metrics")}
SourceMeter = Source{valuer.NewString("meter")}
)
type SavedView struct {
@@ -118,13 +117,12 @@ func (Source) Enum() []any {
SourceLogs,
SourceMetrics,
SourceMeter,
SourceAIObservability,
}
}
func (s Source) Validate() error {
switch s {
case SourceTraces, SourceLogs, SourceMetrics, SourceMeter, SourceAIObservability:
case SourceTraces, SourceLogs, SourceMetrics, SourceMeter:
return nil
default:
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "invalid source: %s", s.StringValue())

View File

@@ -39,7 +39,6 @@ func TestSourceValidate(t *testing.T) {
{name: "logs", source: SourceLogs},
{name: "metrics", source: SourceMetrics},
{name: "meter", source: SourceMeter},
{name: "ai_observability", source: SourceAIObservability},
{name: "unknown is rejected", source: Source{valuer.NewString("bogus")}, expectError: true},
}

View File

@@ -173,21 +173,6 @@ func TestSavedViewSpecValidate(t *testing.T) {
},
expectError: false,
},
{
name: "builder_ai_query is valid",
spec: SavedViewSpec{
DisplayName: "My View",
PanelType: PanelTypeList,
RequestType: qbtypes.RequestTypeRaw,
Queries: []qbtypes.QueryEnvelope{{
Type: qbtypes.QueryTypeBuilderAI,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
},
}},
},
expectError: false,
},
{
name: "graph panel query with no aggregation is still rejected",
spec: SavedViewSpec{

View File

@@ -1,95 +0,0 @@
package systemdashboardtypes
import (
"bytes"
"encoding/json"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
)
// Definition is one shipped system dashboard. Version is bumped on every content
// change and drives upgrade detection; the name is the stable key and never changes.
type Definition struct {
Version int `json:"version"`
Dashboard dashboardtypes.PostableDashboardV2 `json:"definition"`
}
func (definition Definition) Name() string {
return definition.Dashboard.Name
}
func NewDefinition(raw []byte) (Definition, error) {
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
var definition Definition
if err := decoder.Decode(&definition); err != nil {
return Definition{}, errors.WrapInvalidInputf(err, ErrCodeSystemDashboardDefinitionInvalid, "%s", err.Error())
}
if err := definition.validate(); err != nil {
return Definition{}, err
}
return definition, nil
}
func (definition Definition) validate() error {
if definition.Version < 1 {
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "version must be at least 1, got %d", definition.Version)
}
if !strings.HasPrefix(definition.Name(), dashboardtypes.SystemDashboardNamePrefix) {
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "name %q must start with %q", definition.Name(), dashboardtypes.SystemDashboardNamePrefix)
}
if definition.Dashboard.GenerateName {
return errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "%s: generateName is not allowed, the name is the stable key", definition.Name())
}
return nil
}
// ToUpdatable is how an upgrade re-applies a definition onto an existing row:
// everything but the dashboard's identity comes from the shipped definition.
func (definition Definition) ToUpdatable() dashboardtypes.UpdatableDashboardV2 {
return dashboardtypes.UpdatableDashboardV2{
DashboardV2MetadataBase: definition.Dashboard.DashboardV2MetadataBase,
Name: definition.Dashboard.Name,
Tags: definition.Dashboard.Tags,
Spec: definition.Dashboard.Spec,
}
}
// Registry holds every definition embedded in the binary, keyed by name.
type Registry struct {
definitions map[string]Definition
}
func NewRegistry(definitions []Definition) (Registry, error) {
byName := make(map[string]Definition, len(definitions))
for _, definition := range definitions {
if _, duplicate := byName[definition.Name()]; duplicate {
return Registry{}, errors.NewInvalidInputf(ErrCodeSystemDashboardDefinitionInvalid, "duplicate system dashboard name %q", definition.Name())
}
byName[definition.Name()] = definition
}
return Registry{definitions: byName}, nil
}
func (registry Registry) Get(name string) (Definition, bool) {
definition, ok := registry.definitions[name]
return definition, ok
}
// List returns the definitions sorted by name so provisioning order is stable.
func (registry Registry) List() []Definition {
definitions := make([]Definition, 0, len(registry.definitions))
for _, definition := range registry.definitions {
definitions = append(definitions, definition)
}
slices.SortFunc(definitions, func(a, b Definition) int { return strings.Compare(a.Name(), b.Name()) })
return definitions
}

View File

@@ -1,58 +0,0 @@
package systemdashboardtypes
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
ErrCodeSystemDashboardNotFound = errors.MustNewCode("system_dashboard_not_found")
ErrCodeSystemDashboardDefinitionInvalid = errors.MustNewCode("system_dashboard_definition_invalid")
ErrCodeSystemDashboardAlreadyProvisioned = errors.MustNewCode("system_dashboard_already_provisioned")
)
// ProvisionerIdentity is stamped into created_by/updated_by by the reconciler. It
// is deliberately not a valid email, so it can never collide with a real account:
// any other value in updated_by means a foreign write.
const ProvisionerIdentity = "signoz"
type Store interface {
Create(ctx context.Context, storable *StorableSystemDashboard) error
Get(ctx context.Context, orgID valuer.UUID, name string) (*StorableSystemDashboard, error)
UpdateVersion(ctx context.Context, orgID valuer.UUID, name string, version int) error
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
}
// StorableSystemDashboard records the shipped version each org's copy of a system
// dashboard was last provisioned at. That version is the only thing the dashboard
// row cannot answer, since the binary only embeds the latest definition.
type StorableSystemDashboard struct {
bun.BaseModel `bun:"table:system_dashboard"`
types.Identifiable
types.TimeAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
DashboardID valuer.UUID `bun:"dashboard_id,type:text,notnull"`
Name string `bun:"name,type:text,notnull"`
Version int `bun:"version,notnull"`
}
func NewStorableSystemDashboard(orgID valuer.UUID, dashboardID valuer.UUID, name string, version int) *StorableSystemDashboard {
now := time.Now()
return &StorableSystemDashboard{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
OrgID: orgID,
DashboardID: dashboardID,
Name: name,
Version: version,
}
}

View File

@@ -453,3 +453,34 @@ def change_user_role(
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
def license_mapping(license_id: str, key: str, valid_from: int) -> Mapping:
return Mapping(
request=MappingRequest(
method=HttpMethods.GET,
url="/v2/licenses/me",
headers={"X-Signoz-Cloud-Api-Key": {WireMockMatchers.EQUAL_TO: key}},
),
response=MappingResponse(
status=200,
json_body={
"status": "success",
"data": {
"id": license_id,
"key": key,
"valid_from": valid_from,
"valid_until": -1,
"status": "VALID",
"state": "EVALUATING",
"plan": {
"name": "ENTERPRISE",
},
"platform": "CLOUD",
"features": [],
"event_queue": {},
},
},
),
persistent=False,
)

View File

@@ -132,15 +132,7 @@ class MetricsSample(ABC):
class MetricsExpHist(ABC):
"""Represents a row in the exp_hist table for exponential histograms.
`observations` must be non-empty; ClickHouse folds it into the `sketch`
AggregateFunction state on insert.
TODO: take an ExponentialHistogramDataPoint and compute the sketch bytes the
way the collector does, so the fixture exercises the real write path instead
of having ClickHouse build the state.
"""
"""Represents a row in the exp_hist table for exponential histograms."""
env: str
temporality: str
@@ -151,7 +143,7 @@ class MetricsExpHist(ABC):
sum: np.float64
min: np.float64
max: np.float64
observations: list[int]
sketch: bytes
flags: np.uint32
def __init__(
@@ -159,7 +151,11 @@ class MetricsExpHist(ABC):
metric_name: str,
fingerprint: np.uint64,
timestamp: datetime.datetime,
observations: list[int],
count: int,
sum_value: float,
min_value: float,
max_value: float,
sketch: bytes = b"",
temporality: str = "Unspecified",
env: str = "default",
flags: int = 0,
@@ -169,13 +165,28 @@ class MetricsExpHist(ABC):
self.metric_name = metric_name
self.fingerprint = fingerprint
self.unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
self.observations = observations
self.count = np.uint64(len(observations))
self.sum = np.float64(sum(observations))
self.min = np.float64(min(observations))
self.max = np.float64(max(observations))
self.count = np.uint64(count)
self.sum = np.float64(sum_value)
self.min = np.float64(min_value)
self.max = np.float64(max_value)
self.sketch = sketch
self.flags = np.uint32(flags)
def to_row(self) -> list:
return [
self.env,
self.temporality,
self.metric_name,
self.fingerprint,
self.unix_milli,
self.count,
self.sum,
self.min,
self.max,
self.sketch,
self.flags,
]
class MetricsMetadata(ABC):
"""Represents a row in the metadata table for metric metadata."""
@@ -418,73 +429,6 @@ class Metrics(ABC):
return metrics
class ExpHistogramMetrics(ABC):
"""High-level exponential histogram representation. Produces both time series
and exp_hist entries."""
metric_name: str
labels: dict[str, str]
temporality: str
timestamp: datetime.datetime
observations: list[int]
@property
def time_series(self) -> MetricsTimeSeries:
return self._time_series
@property
def exp_hist(self) -> MetricsExpHist:
return self._exp_hist
def __init__(
self,
metric_name: str,
observations: list[int],
labels: dict[str, str] = {},
timestamp: datetime.datetime | None = None,
temporality: str = "Delta",
flags: int = 0,
description: str = "",
unit: str = "",
env: str = "default",
resource_attributes: dict[str, str] = {},
scope_attributes: dict[str, str] = {},
) -> None:
if timestamp is None:
timestamp = datetime.datetime.now()
self.metric_name = metric_name
self.labels = labels
self.temporality = temporality
self.timestamp = timestamp
self.observations = observations
self._time_series = MetricsTimeSeries(
metric_name=metric_name,
labels=labels,
timestamp=timestamp,
temporality=temporality,
description=description,
unit=unit,
# the querier resolves the metric type from this column, and only an
# ExponentialHistogram here routes the query to the sketch read
type_="ExponentialHistogram",
is_monotonic=False,
env=env,
resource_attrs=resource_attributes,
scope_attrs=scope_attributes,
)
self._exp_hist = MetricsExpHist(
metric_name=metric_name,
fingerprint=self._time_series.fingerprint,
timestamp=timestamp,
observations=observations,
temporality=temporality,
env=env,
flags=flags,
)
class MetricsReducedTimeSeries(ABC):
"""Represents a row in the time_series_v4_reduced table i.e what
the time_series_v4_reduced_mv materializes for a metric under a
@@ -742,47 +686,6 @@ class MetricsBufferSample(ABC):
]
def insert_time_series_to_clickhouse(conn, time_series: list[MetricsTimeSeries]) -> None:
"""
Insert one distributed_time_series_v4 registration row per (series, hour
bucket), unix_milli floored to the hour — the exporter's exact shape.
Readers floor lookup windows to these buckets: skipping per-bucket
re-registration or keeping raw mid-hour timestamps hides series in ways
production never sees.
"""
time_series_map: dict[tuple[int, int], MetricsTimeSeries] = {}
for ts in time_series:
fp = int(ts.fingerprint)
hour_bucket = int(ts.unix_milli) // 3_600_000
if (fp, hour_bucket) not in time_series_map:
ts.unix_milli = np.int64(hour_bucket * 3_600_000)
time_series_map[(fp, hour_bucket)] = ts
if len(time_series_map) == 0:
return
conn.insert(
database="signoz_metrics",
table="distributed_time_series_v4",
column_names=[
"env",
"temporality",
"metric_name",
"description",
"unit",
"type",
"is_monotonic",
"fingerprint",
"unix_milli",
"labels",
"attrs",
"scope_attrs",
"resource_attrs",
],
data=[ts.to_row() for ts in time_series_map.values()],
)
def insert_metrics_to_clickhouse(conn, metrics: list[Metrics]) -> None:
"""
Insert metrics into ClickHouse tables.
@@ -794,7 +697,39 @@ def insert_metrics_to_clickhouse(conn, metrics: list[Metrics]) -> None:
Pure function so the seeder container can reuse the exact insert path
used by the pytest fixture. `conn` is a clickhouse-connect Client.
"""
insert_time_series_to_clickhouse(conn, [metric.time_series for metric in metrics])
# One registration row per (series, hour bucket), unix_milli floored to
# the hour — the exporter's exact shape. Readers floor lookup windows to
# these buckets: skipping per-bucket re-registration or keeping raw
# mid-hour timestamps hides series in ways production never sees.
time_series_map: dict[tuple[int, int], MetricsTimeSeries] = {}
for metric in metrics:
fp = int(metric.time_series.fingerprint)
hour_bucket = int(metric.time_series.unix_milli) // 3_600_000
if (fp, hour_bucket) not in time_series_map:
metric.time_series.unix_milli = np.int64(hour_bucket * 3_600_000)
time_series_map[(fp, hour_bucket)] = metric.time_series
if len(time_series_map) > 0:
conn.insert(
database="signoz_metrics",
table="distributed_time_series_v4",
column_names=[
"env",
"temporality",
"metric_name",
"description",
"unit",
"type",
"is_monotonic",
"fingerprint",
"unix_milli",
"labels",
"attrs",
"scope_attrs",
"resource_attrs",
],
data=[ts.to_row() for ts in time_series_map.values()],
)
samples = [metric.sample for metric in metrics]
if len(samples) > 0:
@@ -813,15 +748,6 @@ def insert_metrics_to_clickhouse(conn, metrics: list[Metrics]) -> None:
data=[sample.to_row() for sample in samples],
)
insert_metrics_metadata_to_clickhouse(conn, metrics)
def insert_metrics_metadata_to_clickhouse(conn, metrics: list) -> None:
"""
Insert the distributed_metadata rows describing each metric's point, resource
and scope attributes. Accepts anything exposing `time_series`, `labels` and
`timestamp`.
"""
# (metric_name, attr_type, attr_name, attr_value) -> MetricsMetadata
metadata_map: dict[tuple, MetricsMetadata] = {}
for metric in metrics:
@@ -927,61 +853,6 @@ def insert_metrics(
)
def insert_exp_histogram_metrics_to_clickhouse(conn, metrics: list[ExpHistogramMetrics]) -> None:
"""
Insert exponential histograms into ClickHouse tables.
Handles insertion into:
- distributed_time_series_v4 (time series metadata)
- distributed_exp_hist (per-point sketches)
- distributed_metadata (metric attribute metadata)
"""
insert_time_series_to_clickhouse(conn, [metric.time_series for metric in metrics])
# `sketch` is AggregateFunction(quantilesDD(...), UInt64) — the state has to be
# folded server-side, it cannot be sent as a literal. The quantilesDDState
# parameters must match the column's exactly or the INSERT is rejected.
for metric in metrics:
hist = metric.exp_hist
conn.command(
"INSERT INTO signoz_metrics.distributed_exp_hist "
"(env, temporality, metric_name, fingerprint, unix_milli, count, sum, min, max, sketch, flags) "
"SELECT %(env)s, %(temporality)s, %(metric_name)s, %(fingerprint)s, %(unix_milli)s, "
"%(count)s, %(sum)s, %(min)s, %(max)s, "
"quantilesDDState(0.01, 0.5, 0.75, 0.9, 0.95, 0.99)(toUInt64(observation)), %(flags)s "
"FROM (SELECT arrayJoin(%(observations)s) AS observation)",
parameters={
"env": hist.env,
"temporality": hist.temporality,
"metric_name": hist.metric_name,
"fingerprint": int(hist.fingerprint),
"unix_milli": int(hist.unix_milli),
"count": int(hist.count),
"sum": float(hist.sum),
"min": float(hist.min),
"max": float(hist.max),
"observations": hist.observations,
"flags": int(hist.flags),
},
)
insert_metrics_metadata_to_clickhouse(conn, metrics)
@pytest.fixture(name="insert_exp_histogram_metrics", scope="function")
def insert_exp_histogram_metrics(
clickhouse: types.TestContainerClickhouse,
) -> Generator[Callable[[list[ExpHistogramMetrics]], None], Any]:
def _insert_exp_histogram_metrics(metrics: list[ExpHistogramMetrics]) -> None:
insert_exp_histogram_metrics_to_clickhouse(clickhouse.conn, metrics)
yield _insert_exp_histogram_metrics
truncate_metrics_tables(
clickhouse.conn,
clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"],
)
def insert_reduced_metrics_to_clickhouse(
conn,
time_series: list[MetricsReducedTimeSeries],

View File

@@ -0,0 +1,186 @@
import http
from collections.abc import Callable
import requests
from wiremock.client import Mapping
from fixtures import types
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
change_user_role,
create_active_user,
license_mapping,
)
from fixtures.role import transaction_group
_EDITOR_EMAIL = "editor+licenseauthz@integration.test"
_EDITOR_PASSWORD = "password123Z$"
_VIEWER_EMAIL = "viewer+licenseauthz@integration.test"
_VIEWER_PASSWORD = "password123Z$"
_ACTOR_ROLE_NAME = "license-fga-actor"
_ACTOR_EMAIL = "customrole+licenseauthz@integration.test"
_ACTOR_PASSWORD = "password123Z$"
_LICENSE_ID = "0196360e-90cd-7a74-8313-1aa815ce2a69"
_LICENSE_KEY = "secret-key-authz"
def test_admin_can_activate_license(
signoz: types.SigNoz,
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
) -> None:
make_http_mocks(signoz.zeus, [license_mapping(_LICENSE_ID, _LICENSE_KEY, valid_from=1732146930)])
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
for email, role, password, name in (
(_EDITOR_EMAIL, "signoz-editor", _EDITOR_PASSWORD, "license authz editor"),
(_VIEWER_EMAIL, "signoz-viewer", _VIEWER_PASSWORD, "license authz viewer"),
):
create_active_user(signoz, admin_token, email=email, role=role, password=password, name=name)
response = requests.post(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
json={"key": _LICENSE_KEY},
headers={"Authorization": "Bearer " + admin_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.ACCEPTED, response.text
def test_editor_and_viewer_forbidden(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
) -> None:
for email, password in ((_EDITOR_EMAIL, _EDITOR_PASSWORD), (_VIEWER_EMAIL, _VIEWER_PASSWORD)):
token = get_token(email, password)
response = requests.post(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
json={"key": _LICENSE_KEY},
headers={"Authorization": "Bearer " + token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.FORBIDDEN, f"{email} activate: expected 403, got {response.status_code}: {response.text}"
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.FORBIDDEN, f"{email} refresh: expected 403, got {response.status_code}: {response.text}"
def test_all_roles_can_get_active_license(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
) -> None:
for email, password in (
(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD),
(_EDITOR_EMAIL, _EDITOR_PASSWORD),
(_VIEWER_EMAIL, _VIEWER_PASSWORD),
):
token = get_token(email, password)
response = requests.get(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses/active"),
headers={"Authorization": "Bearer " + token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.OK, f"{email} get active: expected 200, got {response.status_code}: {response.text}"
assert response.json()["data"]["key"] == _LICENSE_KEY
def test_admin_can_refresh_license(
signoz: types.SigNoz,
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
) -> None:
make_http_mocks(signoz.zeus, [license_mapping(_LICENSE_ID, _LICENSE_KEY, valid_from=1732146931)])
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + admin_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.NO_CONTENT, response.text
response = requests.get(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses/active"),
headers={"Authorization": "Bearer " + admin_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.OK, response.text
assert response.json()["data"]["valid_from"] == 1732146931
def test_custom_role_license_grant(
signoz: types.SigNoz,
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
create_role: Callable[..., str],
) -> None:
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = create_role(admin_token, _ACTOR_ROLE_NAME)
user_id = create_active_user(
signoz,
admin_token,
email=_ACTOR_EMAIL,
role="signoz-viewer",
password=_ACTOR_PASSWORD,
name="license authz custom role actor",
)
change_user_role(signoz, admin_token, user_id, "signoz-viewer", _ACTOR_ROLE_NAME)
actor_token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + actor_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.FORBIDDEN, f"refresh without grant: expected 403, got {response.status_code}: {response.text}"
response = requests.put(
url=signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={
"description": "",
"transactionGroups": [
transaction_group("update", "metaresource", "license", ["*"]),
],
},
headers={"Authorization": "Bearer " + admin_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.NO_CONTENT, response.text
make_http_mocks(signoz.zeus, [license_mapping(_LICENSE_ID, _LICENSE_KEY, valid_from=1732146932)])
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + actor_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.NO_CONTENT, f"refresh with grant: expected 204, got {response.status_code}: {response.text}"
response = requests.put(
url=signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={"description": "", "transactionGroups": []},
headers={"Authorization": "Bearer " + admin_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.NO_CONTENT, response.text
response = requests.put(
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
headers={"Authorization": "Bearer " + actor_token},
timeout=5,
)
assert response.status_code == http.HTTPStatus.FORBIDDEN, f"refresh after revoke: expected 403, got {response.status_code}: {response.text}"

View File

@@ -1,172 +0,0 @@
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
import pytest
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import ExpHistogramMetrics
from fixtures.querier import (
build_builder_query,
get_all_series,
get_all_warnings,
get_error_message,
get_series_values,
index_series_by_label,
make_query_request,
)
# quantilesDD carries 0.01 relative accuracy and the log-spaced observations put
# neighbouring ranks ~1.25% apart, so a percentile can land a few percent off
PERCENTILE_TOLERANCE = 0.05
@pytest.mark.parametrize(
"space_aggregation, frontend_first, frontend_last, backend_first, backend_last",
[
("p50", 118, 153, 711, 921),
("p95", 1108, 1435, 6651, 8613),
("p99", 1352, 1751, 8113, 10507),
],
)
@pytest.mark.parametrize("time_aggregation", ["", "rate"])
def test_exp_histogram_percentile_delta_grouped(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_exp_histogram_metrics: Callable[[list[ExpHistogramMetrics]], None],
time_aggregation: str,
space_aggregation: str,
frontend_first: float,
frontend_last: float,
backend_first: float,
backend_last: float,
) -> None:
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_exp_histogram_latency"
insert_exp_histogram_metrics(
[
ExpHistogramMetrics(
metric_name=metric_name,
# log-spaced latencies with a long tail, drifting ~30% higher across
# the hour so each point carries a distinct distribution
observations=[round(base * 1.0125**rank * (1 + minute / 200)) for rank in range(400)],
labels={"service.name": service},
timestamp=now - timedelta(minutes=60 - minute),
temporality="Delta",
)
for service, base in (("frontend", 10), ("backend", 60))
for minute in range(60)
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
time_aggregation,
space_aggregation,
temporality="delta",
group_by=["service.name"],
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()
assert get_all_warnings(data) == [], f"unexpected warnings: {get_all_warnings(data)}"
series_by_service = index_series_by_label(get_all_series(data, "A"), "service.name")
assert set(series_by_service.keys()) == {"frontend", "backend"}, f"got series {set(series_by_service.keys())}"
for service, first, last in (
("frontend", frontend_first, frontend_last),
("backend", backend_first, backend_last),
):
values = [point["value"] for point in sorted(series_by_service[service]["values"], key=lambda point: point["timestamp"])]
assert len(values) == 60, f"{service}: expected a point per minute, got {len(values)}"
assert values[0] == pytest.approx(first, rel=PERCENTILE_TOLERANCE), f"{service} {space_aggregation} at the oldest point: got {values[0]}, want ~{first}"
assert values[-1] == pytest.approx(last, rel=PERCENTILE_TOLERANCE), f"{service} {space_aggregation} at the newest point: got {values[-1]}, want ~{last}"
# every observation drifts up minute over minute, so the sketch must too
assert values == sorted(values), f"{service} {space_aggregation} is not non-decreasing: {values}"
def test_exp_histogram_percentile_delta_merges_across_series(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_exp_histogram_metrics: Callable[[list[ExpHistogramMetrics]], None],
) -> None:
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_exp_histogram_latency_merged"
insert_exp_histogram_metrics(
[
ExpHistogramMetrics(
metric_name=metric_name,
observations=[round(base * 1.0125**rank * (1 + minute / 200)) for rank in range(400)],
labels={"service.name": service},
timestamp=now - timedelta(minutes=60 - minute),
temporality="Delta",
)
for service, base in (("frontend", 10), ("backend", 60))
for minute in range(60)
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query("A", metric_name, "", "p95", temporality="delta")
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK, response.text
data = response.json()
assert get_all_warnings(data) == [], f"unexpected warnings: {get_all_warnings(data)}"
# both services' sketches merge into one, so p95 sits well above the frontend's
# own p95 (~1108) and below the backend's (~6651)
values = [point["value"] for point in sorted(get_series_values(data, "A"), key=lambda point: point["timestamp"])]
assert len(values) == 60, f"expected a point per minute, got {len(values)}"
assert values[0] == pytest.approx(5188, rel=PERCENTILE_TOLERANCE), f"oldest point: got {values[0]}, want ~5188"
assert values[-1] == pytest.approx(6718, rel=PERCENTILE_TOLERANCE), f"newest point: got {values[-1]}, want ~6718"
def test_exp_histogram_percentile_cumulative_is_rejected(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_exp_histogram_metrics: Callable[[list[ExpHistogramMetrics]], None],
) -> None:
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_exp_histogram_latency_cumulative"
insert_exp_histogram_metrics(
[
ExpHistogramMetrics(
metric_name=metric_name,
observations=[round(10 * 1.0125**rank * (1 + minute / 200)) for rank in range(400)],
labels={"service.name": "frontend"},
timestamp=now - timedelta(minutes=60 - minute),
temporality="Cumulative",
)
for minute in range(60)
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query("A", metric_name, "", "p95", temporality="cumulative")
response = make_query_request(signoz, token, start_ms, end_ms, [query])
# the request is well formed; it is the stored metric that cannot be read, so
# this is reported as unsupported rather than as bad input
assert response.status_code == HTTPStatus.NOT_IMPLEMENTED, response.text
assert "only `delta` exponential histograms are supported" in get_error_message(response.json()), response.text

View File

@@ -592,66 +592,6 @@ def test_saved_view_lifecycle(
assert response.status_code == HTTPStatus.NOT_FOUND
def test_ai_observability_view_with_builder_ai_query_roundtrip(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
"""builder_ai_query implies the traces signal -- the spec is sent without
one and must read back with signal pinned to "traces"."""
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(
signoz.self.host_configs["8080"].get(BASE_URL),
json={
"name": "ai-observability-overview",
"generateName": False,
"source": "ai_observability",
"schemaVersion": "v2",
"spec": {
"displayName": "ai-observability-overview",
"requestType": "scalar",
"queries": [{"type": "builder_ai_query", "spec": {"name": "A", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
"selectedFields": [],
"panelType": "table",
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
},
},
headers=headers,
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
view_id = response.json()["data"]["id"]
try:
response = requests.get(
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
headers=headers,
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
got = response.json()["data"]
assert got["source"] == "ai_observability"
assert got["spec"]["queries"][0]["type"] == "builder_ai_query"
assert got["spec"]["queries"][0]["spec"]["signal"] == "traces"
response = requests.get(
signoz.self.host_configs["8080"].get(BASE_URL),
params={"source": "ai_observability"},
headers=headers,
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
assert {v["name"] for v in response.json()["data"]} == {"ai-observability-overview"}
finally:
requests.delete(
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
headers=headers,
timeout=5,
)
def test_empty_name_derives_a_slug_from_display_name(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument