mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-31 16:40:44 +01:00
Compare commits
8 Commits
main
...
feat/licen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c0dce31ed | ||
|
|
f65c27c236 | ||
|
|
405e92cd06 | ||
|
|
ce3966b8c4 | ||
|
|
f2ce56de35 | ||
|
|
5e06339afe | ||
|
|
32f9254035 | ||
|
|
ea7fb4cdaa |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -58,7 +58,6 @@ jobs:
|
||||
- querierai
|
||||
- rawexportdata
|
||||
- promqlconformance
|
||||
- promapiconformance
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
|
||||
1016
docs/api/openapi.yml
1016
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -299,11 +299,8 @@ substituted. One subtlety makes it exact: we write stale markers at absent
|
||||
grid points. Without them, the engine's lookback would resurrect a point
|
||||
from up to `lookback` earlier. The marker encodes "absent here" the way the
|
||||
engine itself encodes it. Units evaluate concurrently. Each unit is one
|
||||
grid statement: the group-key join resolves the matchers, and the samples
|
||||
primary key takes the metric name straight from the selector. Only a
|
||||
selector without a static `__name__` runs the series lookup first, to learn
|
||||
the concrete metric names. A step of 0 is an instant query: a single
|
||||
evaluation at `end`.
|
||||
series lookup plus one grid statement. A step of 0 is an instant query: a
|
||||
single evaluation at `end`.
|
||||
|
||||
A note on the window sliver: when the window is narrower than the step, the
|
||||
grid windows cover only `window/step` of the timeline. A sample in a gap
|
||||
@@ -318,9 +315,8 @@ selectors and `last_over_time` transpile at window < step too.
|
||||
|
||||
## Series lookup
|
||||
|
||||
The engine path resolves matchers once per selector (`selectSeries`); the
|
||||
transpiled path builds the same conditions into its group-key join. Both
|
||||
read the same tables. The series tables hold one row per (fingerprint, bucket)
|
||||
Both paths resolve matchers the same way, once per selector
|
||||
(`selectSeries`). The series tables hold one row per (fingerprint, bucket)
|
||||
at 1h/6h/1d/1w granularities. The shared schema package
|
||||
(`pkg/telemetryschema/metricstelemetryschema`) picks the table whose bucket
|
||||
fits the window. It rounds the window start down to the bucket boundary, so
|
||||
|
||||
@@ -22,89 +22,6 @@ func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
|
||||
return &licensingAPI{licensing: licensing}
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(licensetypes.PostableLicense)
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.licensing.Activate(r.Context(), orgID, req.Key)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusAccepted, nil)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
license, err := api.licensing.GetActive(r.Context(), orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
|
||||
render.Success(rw, http.StatusOK, gettableLicense)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
err = api.licensing.Refresh(r.Context(), orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -96,12 +96,12 @@ func (provider *provider) Validate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
|
||||
data, err := provider.zeus.GetLicense(ctx, key)
|
||||
zeusLicense, err := provider.zeus.GetLicense(ctx, key)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
|
||||
}
|
||||
|
||||
license, err := licensetypes.NewLicense(data, organizationID)
|
||||
license, err := licensetypes.NewLicense(zeusLicense, organizationID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
|
||||
}
|
||||
@@ -115,6 +115,47 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
|
||||
storableLicense, err := provider.store.Get(ctx, organizationID, licenseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return licensetypes.NewLicenseFromStorableLicense(storableLicense)
|
||||
}
|
||||
|
||||
func (provider *provider) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
|
||||
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
licenses := make([]*licensetypes.License, 0, len(storableLicenses))
|
||||
for _, storableLicense := range storableLicenses {
|
||||
license, err := licensetypes.NewLicenseFromStorableLicense(storableLicense)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
licenses = append(licenses, license)
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
|
||||
license, err := provider.Get(ctx, organizationID, licenseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := license.ErrIfCloud(); err != nil {
|
||||
return errors.WithAdditionalf(err, "license %s cannot be deleted", licenseID.StringValue())
|
||||
}
|
||||
|
||||
return provider.store.Delete(ctx, organizationID, licenseID)
|
||||
}
|
||||
|
||||
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
|
||||
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
|
||||
if err != nil {
|
||||
@@ -139,7 +180,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
|
||||
zeusLicense, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
|
||||
if err != nil {
|
||||
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
|
||||
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
|
||||
@@ -154,7 +195,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return err
|
||||
}
|
||||
|
||||
err = activeLicense.Update(data)
|
||||
err = activeLicense.Update(zeusLicense)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
|
||||
}
|
||||
|
||||
@@ -64,6 +64,22 @@ func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*
|
||||
return storableLicenses, nil
|
||||
}
|
||||
|
||||
func (store *store) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewDelete().
|
||||
Model(new(licensetypes.StorableLicense)).
|
||||
Where("org_id = ?", organizationID).
|
||||
Where("id = ?", licenseID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete license with ID: %s", licenseID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
|
||||
@@ -76,11 +76,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
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)
|
||||
|
||||
// v4
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
|
||||
func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustypes.License, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/licenses/me"),
|
||||
@@ -63,7 +63,12 @@ func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
license := new(zeustypes.License)
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(response, "data").String()), license); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal license data")
|
||||
}
|
||||
|
||||
return license, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {
|
||||
|
||||
538
frontend/src/api/generated/services/licenses/index.ts
Normal file
538
frontend/src/api/generated/services/licenses/index.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* ! 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 {
|
||||
DeleteLicensePathParameters,
|
||||
GetActiveLicense200,
|
||||
GetLicense200,
|
||||
GetLicensePathParameters,
|
||||
LicensetypesPostableLicenseDTO,
|
||||
ListLicenses200,
|
||||
RefreshLicensePathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint lists all the licenses of the organization.
|
||||
* @summary List licenses.
|
||||
*/
|
||||
export const listLicenses = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListLicenses200>({
|
||||
url: `/api/v4/licenses`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListLicensesQueryKey = () => {
|
||||
return [`/api/v4/licenses`] as const;
|
||||
};
|
||||
|
||||
export const getListLicensesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listLicenses>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLicenses>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListLicensesQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listLicenses>>> = ({
|
||||
signal,
|
||||
}) => listLicenses(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLicenses>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListLicensesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listLicenses>>
|
||||
>;
|
||||
export type ListLicensesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List licenses.
|
||||
*/
|
||||
|
||||
export function useListLicenses<
|
||||
TData = Awaited<ReturnType<typeof listLicenses>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLicenses>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListLicensesQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List licenses.
|
||||
*/
|
||||
export const invalidateListLicenses = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListLicensesQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint validates the license key with the upstream server and activates the license for the organization.
|
||||
* @summary Activate a license.
|
||||
*/
|
||||
export const activateLicense = (
|
||||
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v4/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 deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.
|
||||
* @summary Delete a license.
|
||||
*/
|
||||
export const deleteLicense = (
|
||||
{ id }: DeleteLicensePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v4/licenses/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteLicenseMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLicense>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLicensePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLicense>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLicensePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteLicense'];
|
||||
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 deleteLicense>>,
|
||||
{ pathParams: DeleteLicensePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteLicense(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteLicenseMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteLicense>>
|
||||
>;
|
||||
|
||||
export type DeleteLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a license.
|
||||
*/
|
||||
export const useDeleteLicense = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLicense>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLicensePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteLicense>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLicensePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteLicenseMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint gets the license by id.
|
||||
* @summary Get a license.
|
||||
*/
|
||||
export const getLicense = (
|
||||
{ id }: GetLicensePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetLicense200>({
|
||||
url: `/api/v4/licenses/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetLicenseQueryKey = ({ id }: GetLicensePathParameters) => {
|
||||
return [`/api/v4/licenses/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetLicenseQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getLicense>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLicensePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLicense>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetLicenseQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLicense>>> = ({
|
||||
signal,
|
||||
}) => getLicense({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<Awaited<ReturnType<typeof getLicense>>, TError, TData> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetLicenseQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getLicense>>
|
||||
>;
|
||||
export type GetLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get a license.
|
||||
*/
|
||||
|
||||
export function useGetLicense<
|
||||
TData = Awaited<ReturnType<typeof getLicense>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLicensePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLicense>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetLicenseQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a license.
|
||||
*/
|
||||
export const invalidateGetLicense = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetLicensePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetLicenseQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint refreshes the active license of the organization from the upstream server.
|
||||
* @summary Refresh a license.
|
||||
*/
|
||||
export const refreshLicense = (
|
||||
{ id }: RefreshLicensePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v4/licenses/${id}`,
|
||||
method: 'PUT',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRefreshLicenseMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
{ pathParams: RefreshLicensePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
{ pathParams: RefreshLicensePathParameters },
|
||||
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>>,
|
||||
{ pathParams: RefreshLicensePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return refreshLicense(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RefreshLicenseMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof refreshLicense>>
|
||||
>;
|
||||
|
||||
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Refresh a license.
|
||||
*/
|
||||
export const useRefreshLicense = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
{ pathParams: RefreshLicensePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
{ pathParams: RefreshLicensePathParameters },
|
||||
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/v4/orgs/me/license`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetActiveLicenseQueryKey = () => {
|
||||
return [`/api/v4/orgs/me/license`] 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;
|
||||
};
|
||||
@@ -1,396 +0,0 @@
|
||||
/**
|
||||
* ! 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 {
|
||||
PrometheusErrorResponseSchemaDTO,
|
||||
PrometheusQueryParams,
|
||||
PrometheusQueryPostParams,
|
||||
PrometheusQueryRangeParams,
|
||||
PrometheusQueryRangePostParams,
|
||||
PrometheusSuccessResponseSchemaDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const prometheusQuery = (
|
||||
params: PrometheusQueryParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryQueryKey = (params?: PrometheusQueryParams) => {
|
||||
return [`/prometheus/api/v1/query`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getPrometheusQueryQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getPrometheusQueryQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof prometheusQuery>>> = ({
|
||||
signal,
|
||||
}) => prometheusQuery(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type PrometheusQueryQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>
|
||||
>;
|
||||
export type PrometheusQueryQueryError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
|
||||
export function usePrometheusQuery<
|
||||
TData = Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQuery>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getPrometheusQueryQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const invalidatePrometheusQuery = async (
|
||||
queryClient: QueryClient,
|
||||
params: PrometheusQueryParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getPrometheusQueryQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const prometheusQueryPost = (
|
||||
params: PrometheusQueryPostParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query`,
|
||||
method: 'POST',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryPostMutationOptions = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['prometheusQueryPost'];
|
||||
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 prometheusQueryPost>>,
|
||||
{ params: PrometheusQueryPostParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return prometheusQueryPost(params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PrometheusQueryPostMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>
|
||||
>;
|
||||
|
||||
export type PrometheusQueryPostMutationError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus instant query
|
||||
*/
|
||||
export const usePrometheusQueryPost = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof prometheusQueryPost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryPostParams },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPrometheusQueryPostMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const prometheusQueryRange = (
|
||||
params: PrometheusQueryRangeParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query_range`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangeQueryKey = (
|
||||
params?: PrometheusQueryRangeParams,
|
||||
) => {
|
||||
return [
|
||||
`/prometheus/api/v1/query_range`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangeQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getPrometheusQueryRangeQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>
|
||||
> = ({ signal }) => prometheusQueryRange(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangeQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>
|
||||
>;
|
||||
export type PrometheusQueryRangeQueryError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
|
||||
export function usePrometheusQueryRange<
|
||||
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
>(
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRange>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getPrometheusQueryRangeQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const invalidatePrometheusQueryRange = async (
|
||||
queryClient: QueryClient,
|
||||
params: PrometheusQueryRangeParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getPrometheusQueryRangeQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const prometheusQueryRangePost = (
|
||||
params: PrometheusQueryRangePostParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
|
||||
url: `/prometheus/api/v1/query_range`,
|
||||
method: 'POST',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPrometheusQueryRangePostMutationOptions = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['prometheusQueryRangePost'];
|
||||
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 prometheusQueryRangePost>>,
|
||||
{ params: PrometheusQueryRangePostParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return prometheusQueryRangePost(params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangePostMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>
|
||||
>;
|
||||
|
||||
export type PrometheusQueryRangePostMutationError = ErrorType<
|
||||
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
|
||||
>;
|
||||
|
||||
/**
|
||||
* @summary Prometheus range query
|
||||
*/
|
||||
export const usePrometheusQueryRangePost = <
|
||||
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
|
||||
TError,
|
||||
{ params: PrometheusQueryRangePostParams },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPrometheusQueryRangePostMutationOptions(options));
|
||||
};
|
||||
@@ -7173,6 +7173,197 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface LicensetypesFeatureDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
active?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
route?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
usage?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
usage_limit?: number;
|
||||
}
|
||||
|
||||
export interface LicensetypesLicenseEventQueueDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
event: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
scheduledAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LicensetypesLicensePlanDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isActive: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LicensetypesGettableLicenseDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
eventQueue: LicensetypesLicenseEventQueueDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
features: LicensetypesFeatureDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
freeUntil: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
plan: LicensetypesLicensePlanDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
platform: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
state: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
validFrom: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
validUntil: number;
|
||||
}
|
||||
|
||||
export interface LicensetypesGettableLicenseWithKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
eventQueue: LicensetypesLicenseEventQueueDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
features: LicensetypesFeatureDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
freeUntil: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
plan: LicensetypesLicensePlanDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
platform: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
state: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
validFrom: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
validUntil: number;
|
||||
}
|
||||
|
||||
export interface LicensetypesPostableLicenseDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -7974,164 +8165,6 @@ export interface PreferencetypesUpdatablePreferenceDTO {
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export enum PrometheusErrorResponseSchemaDTOErrorType {
|
||||
bad_data = 'bad_data',
|
||||
execution = 'execution',
|
||||
canceled = 'canceled',
|
||||
timeout = 'timeout',
|
||||
internal = 'internal',
|
||||
}
|
||||
export enum PrometheusErrorResponseSchemaDTOStatus {
|
||||
error = 'error',
|
||||
}
|
||||
export interface PrometheusErrorResponseSchemaDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* @enum bad_data,execution,canceled,timeout,internal
|
||||
* @type string
|
||||
*/
|
||||
errorType: PrometheusErrorResponseSchemaDTOErrorType;
|
||||
/**
|
||||
* @enum error
|
||||
* @type string
|
||||
*/
|
||||
status: PrometheusErrorResponseSchemaDTOStatus;
|
||||
}
|
||||
|
||||
export enum PrometheusMatrixDataSchemaDTOResultType {
|
||||
matrix = 'matrix',
|
||||
}
|
||||
export type PrometheusSamplePairSchemaDTOItem = number | string;
|
||||
|
||||
/**
|
||||
* A [timestamp, value] pair: float unix seconds, then the string-encoded sample value ("NaN", "+Inf", "-Inf" included).
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusSamplePairSchemaDTO =
|
||||
| PrometheusSamplePairSchemaDTOItem[]
|
||||
| null;
|
||||
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetricAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusMatrixSeriesSchemaDTOMetric =
|
||||
PrometheusMatrixSeriesSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusMatrixSeriesSchemaDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
metric: PrometheusMatrixSeriesSchemaDTOMetric;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
values: (PrometheusSamplePairSchemaDTO | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PrometheusMatrixDataSchemaDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
result: PrometheusMatrixSeriesSchemaDTO[] | null;
|
||||
/**
|
||||
* @enum matrix
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusMatrixDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export type PrometheusVectorSampleSchemaDTOMetricAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type PrometheusVectorSampleSchemaDTOMetric =
|
||||
PrometheusVectorSampleSchemaDTOMetricAnyOf | null;
|
||||
|
||||
export interface PrometheusVectorSampleSchemaDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
metric: PrometheusVectorSampleSchemaDTOMetric;
|
||||
value: PrometheusSamplePairSchemaDTO | null;
|
||||
}
|
||||
|
||||
export enum PrometheusVectorDataSchemaDTOResultType {
|
||||
vector = 'vector',
|
||||
}
|
||||
export interface PrometheusVectorDataSchemaDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
result: PrometheusVectorSampleSchemaDTO[] | null;
|
||||
/**
|
||||
* @enum vector
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusVectorDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export enum PrometheusScalarDataSchemaDTOResultType {
|
||||
scalar = 'scalar',
|
||||
}
|
||||
export interface PrometheusScalarDataSchemaDTO {
|
||||
result: PrometheusSamplePairSchemaDTO | null;
|
||||
/**
|
||||
* @enum scalar
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusScalarDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export enum PrometheusStringDataSchemaDTOResultType {
|
||||
string = 'string',
|
||||
}
|
||||
export interface PrometheusStringDataSchemaDTO {
|
||||
result: PrometheusSamplePairSchemaDTO | null;
|
||||
/**
|
||||
* @enum string
|
||||
* @type string
|
||||
*/
|
||||
resultType: PrometheusStringDataSchemaDTOResultType;
|
||||
}
|
||||
|
||||
export type PrometheusQueryDataSchemaDTO =
|
||||
| PrometheusMatrixDataSchemaDTO
|
||||
| PrometheusVectorDataSchemaDTO
|
||||
| PrometheusScalarDataSchemaDTO
|
||||
| PrometheusStringDataSchemaDTO;
|
||||
|
||||
export enum PrometheusSuccessResponseSchemaDTOStatus {
|
||||
success = 'success',
|
||||
}
|
||||
export interface PrometheusSuccessResponseSchemaDTO {
|
||||
data: PrometheusQueryDataSchemaDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
infos?: string[];
|
||||
/**
|
||||
* @enum success
|
||||
* @type string
|
||||
*/
|
||||
status: PrometheusSuccessResponseSchemaDTOStatus;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface PromotetypesWrappedIndexDTO {
|
||||
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
|
||||
/**
|
||||
@@ -12587,6 +12620,42 @@ export type GetFlamegraph200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListLicenses200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: LicensetypesGettableLicenseDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteLicensePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLicensePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLicense200 = {
|
||||
data: LicensetypesGettableLicenseWithKeyDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type RefreshLicensePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetActiveLicense200 = {
|
||||
data: LicensetypesGettableLicenseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallV4PathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
@@ -12629,115 +12698,3 @@ export type ReplaceVariables200 = {
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time.
|
||||
*/
|
||||
time?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryPostParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time.
|
||||
*/
|
||||
time?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangeParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range start: RFC3339 or float unix seconds.
|
||||
*/
|
||||
start: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range end: RFC3339 or float unix seconds.
|
||||
*/
|
||||
end: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Resolution step: duration string or float seconds.
|
||||
*/
|
||||
step: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
export type PrometheusQueryRangePostParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description PromQL expression.
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range start: RFC3339 or float unix seconds.
|
||||
*/
|
||||
start: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Range end: RFC3339 or float unix seconds.
|
||||
*/
|
||||
end: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Resolution step: duration string or float seconds.
|
||||
*/
|
||||
step: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Evaluation timeout: duration string or float seconds.
|
||||
*/
|
||||
timeout?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description Any non-empty value includes query statistics in the response.
|
||||
*/
|
||||
stats?: string;
|
||||
};
|
||||
|
||||
165
pkg/apiserver/signozapiserver/licensing.go
Normal file
165
pkg/apiserver/signozapiserver/licensing.go
Normal file
@@ -0,0 +1,165 @@
|
||||
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/v4/licenses", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Create, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ActivateLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Activate a license.",
|
||||
Description: "This endpoint validates the license key with the upstream server and activates the license for the organization.",
|
||||
Request: new(licensetypes.PostableLicense),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
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/v4/licenses", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.List, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListLicenses",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "List licenses.",
|
||||
Description: "This endpoint lists all the licenses of the organization.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*licensetypes.GettableLicense, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceLicense,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v4/orgs/me/license", 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, http.StatusNotImplemented},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes(nil),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Get, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Get a license.",
|
||||
Description: "This endpoint gets the license by id.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(licensetypes.GettableLicenseWithKey),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbRead)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceLicense,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Refresh, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "RefreshLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Refresh a license.",
|
||||
Description: "This endpoint refreshes the active license of the organization from the upstream server.",
|
||||
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,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Delete, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Delete a license.",
|
||||
Description: "This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.",
|
||||
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.VerbDelete)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceLicense,
|
||||
Verb: coretypes.VerbDelete,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
ID: coretypes.PathParam("id"),
|
||||
Selector: coretypes.IDSelector,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
openapi "github.com/swaggest/openapi-go"
|
||||
)
|
||||
|
||||
// prometheusOpenAPIHandler skips the default handler wrapper: that wraps
|
||||
// every response in the house envelope, and these endpoints follow
|
||||
// Prometheus' wire contract, described by the prometheus package's *Schema
|
||||
// types.
|
||||
type prometheusOpenAPIHandler struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
id string
|
||||
summary string
|
||||
params any
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
h.handlerFunc.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext) {
|
||||
// One route serves GET and POST; operation IDs must stay unique.
|
||||
id := h.id
|
||||
if strings.EqualFold(opCtx.Method(), http.MethodPost) {
|
||||
id += "Post"
|
||||
}
|
||||
opCtx.SetID(id)
|
||||
opCtx.SetTags("prometheus")
|
||||
opCtx.SetSummary(h.summary)
|
||||
opCtx.SetDescription("Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.")
|
||||
|
||||
for _, scheme := range newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}) {
|
||||
opCtx.AddSecurity(scheme.Name, scheme.Scopes...)
|
||||
}
|
||||
|
||||
opCtx.AddReqStructure(h.params)
|
||||
|
||||
opCtx.AddRespStructure(
|
||||
prometheus.SuccessResponseSchema{},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(http.StatusOK),
|
||||
)
|
||||
for _, statusCode := range []int{http.StatusBadRequest, http.StatusUnprocessableEntity, http.StatusServiceUnavailable, http.StatusInternalServerError} {
|
||||
opCtx.AddRespStructure(
|
||||
prometheus.ErrorResponseSchema{},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
// The auth middleware answers before the handler and uses the house
|
||||
// envelope, not Prometheus'.
|
||||
for _, statusCode := range []int{http.StatusUnauthorized, http.StatusForbidden} {
|
||||
opCtx.AddRespStructure(
|
||||
render.ErrorResponse{Status: render.StatusError.String(), Error: &errors.JSON{}},
|
||||
openapi.WithContentType("application/json"),
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *prometheusOpenAPIHandler) ResourceDefs() []handler.ResourceDef {
|
||||
return []handler.ResourceDef{handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.PromQLResources,
|
||||
}}
|
||||
}
|
||||
|
||||
func (provider *provider) addPrometheusRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/prometheus/api/v1/query", &prometheusOpenAPIHandler{
|
||||
handlerFunc: provider.authzMiddleware.CheckResources(provider.prometheusHandler.Query, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
id: "PrometheusQuery",
|
||||
summary: "Prometheus instant query",
|
||||
params: new(prometheus.QueryParamsSchema),
|
||||
}).Methods(http.MethodGet, http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/prometheus/api/v1/query_range", &prometheusOpenAPIHandler{
|
||||
handlerFunc: provider.authzMiddleware.CheckResources(provider.prometheusHandler.QueryRange, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
id: "PrometheusQueryRange",
|
||||
summary: "Prometheus range query",
|
||||
params: new(prometheus.QueryRangeParamsSchema),
|
||||
}).Methods(http.MethodGet, http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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"
|
||||
@@ -32,7 +33,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
@@ -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
|
||||
@@ -76,7 +77,6 @@ type provider struct {
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
spanMapperHandler spanmapper.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
prometheusHandler prometheus.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
@@ -107,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,
|
||||
@@ -115,7 +116,6 @@ func NewFactory(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
prometheusHandler prometheus.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -149,6 +149,7 @@ func NewFactory(
|
||||
authzHandler,
|
||||
rawDataExportHandler,
|
||||
zeusHandler,
|
||||
licensingHandler,
|
||||
querierHandler,
|
||||
serviceAccountHandler,
|
||||
serviceAccountGetter,
|
||||
@@ -157,7 +158,6 @@ func NewFactory(
|
||||
ruleStateHistoryHandler,
|
||||
spanMapperHandler,
|
||||
alertmanagerHandler,
|
||||
prometheusHandler,
|
||||
llmPricingRuleHandler,
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
@@ -193,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,
|
||||
@@ -201,7 +202,6 @@ func newProvider(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
prometheusHandler prometheus.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -236,6 +236,7 @@ func newProvider(
|
||||
authzHandler: authzHandler,
|
||||
rawDataExportHandler: rawDataExportHandler,
|
||||
zeusHandler: zeusHandler,
|
||||
licensingHandler: licensingHandler,
|
||||
querierHandler: querierHandler,
|
||||
serviceAccountHandler: serviceAccountHandler,
|
||||
serviceAccountGetter: serviceAccountGetter,
|
||||
@@ -244,7 +245,6 @@ func newProvider(
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
spanMapperHandler: spanMapperHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
prometheusHandler: prometheusHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
@@ -338,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
|
||||
}
|
||||
@@ -346,10 +350,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addPrometheusRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addServiceAccountRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
169
pkg/licensing/handler.go
Normal file
169
pkg/licensing/handler.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package licensing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
licensing Licensing
|
||||
}
|
||||
|
||||
func NewHandler(licensing Licensing) Handler {
|
||||
return &handler{licensing: licensing}
|
||||
}
|
||||
|
||||
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(licensetypes.PostableLicense)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.licensing.Activate(ctx, valuer.MustNewUUID(claims.OrgID), req.Key)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusAccepted, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
licenses, err := handler.licensing.List(ctx, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableLicenses := make([]*licensetypes.GettableLicense, 0, len(licenses))
|
||||
for _, license := range licenses {
|
||||
gettableLicenses = append(gettableLicenses, licensetypes.NewGettableLicense(license))
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, gettableLicenses)
|
||||
}
|
||||
|
||||
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
license, err := handler.licensing.Get(ctx, valuer.MustNewUUID(claims.OrgID), licenseID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, licensetypes.NewGettableLicenseWithKey(license))
|
||||
}
|
||||
|
||||
func (handler *handler) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
activeLicense, err := handler.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if activeLicense.ID != licenseID {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only the active license %s can be refreshed", activeLicense.ID.StringValue()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.licensing.Refresh(ctx, orgID); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
licenseID, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.licensing.Delete(ctx, valuer.MustNewUUID(claims.OrgID), licenseID); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
license, err := handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, licensetypes.NewGettableLicense(license))
|
||||
}
|
||||
@@ -21,10 +21,16 @@ type Licensing interface {
|
||||
|
||||
// Validate validates the license with the upstream server
|
||||
Validate(ctx context.Context) error
|
||||
// Activate validates and enables the license
|
||||
// Activate validates the key with the upstream server and enables the license
|
||||
Activate(ctx context.Context, organizationID valuer.UUID, key string) error
|
||||
// GetActive fetches the current active license in org
|
||||
GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error)
|
||||
// Get fetches the license by id in org
|
||||
Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error)
|
||||
// List fetches all the licenses in org
|
||||
List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error)
|
||||
// Delete deletes the license by id in org, cloud licenses cannot be deleted
|
||||
Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error
|
||||
// Refresh refreshes the license state from upstream server
|
||||
Refresh(ctx context.Context, organizationID valuer.UUID) error
|
||||
// Checkout creates a checkout session via upstream server and returns the redirection link
|
||||
@@ -38,10 +44,20 @@ type Licensing interface {
|
||||
}
|
||||
|
||||
type API interface {
|
||||
Activate(http.ResponseWriter, *http.Request)
|
||||
Refresh(http.ResponseWriter, *http.Request)
|
||||
GetActive(http.ResponseWriter, *http.Request)
|
||||
|
||||
Checkout(http.ResponseWriter, *http.Request)
|
||||
Portal(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Create(http.ResponseWriter, *http.Request)
|
||||
|
||||
List(http.ResponseWriter, *http.Request)
|
||||
|
||||
Get(http.ResponseWriter, *http.Request)
|
||||
|
||||
Refresh(http.ResponseWriter, *http.Request)
|
||||
|
||||
Delete(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetActive(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -14,18 +14,6 @@ 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"))
|
||||
}
|
||||
|
||||
@@ -39,6 +39,18 @@ func (provider *noopLicensing) Activate(ctx context.Context, organizationID valu
|
||||
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported")
|
||||
}
|
||||
|
||||
func (provider *noopLicensing) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported")
|
||||
}
|
||||
|
||||
func (provider *noopLicensing) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "listing licenses is not supported")
|
||||
}
|
||||
|
||||
func (provider *noopLicensing) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
|
||||
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "deleting license is not supported")
|
||||
}
|
||||
|
||||
func (provider *noopLicensing) Validate(ctx context.Context) error {
|
||||
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "validating license is not supported")
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ func (c *captureQuerier) LabelNames(context.Context, *storage.LabelHints, ...*la
|
||||
}
|
||||
|
||||
// metricNamesFromMatchers extracts the statically known metric name, if any.
|
||||
// Only a __name__ equality contributes; a regex selector needs a series
|
||||
// lookup to learn the concrete names.
|
||||
// The live path derives names from the matched series; the capture path has
|
||||
// no execution results, so only a __name__ equality contributes.
|
||||
func metricNamesFromMatchers(matchers []*labels.Matcher) []string {
|
||||
for _, m := range matchers {
|
||||
if m.Name == metricNameLabel && m.Type == labels.MatchEqual && m.Value != "" {
|
||||
|
||||
@@ -88,8 +88,7 @@ func (e *executor) TryExecuteRange(ctx context.Context, qs string, start, end ti
|
||||
}
|
||||
|
||||
// Evaluate every unit concurrently on its own grid (the query grid, or a
|
||||
// subquery grid); each is one grid query (see executeUnit for when a
|
||||
// series lookup precedes it).
|
||||
// subquery grid); each is one series lookup plus one grid query.
|
||||
results := make([][]transpiledSeries, len(plan.units))
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
for i, unit := range plan.units {
|
||||
@@ -143,27 +142,19 @@ func (e *executor) executeUnit(ctx context.Context, unit *coreUnit, grid gridCon
|
||||
dataStart := startMs - unit.offsetMs - windowMs
|
||||
dataEnd := endMs - unit.offsetMs
|
||||
|
||||
// The group-key join resolves the matchers on its own, so the unit
|
||||
// statement only needs concrete metric names for the samples
|
||||
// primary-key prefix. A selector without a static __name__ learns them
|
||||
// through the series lookup; every other selector skips the roundtrip.
|
||||
metricNames := metricNamesFromMatchers(unit.matchers)
|
||||
if metricNames == nil {
|
||||
seriesQuery, seriesArgs, err := buildSeriesQuery(dataStart, dataEnd, unit.matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookup, err := e.client.selectSeries(ctx, seriesQuery, seriesArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(lookup.fingerprints) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
metricNames = lookup.metricNames
|
||||
seriesQuery, seriesArgs, err := buildSeriesQuery(dataStart, dataEnd, unit.matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookup, err := e.client.selectSeries(ctx, seriesQuery, seriesArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(lookup.fingerprints) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
query, args, err := buildUnitSQL(unit, metricNames, dataStart, dataEnd, startMs, endMs, stepMs, e.client.lookbackMs)
|
||||
query, args, err := buildUnitSQL(unit, lookup.metricNames, dataStart, dataEnd, startMs, endMs, stepMs, e.client.lookbackMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,15 +27,9 @@ func newTestClient(t *testing.T) (*client, *telemetrystoretest.Provider) {
|
||||
return newClient(settings, store, prometheus.Config{}), store
|
||||
}
|
||||
|
||||
var unitCols = []cmock.ColumnType{
|
||||
{Name: "gkey", Type: "String"},
|
||||
{Name: "grid", Type: "Array(Nullable(Float64))"},
|
||||
}
|
||||
|
||||
// anyArgs matches a bound-argument list by count alone: the mock treats a
|
||||
// nil expected argument as a wildcard.
|
||||
func anyArgs(n int) []any {
|
||||
return make([]any, n)
|
||||
var seriesCols = []cmock.ColumnType{
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "labels", Type: "String"},
|
||||
}
|
||||
|
||||
func parse(t *testing.T, q string) parser.Expr {
|
||||
@@ -559,7 +553,7 @@ func TestTryExecuteRange_WindowedGateFallsBack(t *testing.T) {
|
||||
|
||||
// 1m range at 5m step: the windows are disjoint slivers — no
|
||||
// divisibility or width requirement, so this transpiles.
|
||||
store.Mock().ExpectQuery("FROM signoz_metrics\\.distributed_samples_v4").WithArgs(anyArgs(9)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
_, ok, err = e.TryExecuteRange(context.Background(), `avg_over_time(up[1m])`, start, end, 5*time.Minute)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "range below step is the disjoint form and must transpile")
|
||||
@@ -643,12 +637,12 @@ func TestTryExecuteRange_LastStyleWindowBelowStepTranspiles(t *testing.T) {
|
||||
start := time.UnixMilli(1_700_000_000_000)
|
||||
end := time.UnixMilli(1_700_003_600_000)
|
||||
|
||||
store.Mock().ExpectQuery("timeSeriesLastToGrid").WithArgs(anyArgs(10)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
_, ok, err := e.TryExecuteRange(context.Background(), `sum by (pod) (up)`, start, end, time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "instant selection at step > lookback must transpile")
|
||||
|
||||
store.Mock().ExpectQuery("timeSeriesLastToGrid").WithArgs(anyArgs(9)...).WillReturnRows(cmock.NewRows(unitCols, [][]any{}))
|
||||
store.Mock().ExpectQuery("SELECT fingerprint, any\\(labels\\)").WithArgs("up", int64(1_699_999_200_000), int64(1_700_003_600_000)).WillReturnRows(cmock.NewRows(seriesCols, [][]any{}))
|
||||
_, ok, err = e.TryExecuteRange(context.Background(), `last_over_time(up[10m])`, start, end, time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok, "last_over_time at range < step must transpile")
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
promModel "github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// Handler serves the Prometheus HTTP query API over a Prometheus provider:
|
||||
// /query and /query_range in the shape of Prometheus' /api/v1 endpoints
|
||||
// (https://prometheus.io/docs/prometheus/latest/querying/api/), intended to
|
||||
// be mounted under a distinguishing prefix (/prometheus/api/v1) so
|
||||
// PromQL-only endpoints are separate from the SigNoz query APIs. The request
|
||||
// and response contracts follow Prometheus: form-encoded GET/POST params,
|
||||
// {"status":"success","data":{resultType,result}} on success and
|
||||
// {"status":"error","errorType","error"} with Prometheus' status codes on
|
||||
// failure — so Prometheus-compatible clients can point at the prefix. The
|
||||
// wire shapes are documented as OpenAPI schemas in render.go.
|
||||
type Handler interface {
|
||||
Query(http.ResponseWriter, *http.Request)
|
||||
|
||||
QueryRange(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
logger *slog.Logger
|
||||
prom Prometheus
|
||||
}
|
||||
|
||||
func NewHandler(logger *slog.Logger, prom Prometheus) Handler {
|
||||
return &handler{logger: logger, prom: prom}
|
||||
}
|
||||
|
||||
// QueryRange evaluates an expression over a grid: query, start, end, step,
|
||||
// and optional timeout/stats params, all in Prometheus' formats.
|
||||
func (h *handler) QueryRange(w http.ResponseWriter, r *http.Request) {
|
||||
start, err := parseTime(r.FormValue("start"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
end, err := parseTime(r.FormValue("end"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
if end.Before(start) {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "end timestamp must not be before start time"))
|
||||
return
|
||||
}
|
||||
step, err := parseDuration(r.FormValue("step"))
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
if step <= 0 {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "zero or negative query resolution step widths are not accepted. Try a positive integer"))
|
||||
return
|
||||
}
|
||||
// The engine materializes every point of every series; an unbounded
|
||||
// grid is an unbounded allocation. 11,000 points covers 60s resolution
|
||||
// for a week or 1h resolution for a year.
|
||||
if end.Sub(start)/step > 11000 {
|
||||
h.respondError(r.Context(), w, errBadData, errors.NewInvalidInputf(errors.CodeInvalidInput, "exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel, err := h.contextWithTimeout(r)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
if h.tryRangeExecutor(ctx, w, r, start, end, step) {
|
||||
return
|
||||
}
|
||||
|
||||
qry, err := h.prom.Engine().NewRangeQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), start, end, step)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
h.exec(ctx, w, r, qry)
|
||||
}
|
||||
|
||||
// tryRangeExecutor serves the query the way a RangeExecutor provider is
|
||||
// designed to serve: evaluated inside the datastore when the shape allows.
|
||||
// It reports whether the response was written.
|
||||
func (h *handler) tryRangeExecutor(ctx context.Context, w http.ResponseWriter, r *http.Request, start, end time.Time, step time.Duration) bool {
|
||||
re, ok := h.prom.(RangeExecutor)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
matrix, served, err := re.TryExecuteRange(ctx, r.FormValue("query"), start, end, step)
|
||||
if err != nil {
|
||||
h.respondError(ctx, w, errExec, err)
|
||||
return true
|
||||
}
|
||||
if !served {
|
||||
return false
|
||||
}
|
||||
h.respond(ctx, w, &queryData{ResultType: matrix.Type(), Result: matrix}, nil, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// Query evaluates an expression at a single instant: query and optional
|
||||
// time/timeout/stats params. A missing time evaluates at the server's now,
|
||||
// as in Prometheus.
|
||||
func (h *handler) Query(w http.ResponseWriter, r *http.Request) {
|
||||
ts := time.Now()
|
||||
if t := r.FormValue("time"); t != "" {
|
||||
var err error
|
||||
ts, err = parseTime(t)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel, err := h.contextWithTimeout(r)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
qry, err := h.prom.Engine().NewInstantQuery(ctx, h.prom.Storage(), nil, r.FormValue("query"), ts)
|
||||
if err != nil {
|
||||
h.respondError(r.Context(), w, errBadData, err)
|
||||
return
|
||||
}
|
||||
h.exec(ctx, w, r, qry)
|
||||
}
|
||||
|
||||
func (h *handler) exec(ctx context.Context, w http.ResponseWriter, r *http.Request, qry promql.Query) {
|
||||
defer qry.Close()
|
||||
res := qry.Exec(ctx)
|
||||
if res.Err != nil {
|
||||
h.logger.ErrorContext(ctx, "error evaluating promql query", errors.Attr(res.Err))
|
||||
switch res.Err.(type) {
|
||||
case promql.ErrQueryCanceled:
|
||||
h.respondError(ctx, w, errCanceled, res.Err)
|
||||
case promql.ErrQueryTimeout:
|
||||
h.respondError(ctx, w, errTimeout, res.Err)
|
||||
case promql.ErrStorage:
|
||||
h.respondError(ctx, w, errInternal, res.Err)
|
||||
default:
|
||||
h.respondError(ctx, w, errExec, res.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
data := &queryData{ResultType: res.Value.Type(), Result: res.Value}
|
||||
if r.FormValue("stats") != "" {
|
||||
data.Stats = stats.NewQueryStats(qry.Stats())
|
||||
}
|
||||
warnings, infos := res.Warnings.AsStrings(r.FormValue("query"), 10, 10)
|
||||
h.respond(ctx, w, data, warnings, infos)
|
||||
}
|
||||
|
||||
func (h *handler) contextWithTimeout(r *http.Request) (context.Context, context.CancelFunc, error) {
|
||||
ctx := r.Context()
|
||||
if to := r.FormValue("timeout"); to != "" {
|
||||
timeout, err := parseDuration(to)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
return ctx, cancel, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return ctx, cancel, nil
|
||||
}
|
||||
|
||||
// parseTime accepts Prometheus' time formats: float unix seconds or RFC3339.
|
||||
func parseTime(s string) (time.Time, error) {
|
||||
if t, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
sec, ns := math.Modf(t)
|
||||
return time.Unix(int64(sec), int64(ns*float64(time.Second))), nil
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
return time.Time{}, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid timestamp", s)
|
||||
}
|
||||
|
||||
// parseDuration accepts Prometheus' duration formats: float seconds or a
|
||||
// duration string like 5m.
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
if d, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
ts := d * float64(time.Second)
|
||||
if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) {
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration. It overflows int64", s)
|
||||
}
|
||||
return time.Duration(ts), nil
|
||||
}
|
||||
if d, err := promModel.ParseDuration(s); err == nil {
|
||||
return time.Duration(d), nil
|
||||
}
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "cannot parse %q to a valid duration", s)
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// This file is the single description of the Prometheus API wire shapes:
|
||||
// the runtime envelope the handler encodes, and the *Schema types that
|
||||
// document the same shapes in the generated OpenAPI spec. The contract is
|
||||
// upstream's (https://prometheus.io/docs/prometheus/latest/querying/api/);
|
||||
// the schemas describe it, they do not define it.
|
||||
|
||||
type errorType string
|
||||
|
||||
const (
|
||||
errBadData errorType = "bad_data"
|
||||
errExec errorType = "execution"
|
||||
errCanceled errorType = "canceled"
|
||||
errTimeout errorType = "timeout"
|
||||
errInternal errorType = "internal"
|
||||
)
|
||||
|
||||
type queryData struct {
|
||||
ResultType parser.ValueType `json:"resultType"`
|
||||
Result parser.Value `json:"result"`
|
||||
Stats stats.QueryStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Status string `json:"status"`
|
||||
Data *queryData `json:"data,omitempty"`
|
||||
ErrorType errorType `json:"errorType,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Infos []string `json:"infos,omitempty"`
|
||||
}
|
||||
|
||||
func (h *handler) respond(ctx context.Context, w http.ResponseWriter, data *queryData, warnings, infos []string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(&response{Status: "success", Data: data, Warnings: warnings, Infos: infos}); err != nil {
|
||||
h.logger.ErrorContext(ctx, "error writing prometheus api response", errors.Attr(err))
|
||||
}
|
||||
}
|
||||
|
||||
// respondError follows Prometheus' status-code mapping: bad_data 400,
|
||||
// execution 422, canceled/timeout 503, internal 500.
|
||||
func (h *handler) respondError(ctx context.Context, w http.ResponseWriter, typ errorType, err error) {
|
||||
code := http.StatusInternalServerError
|
||||
switch typ {
|
||||
case errBadData:
|
||||
code = http.StatusBadRequest
|
||||
case errExec:
|
||||
code = http.StatusUnprocessableEntity
|
||||
case errCanceled, errTimeout:
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
if encErr := json.NewEncoder(w).Encode(&response{Status: "error", ErrorType: typ, Error: err.Error()}); encErr != nil {
|
||||
h.logger.ErrorContext(ctx, "error writing prometheus api error response", errors.Attr(encErr))
|
||||
}
|
||||
}
|
||||
|
||||
// The endpoints accept parameters as URL query params or a form-encoded
|
||||
// body, on GET and POST alike.
|
||||
type QueryParamsSchema struct {
|
||||
Query string `query:"query" required:"true" description:"PromQL expression."`
|
||||
Time string `query:"time" description:"Evaluation timestamp: RFC3339 or float unix seconds. Defaults to the server's current time."`
|
||||
Timeout string `query:"timeout" description:"Evaluation timeout: duration string or float seconds."`
|
||||
Stats string `query:"stats" description:"Any non-empty value includes query statistics in the response."`
|
||||
}
|
||||
|
||||
type QueryRangeParamsSchema struct {
|
||||
Query string `query:"query" required:"true" description:"PromQL expression."`
|
||||
Start string `query:"start" required:"true" description:"Range start: RFC3339 or float unix seconds."`
|
||||
End string `query:"end" required:"true" description:"Range end: RFC3339 or float unix seconds."`
|
||||
Step string `query:"step" required:"true" description:"Resolution step: duration string or float seconds."`
|
||||
Timeout string `query:"timeout" description:"Evaluation timeout: duration string or float seconds."`
|
||||
Stats string `query:"stats" description:"Any non-empty value includes query statistics in the response."`
|
||||
}
|
||||
|
||||
type SuccessResponseSchema struct {
|
||||
Status string `json:"status" enum:"success" required:"true"`
|
||||
Data QueryDataSchema `json:"data" required:"true"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Infos []string `json:"infos,omitempty"`
|
||||
}
|
||||
|
||||
// QueryDataSchema is the result union, discriminated by resultType.
|
||||
type QueryDataSchema struct{}
|
||||
|
||||
var _ jsonschema.OneOfExposer = QueryDataSchema{}
|
||||
|
||||
func (QueryDataSchema) JSONSchemaOneOf() []interface{} {
|
||||
return []interface{}{MatrixDataSchema{}, VectorDataSchema{}, ScalarDataSchema{}, StringDataSchema{}}
|
||||
}
|
||||
|
||||
type MatrixDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"matrix" required:"true"`
|
||||
Result []MatrixSeriesSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type MatrixSeriesSchema struct {
|
||||
Metric map[string]string `json:"metric" required:"true"`
|
||||
Values []SamplePairSchema `json:"values" required:"true"`
|
||||
}
|
||||
|
||||
type VectorDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"vector" required:"true"`
|
||||
Result []VectorSampleSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type VectorSampleSchema struct {
|
||||
Metric map[string]string `json:"metric" required:"true"`
|
||||
Value SamplePairSchema `json:"value" required:"true"`
|
||||
}
|
||||
|
||||
type ScalarDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"scalar" required:"true"`
|
||||
Result SamplePairSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
type StringDataSchema struct {
|
||||
ResultType string `json:"resultType" enum:"string" required:"true"`
|
||||
Result SamplePairSchema `json:"result" required:"true"`
|
||||
}
|
||||
|
||||
// SamplePairSchema is the positional [timestamp, value] pair: a float of
|
||||
// unix seconds, then the value as a string ("NaN", "+Inf" and "-Inf"
|
||||
// included). Struct reflection cannot express a positional array, so the
|
||||
// schema is authored by hand.
|
||||
type SamplePairSchema struct{}
|
||||
|
||||
var _ jsonschema.Exposer = SamplePairSchema{}
|
||||
|
||||
func (SamplePairSchema) JSONSchema() (jsonschema.Schema, error) {
|
||||
item := jsonschema.Schema{}
|
||||
item.WithOneOf(
|
||||
(&jsonschema.Schema{}).WithType(jsonschema.Number.Type()).ToSchemaOrBool(),
|
||||
(&jsonschema.Schema{}).WithType(jsonschema.String.Type()).ToSchemaOrBool(),
|
||||
)
|
||||
s := jsonschema.Schema{}
|
||||
s.WithType(jsonschema.Array.Type())
|
||||
s.WithMinItems(2)
|
||||
s.WithMaxItems(2)
|
||||
s.WithItems(*(&jsonschema.Items{}).WithSchemaOrBool(item.ToSchemaOrBool()))
|
||||
s.WithDescription(`A [timestamp, value] pair: float unix seconds, then the string-encoded sample value ("NaN", "+Inf", "-Inf" included).`)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type ErrorResponseSchema struct {
|
||||
Status string `json:"status" enum:"error" required:"true"`
|
||||
ErrorType string `json:"errorType" enum:"bad_data,execution,canceled,timeout,internal" required:"true"`
|
||||
Error string `json:"error" required:"true"`
|
||||
}
|
||||
@@ -387,7 +387,6 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
|
||||
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)
|
||||
@@ -458,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
|
||||
|
||||
@@ -75,16 +75,6 @@ func queryRangeVariables(body []byte) (map[string]qbtypes.VariableItem, error) {
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// PromQLResources is the resource set of a bare PromQL query: metrics on
|
||||
// the promql wildcard, the same ID resourcesForQuery assigns to a PromQL
|
||||
// query inside a composite — one grant covers both entry points.
|
||||
func PromQLResources(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
return []coretypes.ResourceWithID{{
|
||||
Resource: coretypes.ResourceTelemetryResourceMetrics,
|
||||
ID: qbtypes.QueryTypePromQL.StringValue() + "/" + coretypes.WildCardSelectorString,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
queryType := query.Get("type").String()
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
@@ -50,7 +50,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
@@ -78,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
|
||||
@@ -85,7 +85,6 @@ type Handlers struct {
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
SpanMapperHandler spanmapper.Handler
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
PrometheusHandler prometheus.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
RulerHandler ruler.Handler
|
||||
LLMPricingRuleHandler llmpricingrule.Handler
|
||||
@@ -97,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,
|
||||
@@ -106,7 +105,6 @@ func NewHandlers(
|
||||
zeusService zeus.Zeus,
|
||||
registryHandler factory.Handler,
|
||||
alertmanagerService alertmanager.Alertmanager,
|
||||
prometheusService prometheus.Prometheus,
|
||||
rulerService ruler.Ruler,
|
||||
statsAggregator statsreporter.Aggregator,
|
||||
) Handlers {
|
||||
@@ -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,
|
||||
@@ -136,7 +135,6 @@ func NewHandlers(
|
||||
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
|
||||
SpanMapperHandler: implspanmapper.NewHandler(modules.SpanMapper),
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
PrometheusHandler: prometheus.NewHandler(providerSettings.Logger, prometheusService),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
LLMPricingRuleHandler: impllmpricingrule.NewHandler(modules.LLMPricingRule),
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager, nil, nil)
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
f := reflectVal.Field(i)
|
||||
|
||||
@@ -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"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
@@ -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 }{},
|
||||
@@ -89,7 +90,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ spanmapper.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ prometheus.Handler }{},
|
||||
struct{ llmpricingrule.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
|
||||
@@ -245,6 +245,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddLicenseTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -335,6 +336,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.AuthzHandler,
|
||||
handlers.RawDataExport,
|
||||
handlers.ZeusHandler,
|
||||
handlers.LicensingHandler,
|
||||
handlers.QuerierHandler,
|
||||
handlers.ServiceAccountHandler,
|
||||
modules.ServiceAccountGetter,
|
||||
@@ -343,7 +345,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RuleStateHistory,
|
||||
handlers.SpanMapperHandler,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.PrometheusHandler,
|
||||
handlers.LLMPricingRuleHandler,
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
|
||||
@@ -617,7 +617,7 @@ func New(
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
registryHandler := factory.NewHandler(registry)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, prometheus, rulerInstance, statsAggregator)
|
||||
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager, rulerInstance, statsAggregator)
|
||||
|
||||
// Initialize the API server (after registry so it can access service health)
|
||||
apiserverInstance, err := factory.NewProviderFromNamedMap(
|
||||
|
||||
159
pkg/sqlmigration/119_add_license_tuples.go
Normal file
159
pkg/sqlmigration/119_add_license_tuples.go
Normal 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
|
||||
}
|
||||
@@ -64,11 +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
|
||||
// route serves them today.
|
||||
// Uniform LCRUD shape served by /api/v4/licenses: create = Activate,
|
||||
// update = Refresh, read = Get (includes the key), list, delete (non-cloud
|
||||
// licenses only). GET /api/v4/orgs/me/license is OpenAccess, so the read
|
||||
// grant is not enforced there.
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
|
||||
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
|
||||
|
||||
@@ -3,15 +3,19 @@ package licensetypes
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeCloudLicenseOperationUnsupported = errors.MustNewCode("cloud_license_operation_unsupported")
|
||||
)
|
||||
|
||||
type StorableLicense struct {
|
||||
bun.BaseModel `bun:"table:license"`
|
||||
|
||||
@@ -28,10 +32,12 @@ type License struct {
|
||||
ID valuer.UUID
|
||||
Key string
|
||||
Data map[string]interface{}
|
||||
PlanName valuer.String
|
||||
Plan LicensePlan
|
||||
EventQueue LicenseEventQueue
|
||||
Features []*Feature
|
||||
Status valuer.String
|
||||
State string
|
||||
State valuer.String
|
||||
Platform valuer.String
|
||||
FreeUntil time.Time
|
||||
ValidFrom int64
|
||||
ValidUntil int64
|
||||
@@ -41,28 +47,47 @@ type License struct {
|
||||
OrganizationID valuer.UUID
|
||||
}
|
||||
|
||||
type GettableLicense map[string]any
|
||||
type LicensePlan struct {
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
Name valuer.String `json:"name" required:"true"`
|
||||
Description string `json:"description" required:"true"`
|
||||
IsActive bool `json:"isActive" required:"true"`
|
||||
CreatedAt time.Time `json:"createdAt" required:"true"`
|
||||
UpdatedAt time.Time `json:"updatedAt" required:"true"`
|
||||
}
|
||||
|
||||
type LicenseEventQueue struct {
|
||||
Event valuer.String `json:"event" required:"true"`
|
||||
Status valuer.String `json:"status" required:"true"`
|
||||
ScheduledAt time.Time `json:"scheduledAt" required:"true"`
|
||||
CreatedAt time.Time `json:"createdAt" required:"true"`
|
||||
UpdatedAt time.Time `json:"updatedAt" required:"true"`
|
||||
}
|
||||
|
||||
type GettableLicense struct {
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
ValidFrom int64 `json:"validFrom" required:"true"`
|
||||
ValidUntil int64 `json:"validUntil" required:"true"`
|
||||
Status valuer.String `json:"status" required:"true"`
|
||||
State valuer.String `json:"state" required:"true"`
|
||||
Platform valuer.String `json:"platform" required:"true"`
|
||||
FreeUntil time.Time `json:"freeUntil" required:"true"`
|
||||
CreatedAt time.Time `json:"createdAt" required:"true"`
|
||||
UpdatedAt time.Time `json:"updatedAt" required:"true"`
|
||||
Plan LicensePlan `json:"plan" required:"true"`
|
||||
Features []*Feature `json:"features" required:"true"`
|
||||
EventQueue LicenseEventQueue `json:"eventQueue" required:"true"`
|
||||
}
|
||||
|
||||
type GettableLicenseWithKey struct {
|
||||
GettableLicense
|
||||
Key string `json:"key" required:"true"`
|
||||
}
|
||||
|
||||
type PostableLicense struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func NewStorableLicense(ID valuer.UUID, key string, data map[string]any, createdAt, updatedAt, lastValidatedAt time.Time, organizationID valuer.UUID) *StorableLicense {
|
||||
return &StorableLicense{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: ID,
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
Key: key,
|
||||
Data: data,
|
||||
LastValidatedAt: lastValidatedAt,
|
||||
OrgID: organizationID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorableLicenseFromLicense(license *License) *StorableLicense {
|
||||
return &StorableLicense{
|
||||
Identifiable: types.Identifiable{
|
||||
@@ -106,263 +131,201 @@ func GetActiveLicenseFromStorableLicenses(storableLicenses []*StorableLicense, o
|
||||
return activeLicense, nil
|
||||
}
|
||||
|
||||
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
|
||||
var zeroValue T
|
||||
if val, ok := data[key]; ok {
|
||||
if value, ok := val.(T); ok {
|
||||
return value, nil
|
||||
}
|
||||
return zeroValue, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
|
||||
}
|
||||
return zeroValue, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s key is missing", key)
|
||||
}
|
||||
|
||||
func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
|
||||
licenseData := map[string]any{}
|
||||
err := json.Unmarshal(data, &licenseData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
|
||||
func NewLicense(zeusLicense *zeustypes.License, organizationID valuer.UUID) (*License, error) {
|
||||
if zeusLicense.ID.IsZero() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license id is missing")
|
||||
}
|
||||
|
||||
var features []*Feature
|
||||
|
||||
// extract id from data
|
||||
licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if zeusLicense.Key == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license key is missing")
|
||||
}
|
||||
licenseID, err := valuer.NewUUID(licenseIDStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(licenseData, "id")
|
||||
|
||||
// extract key from data
|
||||
licenseKey, err := extractKeyFromMapStringInterface[string](licenseData, "key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(licenseData, "key")
|
||||
|
||||
// extract status from data
|
||||
statusStr, err := extractKeyFromMapStringInterface[string](licenseData, "status")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := valuer.NewString(statusStr)
|
||||
|
||||
planMap, err := extractKeyFromMapStringInterface[map[string]any](licenseData, "plan")
|
||||
planName, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
|
||||
features := newMergedFeatures(planName, zeusLicense.Features)
|
||||
|
||||
data, err := newDataFromZeusLicense(zeusLicense, features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
planName := valuer.NewString(planNameStr)
|
||||
// if license status is invalid then default it to basic
|
||||
if status == LicenseStatusInvalid {
|
||||
planName = PlanNameBasic
|
||||
}
|
||||
|
||||
state, err := extractKeyFromMapStringInterface[string](licenseData, "state")
|
||||
if err != nil {
|
||||
state = ""
|
||||
}
|
||||
|
||||
freeUntilStr, err := extractKeyFromMapStringInterface[string](licenseData, "free_until")
|
||||
if err != nil {
|
||||
freeUntilStr = ""
|
||||
}
|
||||
|
||||
freeUntil, err := time.Parse(time.RFC3339, freeUntilStr)
|
||||
if err != nil {
|
||||
freeUntil = time.Time{}
|
||||
}
|
||||
|
||||
featuresFromZeus := make([]*Feature, 0)
|
||||
if _features, ok := licenseData["features"]; ok {
|
||||
featuresData, err := json.Marshal(_features)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data")
|
||||
}
|
||||
}
|
||||
|
||||
switch planName {
|
||||
case PlanNameEnterprise:
|
||||
features = append(features, EnterprisePlan...)
|
||||
case PlanNameBasic:
|
||||
features = append(features, BasicPlan...)
|
||||
default:
|
||||
features = append(features, BasicPlan...)
|
||||
}
|
||||
|
||||
if len(featuresFromZeus) > 0 {
|
||||
for _, feature := range featuresFromZeus {
|
||||
exists := false
|
||||
for i, existingFeature := range features {
|
||||
if existingFeature.Name == feature.Name {
|
||||
features[i] = feature // Replace existing feature
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
features = append(features, feature) // Append if it doesn't exist
|
||||
}
|
||||
}
|
||||
}
|
||||
licenseData["features"] = features
|
||||
|
||||
_validFrom, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_from")
|
||||
if err != nil {
|
||||
_validFrom = 0
|
||||
}
|
||||
validFrom := int64(_validFrom)
|
||||
|
||||
_validUntil, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_until")
|
||||
if err != nil {
|
||||
_validUntil = 0
|
||||
}
|
||||
validUntil := int64(_validUntil)
|
||||
|
||||
return &License{
|
||||
ID: licenseID,
|
||||
Key: licenseKey,
|
||||
Data: licenseData,
|
||||
PlanName: planName,
|
||||
ID: zeusLicense.ID,
|
||||
Key: zeusLicense.Key,
|
||||
Data: data,
|
||||
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
|
||||
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
|
||||
Features: features,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
ValidFrom: zeusLicense.ValidFrom,
|
||||
ValidUntil: zeusLicense.ValidUntil,
|
||||
Status: status,
|
||||
State: state,
|
||||
FreeUntil: freeUntil,
|
||||
State: valuer.NewString(zeusLicense.State),
|
||||
Platform: valuer.NewString(zeusLicense.Platform),
|
||||
FreeUntil: zeusLicense.FreeUntil,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
LastValidatedAt: time.Now(),
|
||||
OrganizationID: organizationID,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
|
||||
var features []*Feature
|
||||
// extract status from data
|
||||
statusStr, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := valuer.NewString(statusStr)
|
||||
|
||||
planMap, err := extractKeyFromMapStringInterface[map[string]any](storableLicense.Data, "plan")
|
||||
zeusLicense, err := NewZeusLicenseFromData(storableLicense.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
|
||||
planName, status, err := newPlanNameAndStatusFromZeusLicense(zeusLicense)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
planName := valuer.NewString(planNameStr)
|
||||
// if license status is invalid then default it to basic
|
||||
if status == LicenseStatusInvalid {
|
||||
planName = PlanNameBasic
|
||||
}
|
||||
|
||||
featuresFromZeus := make([]*Feature, 0)
|
||||
if _features, ok := storableLicense.Data["features"]; ok {
|
||||
featuresData, err := json.Marshal(_features)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data")
|
||||
}
|
||||
}
|
||||
|
||||
switch planName {
|
||||
case PlanNameEnterprise:
|
||||
features = append(features, EnterprisePlan...)
|
||||
case PlanNameBasic:
|
||||
features = append(features, BasicPlan...)
|
||||
default:
|
||||
features = append(features, BasicPlan...)
|
||||
}
|
||||
|
||||
if len(featuresFromZeus) > 0 {
|
||||
for _, feature := range featuresFromZeus {
|
||||
exists := false
|
||||
for i, existingFeature := range features {
|
||||
if existingFeature.Name == feature.Name {
|
||||
features[i] = feature // Replace existing feature
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
features = append(features, feature) // Append if it doesn't exist
|
||||
}
|
||||
}
|
||||
}
|
||||
features := newMergedFeatures(planName, zeusLicense.Features)
|
||||
storableLicense.Data["features"] = features
|
||||
|
||||
_validFrom, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_from")
|
||||
if err != nil {
|
||||
_validFrom = 0
|
||||
}
|
||||
validFrom := int64(_validFrom)
|
||||
|
||||
_validUntil, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_until")
|
||||
if err != nil {
|
||||
_validUntil = 0
|
||||
}
|
||||
validUntil := int64(_validUntil)
|
||||
|
||||
state, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "state")
|
||||
if err != nil {
|
||||
state = ""
|
||||
}
|
||||
|
||||
freeUntilStr, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "free_until")
|
||||
if err != nil {
|
||||
freeUntilStr = ""
|
||||
}
|
||||
|
||||
freeUntil, err := time.Parse(time.RFC3339, freeUntilStr)
|
||||
if err != nil {
|
||||
freeUntil = time.Time{}
|
||||
}
|
||||
|
||||
return &License{
|
||||
ID: storableLicense.ID,
|
||||
Key: storableLicense.Key,
|
||||
Data: storableLicense.Data,
|
||||
PlanName: planName,
|
||||
Plan: newLicensePlanFromZeusLicense(zeusLicense, planName),
|
||||
EventQueue: newLicenseEventQueueFromZeusLicense(zeusLicense),
|
||||
Features: features,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
ValidFrom: zeusLicense.ValidFrom,
|
||||
ValidUntil: zeusLicense.ValidUntil,
|
||||
Status: status,
|
||||
State: state,
|
||||
FreeUntil: freeUntil,
|
||||
State: valuer.NewString(zeusLicense.State),
|
||||
Platform: valuer.NewString(zeusLicense.Platform),
|
||||
FreeUntil: zeusLicense.FreeUntil,
|
||||
CreatedAt: storableLicense.CreatedAt,
|
||||
UpdatedAt: storableLicense.UpdatedAt,
|
||||
LastValidatedAt: storableLicense.LastValidatedAt,
|
||||
OrganizationID: storableLicense.OrgID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewZeusLicenseFromData(data map[string]any) (*zeustypes.License, error) {
|
||||
dataBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal license data")
|
||||
}
|
||||
|
||||
zeusLicense := new(zeustypes.License)
|
||||
if err := json.Unmarshal(dataBytes, zeusLicense); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
|
||||
}
|
||||
|
||||
return zeusLicense, nil
|
||||
}
|
||||
|
||||
func newPlanNameAndStatusFromZeusLicense(zeusLicense *zeustypes.License) (valuer.String, valuer.String, error) {
|
||||
if zeusLicense.Status == "" {
|
||||
return valuer.String{}, valuer.String{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license status is missing")
|
||||
}
|
||||
|
||||
if zeusLicense.Plan.Name == "" {
|
||||
return valuer.String{}, valuer.String{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license plan name is missing")
|
||||
}
|
||||
|
||||
status := valuer.NewString(zeusLicense.Status)
|
||||
planName := valuer.NewString(zeusLicense.Plan.Name)
|
||||
// if license status is invalid then default it to basic
|
||||
if status == LicenseStatusInvalid {
|
||||
planName = PlanNameBasic
|
||||
}
|
||||
|
||||
return planName, status, nil
|
||||
}
|
||||
|
||||
func newLicensePlanFromZeusLicense(zeusLicense *zeustypes.License, planName valuer.String) LicensePlan {
|
||||
return LicensePlan{
|
||||
ID: zeusLicense.Plan.ID,
|
||||
Name: planName,
|
||||
Description: zeusLicense.Plan.Description,
|
||||
IsActive: zeusLicense.Plan.IsActive,
|
||||
CreatedAt: zeusLicense.Plan.CreatedAt,
|
||||
UpdatedAt: zeusLicense.Plan.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func newLicenseEventQueueFromZeusLicense(zeusLicense *zeustypes.License) LicenseEventQueue {
|
||||
return LicenseEventQueue{
|
||||
Event: valuer.NewString(zeusLicense.EventQueue.Event),
|
||||
Status: valuer.NewString(zeusLicense.EventQueue.Status),
|
||||
ScheduledAt: zeusLicense.EventQueue.ScheduledAt,
|
||||
CreatedAt: zeusLicense.EventQueue.CreatedAt,
|
||||
UpdatedAt: zeusLicense.EventQueue.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func newMergedFeatures(planName valuer.String, zeusFeatures []zeustypes.LicenseFeature) []*Feature {
|
||||
features := make([]*Feature, 0)
|
||||
switch planName {
|
||||
case PlanNameEnterprise:
|
||||
features = append(features, EnterprisePlan...)
|
||||
default:
|
||||
features = append(features, BasicPlan...)
|
||||
}
|
||||
|
||||
for _, zeusFeature := range zeusFeatures {
|
||||
feature := &Feature{
|
||||
Name: valuer.NewString(zeusFeature.Name),
|
||||
Active: zeusFeature.Active,
|
||||
Usage: zeusFeature.Usage,
|
||||
UsageLimit: zeusFeature.UsageLimit,
|
||||
Route: zeusFeature.Route,
|
||||
}
|
||||
|
||||
exists := false
|
||||
for i, existingFeature := range features {
|
||||
if existingFeature.Name == feature.Name {
|
||||
features[i] = feature
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
features = append(features, feature)
|
||||
}
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
func newDataFromZeusLicense(zeusLicense *zeustypes.License, features []*Feature) (map[string]any, error) {
|
||||
dataBytes, err := json.Marshal(zeusLicense)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal license data")
|
||||
}
|
||||
|
||||
data := map[string]any{}
|
||||
if err := json.Unmarshal(dataBytes, &data); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
|
||||
}
|
||||
|
||||
delete(data, "id")
|
||||
delete(data, "key")
|
||||
data["features"] = features
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ErrIfCloud returns an error if the license is managed by SigNoz Cloud. The
|
||||
// caller should enrich the error with the specific operation using errors.WithAdditionalf.
|
||||
func (license *License) ErrIfCloud() error {
|
||||
if license.Platform == LicensePlatformCloud {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeCloudLicenseOperationUnsupported, "this operation is not supported for licenses managed by SigNoz Cloud")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewStatsFromLicense(license *License) map[string]any {
|
||||
return map[string]any{
|
||||
"license.id": license.ID.StringValue(),
|
||||
"license.plan.name": license.PlanName.StringValue(),
|
||||
"license.state.name": license.State,
|
||||
"license.plan.name": license.Plan.Name.StringValue(),
|
||||
"license.state.name": license.State.StringValue(),
|
||||
"license.free_until.time": license.FreeUntil.UTC(),
|
||||
}
|
||||
}
|
||||
@@ -371,8 +334,8 @@ func (license *License) UpdateFeatures(features []*Feature) {
|
||||
license.Features = features
|
||||
}
|
||||
|
||||
func (license *License) Update(data []byte) error {
|
||||
updatedLicense, err := NewLicense(data, license.OrganizationID)
|
||||
func (license *License) Update(zeusLicense *zeustypes.License) error {
|
||||
updatedLicense, err := NewLicense(zeusLicense, license.OrganizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -382,8 +345,11 @@ func (license *License) Update(data []byte) error {
|
||||
license.Features = updatedLicense.Features
|
||||
license.ID = updatedLicense.ID
|
||||
license.Key = updatedLicense.Key
|
||||
license.PlanName = updatedLicense.PlanName
|
||||
license.Plan = updatedLicense.Plan
|
||||
license.EventQueue = updatedLicense.EventQueue
|
||||
license.Status = updatedLicense.Status
|
||||
license.State = updatedLicense.State
|
||||
license.Platform = updatedLicense.Platform
|
||||
license.ValidFrom = updatedLicense.ValidFrom
|
||||
license.ValidUntil = updatedLicense.ValidUntil
|
||||
license.UpdatedAt = currentTime
|
||||
@@ -392,13 +358,28 @@ func (license *License) Update(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewGettableLicense(data map[string]any, key string) *GettableLicense {
|
||||
gettableLicense := make(GettableLicense)
|
||||
for k, v := range data {
|
||||
gettableLicense[k] = v
|
||||
func NewGettableLicense(license *License) *GettableLicense {
|
||||
return &GettableLicense{
|
||||
ID: license.ID,
|
||||
ValidFrom: license.ValidFrom,
|
||||
ValidUntil: license.ValidUntil,
|
||||
Status: license.Status,
|
||||
State: license.State,
|
||||
Platform: license.Platform,
|
||||
FreeUntil: license.FreeUntil,
|
||||
CreatedAt: license.CreatedAt,
|
||||
UpdatedAt: license.UpdatedAt,
|
||||
Plan: license.Plan,
|
||||
Features: license.Features,
|
||||
EventQueue: license.EventQueue,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGettableLicenseWithKey(license *License) *GettableLicenseWithKey {
|
||||
return &GettableLicenseWithKey{
|
||||
GettableLicense: *NewGettableLicense(license),
|
||||
Key: license.Key,
|
||||
}
|
||||
gettableLicense["key"] = key
|
||||
return &gettableLicense
|
||||
}
|
||||
|
||||
func (p *PostableLicense) UnmarshalJSON(data []byte) error {
|
||||
@@ -424,4 +405,5 @@ type Store interface {
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableLicense, error)
|
||||
GetAll(context.Context, valuer.UUID) ([]*StorableLicense, error)
|
||||
Update(context.Context, valuer.UUID, *StorableLicense) error
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
|
||||
@@ -1,178 +1,135 @@
|
||||
package licensetypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewLicenseV3(t *testing.T) {
|
||||
func TestNewLicenseValidation(t *testing.T) {
|
||||
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
pass bool
|
||||
expected *License
|
||||
error error
|
||||
name string
|
||||
data string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Error for missing license id",
|
||||
data: []byte(`{}`),
|
||||
pass: false,
|
||||
error: errors.New("id key is missing"),
|
||||
name: "missing license id",
|
||||
data: `{}`,
|
||||
errorContains: "license id is missing",
|
||||
},
|
||||
{
|
||||
name: "Error for license id not being a valid string",
|
||||
data: []byte(`{"id": 10}`),
|
||||
pass: false,
|
||||
error: errors.New("id key is not a valid string"),
|
||||
name: "missing license key",
|
||||
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`,
|
||||
errorContains: "license key is missing",
|
||||
},
|
||||
{
|
||||
name: "Error for missing license key",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`),
|
||||
pass: false,
|
||||
error: errors.New("key key is missing"),
|
||||
name: "missing license status",
|
||||
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter"}`,
|
||||
errorContains: "license status is missing",
|
||||
},
|
||||
{
|
||||
name: "Error for invalid string license key",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":10}`),
|
||||
pass: false,
|
||||
error: errors.New("key key is not a valid string"),
|
||||
},
|
||||
{
|
||||
name: "Error for missing license status",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e", "key": "does-not-matter","category":"FREE"}`),
|
||||
pass: false,
|
||||
error: errors.New("status key is missing"),
|
||||
},
|
||||
{
|
||||
name: "Error for invalid string license status",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key": "does-not-matter", "category":"FREE", "status":10}`),
|
||||
pass: false,
|
||||
error: errors.New("status key is not a valid string"),
|
||||
},
|
||||
{
|
||||
name: "Error for missing license plan",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
|
||||
pass: false,
|
||||
error: errors.New("plan key is missing"),
|
||||
},
|
||||
{
|
||||
name: "Error for invalid json license plan",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
|
||||
pass: false,
|
||||
error: errors.New("plan key is not a valid map[string]interface {}"),
|
||||
},
|
||||
{
|
||||
name: "Error for invalid license plan",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
|
||||
pass: false,
|
||||
error: errors.New("name key is missing"),
|
||||
},
|
||||
{
|
||||
name: "Parse the entire license properly",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1,"state":"test","free_until":"2025-05-16T11:17:48.124202Z"}`),
|
||||
pass: true,
|
||||
expected: &License{
|
||||
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"category": "FREE",
|
||||
"status": "ACTIVE",
|
||||
"valid_from": float64(1730899309),
|
||||
"valid_until": float64(-1),
|
||||
"state": "test",
|
||||
"free_until": "2025-05-16T11:17:48.124202Z",
|
||||
},
|
||||
PlanName: PlanNameEnterprise,
|
||||
ValidFrom: 1730899309,
|
||||
ValidUntil: -1,
|
||||
Status: valuer.NewString("ACTIVE"),
|
||||
State: "test",
|
||||
FreeUntil: time.Date(2025, 5, 16, 11, 17, 48, 124202000, time.UTC),
|
||||
Features: make([]*Feature, 0),
|
||||
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Fallback to basic plan if license status is invalid",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
|
||||
pass: true,
|
||||
expected: &License{
|
||||
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"category": "FREE",
|
||||
"status": "INVALID",
|
||||
"valid_from": float64(1730899309),
|
||||
"valid_until": float64(-1),
|
||||
},
|
||||
PlanName: PlanNameBasic,
|
||||
ValidFrom: 1730899309,
|
||||
ValidUntil: -1,
|
||||
Status: valuer.NewString("INVALID"),
|
||||
Features: make([]*Feature, 0),
|
||||
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback states for validFrom and validUntil",
|
||||
data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`),
|
||||
pass: true,
|
||||
expected: &License{
|
||||
ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"valid_from": 1234.456,
|
||||
"valid_until": 5678.567,
|
||||
"category": "FREE",
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
PlanName: PlanNameEnterprise,
|
||||
ValidFrom: 1234,
|
||||
ValidUntil: 5678,
|
||||
Status: valuer.NewString("ACTIVE"),
|
||||
Features: make([]*Feature, 0),
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
LastValidatedAt: time.Time{},
|
||||
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
|
||||
},
|
||||
name: "missing license plan name",
|
||||
data: `{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter","status":"ACTIVE","plan":{}}`,
|
||||
errorContains: "license plan name is missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
license, err := NewLicense(tc.data, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"))
|
||||
if license != nil {
|
||||
license.Features = make([]*Feature, 0)
|
||||
delete(license.Data, "features")
|
||||
}
|
||||
|
||||
if tc.pass {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, license)
|
||||
// as the new license will pick the time.Now() value. doesn't make sense to compare them
|
||||
license.CreatedAt = time.Time{}
|
||||
license.UpdatedAt = time.Time{}
|
||||
license.LastValidatedAt = time.Time{}
|
||||
assert.Equal(t, tc.expected, license)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.error.Error())
|
||||
require.Nil(t, license)
|
||||
}
|
||||
zeusLicense := new(zeustypes.License)
|
||||
require.NoError(t, json.Unmarshal([]byte(tc.data), zeusLicense), tc.name)
|
||||
|
||||
license, err := NewLicense(zeusLicense, organizationID)
|
||||
require.Error(t, err, tc.name)
|
||||
assert.ErrorContains(t, err, tc.errorContains, tc.name)
|
||||
require.Nil(t, license, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLicense(t *testing.T) {
|
||||
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
|
||||
|
||||
zeusLicense := new(zeustypes.License)
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"ACTIVE","state":"EVALUATING","platform":"SELF_HOSTED","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1,"free_until":"2025-05-16T11:17:48.124202Z","features":[{"name":"sso","active":true,"usage":0,"usage_limit":-1,"route":""}],"event_queue":{"event":"DEFAULT","status":"SCHEDULED"}}`), zeusLicense))
|
||||
|
||||
license, err := NewLicense(zeusLicense, organizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), license.ID)
|
||||
assert.Equal(t, "does-not-matter-key", license.Key)
|
||||
assert.Equal(t, PlanNameEnterprise, license.Plan.Name)
|
||||
assert.Equal(t, valuer.NewString("active"), license.Status)
|
||||
assert.Equal(t, valuer.NewString("evaluating"), license.State)
|
||||
assert.Equal(t, LicensePlatformSelfHosted, license.Platform)
|
||||
assert.Equal(t, valuer.NewString("default"), license.EventQueue.Event)
|
||||
assert.Equal(t, valuer.NewString("scheduled"), license.EventQueue.Status)
|
||||
assert.Equal(t, int64(1730899309), license.ValidFrom)
|
||||
assert.Equal(t, int64(-1), license.ValidUntil)
|
||||
assert.Equal(t, time.Date(2025, 5, 16, 11, 17, 48, 124202000, time.UTC), license.FreeUntil)
|
||||
assert.Equal(t, organizationID, license.OrganizationID)
|
||||
|
||||
ssoFeature := false
|
||||
for _, feature := range license.Features {
|
||||
if feature.Name == SSO {
|
||||
ssoFeature = feature.Active
|
||||
}
|
||||
}
|
||||
assert.True(t, ssoFeature)
|
||||
|
||||
assert.NotContains(t, license.Data, "id")
|
||||
assert.NotContains(t, license.Data, "key")
|
||||
assert.Equal(t, "ACTIVE", license.Data["status"])
|
||||
|
||||
gettableLicense := NewGettableLicense(license)
|
||||
assert.Equal(t, license.ID, gettableLicense.ID)
|
||||
assert.Equal(t, valuer.NewString("active"), gettableLicense.Status)
|
||||
assert.Equal(t, LicensePlatformSelfHosted, gettableLicense.Platform)
|
||||
assert.Equal(t, PlanNameEnterprise, gettableLicense.Plan.Name)
|
||||
|
||||
gettableLicenseWithKey := NewGettableLicenseWithKey(license)
|
||||
assert.Equal(t, "does-not-matter-key", gettableLicenseWithKey.Key)
|
||||
}
|
||||
|
||||
func TestNewLicenseFallsBackToBasicPlanOnInvalidStatus(t *testing.T) {
|
||||
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
|
||||
|
||||
zeusLicense := new(zeustypes.License)
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1}`), zeusLicense))
|
||||
|
||||
license, err := NewLicense(zeusLicense, organizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, PlanNameBasic, license.Plan.Name)
|
||||
}
|
||||
|
||||
func TestNewLicenseFromStorableLicenseRoundTrip(t *testing.T) {
|
||||
organizationID := valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")
|
||||
|
||||
zeusLicense := new(zeustypes.License)
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","status":"ACTIVE","state":"EVALUATING","platform":"CLOUD","plan":{"name":"ENTERPRISE"},"valid_from":1730899309,"valid_until":-1}`), zeusLicense))
|
||||
|
||||
license, err := NewLicense(zeusLicense, organizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
storableLicense := NewStorableLicenseFromLicense(license)
|
||||
|
||||
roundTrippedLicense, err := NewLicenseFromStorableLicense(storableLicense)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, license.ID, roundTrippedLicense.ID)
|
||||
assert.Equal(t, license.Key, roundTrippedLicense.Key)
|
||||
assert.Equal(t, license.Plan.Name, roundTrippedLicense.Plan.Name)
|
||||
assert.Equal(t, license.Status, roundTrippedLicense.Status)
|
||||
assert.Equal(t, license.State, roundTrippedLicense.State)
|
||||
assert.Equal(t, LicensePlatformCloud, roundTrippedLicense.Platform)
|
||||
assert.Equal(t, license.ValidFrom, roundTrippedLicense.ValidFrom)
|
||||
assert.Equal(t, license.ValidUntil, roundTrippedLicense.ValidUntil)
|
||||
|
||||
assert.ErrorContains(t, roundTrippedLicense.ErrIfCloud(), "not supported for licenses managed by SigNoz Cloud")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ var (
|
||||
// License State.
|
||||
LicenseStatusInvalid = valuer.NewString("invalid")
|
||||
|
||||
// License Platform.
|
||||
LicensePlatformCloud = valuer.NewString("cloud")
|
||||
LicensePlatformSelfHosted = valuer.NewString("self_hosted")
|
||||
|
||||
// Plan.
|
||||
PlanNameEnterprise = valuer.NewString("enterprise")
|
||||
PlanNameBasic = valuer.NewString("basic")
|
||||
|
||||
49
pkg/types/zeustypes/license.go
Normal file
49
pkg/types/zeustypes/license.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package zeustypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type LicenseFeature struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Usage int64 `json:"usage"`
|
||||
UsageLimit int64 `json:"usage_limit"`
|
||||
Route string `json:"route"`
|
||||
}
|
||||
|
||||
type LicensePlan struct {
|
||||
ID valuer.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LicenseEventQueue struct {
|
||||
Event string `json:"event"`
|
||||
Status string `json:"status"`
|
||||
ScheduledAt time.Time `json:"scheduled_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type License struct {
|
||||
ID valuer.UUID `json:"id"`
|
||||
Key string `json:"key"`
|
||||
ValidFrom int64 `json:"valid_from"`
|
||||
ValidUntil int64 `json:"valid_until"`
|
||||
Status string `json:"status"`
|
||||
State string `json:"state"`
|
||||
Platform string `json:"platform"`
|
||||
FreeUntil time.Time `json:"free_until"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PlanID valuer.UUID `json:"plan_id"`
|
||||
Plan LicensePlan `json:"plan"`
|
||||
Features []LicenseFeature `json:"features"`
|
||||
EventQueue LicenseEventQueue `json:"event_queue"`
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func New(_ context.Context, _ factory.ProviderSettings, _ zeus.Config) (zeus.Zeu
|
||||
return &provider{}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetLicense(_ context.Context, _ string) ([]byte, error) {
|
||||
func (provider *provider) GetLicense(_ context.Context, _ string) (*zeustypes.License, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "fetching license is not supported")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ var (
|
||||
|
||||
type Zeus interface {
|
||||
// Returns the license for the given key.
|
||||
GetLicense(context.Context, string) ([]byte, error)
|
||||
GetLicense(context.Context, string) (*zeustypes.License, error)
|
||||
|
||||
// Returns the checkout URL for the given license key.
|
||||
GetCheckoutURL(context.Context, string, []byte) ([]byte, error)
|
||||
|
||||
66
tests/fixtures/promqltestcorpus.py
vendored
66
tests/fixtures/promqltestcorpus.py
vendored
@@ -1,66 +0,0 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fixtures.metrics import Metrics
|
||||
|
||||
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "integration", "testdata", "promqltestcorpus")
|
||||
CORPUS_FILE = os.path.join(TESTDATA_DIR, "corpus.json")
|
||||
|
||||
# Datasets sit on disjoint time windows (2h gaps, far beyond the 5m lookback)
|
||||
# so one bulk ingest serves every case without cross-talk.
|
||||
ISOLATION_GAP_MS = 2 * 3600 * 1000
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "-Inf": -math.inf}
|
||||
|
||||
|
||||
def ingest_promqltest_corpus(insert_metrics: Callable[[list[Metrics]], None]) -> tuple[dict, dict[int, int]]:
|
||||
"""Loads the frozen corpus, lays its datasets end to end on the timeline
|
||||
(newest last, ending safely in the past), ingests every sample, and
|
||||
returns (corpus, dataset base timestamps).
|
||||
|
||||
Dataset bases are hour-aligned: registration rows are hour-bucketed, so
|
||||
behavior depends on where samples fall relative to hour boundaries, and
|
||||
exact known-divergences enforcement needs identical placement every run."""
|
||||
with open(CORPUS_FILE, encoding="utf-8") as f:
|
||||
corpus = json.load(f)
|
||||
|
||||
cases_by_dataset: dict[int, list[dict]] = {}
|
||||
for case in corpus["cases"]:
|
||||
cases_by_dataset.setdefault(case["dataset"], []).append(case)
|
||||
|
||||
spans = {}
|
||||
for ds in corpus["datasets"]:
|
||||
sample_max = max((s["samples"][-1][0] for s in ds["series"] if s["samples"]), default=0)
|
||||
case_max = max((c["end_ms"] for c in cases_by_dataset.get(ds["id"], [])), default=0)
|
||||
spans[ds["id"]] = max(sample_max, case_max) + corpus["meta"]["lookback_ms"]
|
||||
|
||||
hour_ms = 3_600_000
|
||||
advances = {ds["id"]: -(-(spans[ds["id"]] + ISOLATION_GAP_MS) // hour_ms) * hour_ms for ds in corpus["datasets"]}
|
||||
total = sum(advances.values())
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
cursor = (int((now - timedelta(hours=1)).timestamp() * 1000) - total) // hour_ms * hour_ms
|
||||
|
||||
bases: dict[int, int] = {}
|
||||
metrics: list[Metrics] = []
|
||||
for ds in corpus["datasets"]:
|
||||
bases[ds["id"]] = cursor
|
||||
for series in ds["series"]:
|
||||
labels = dict(series["labels"])
|
||||
metric_name = labels.pop("__name__")
|
||||
for off_ms, raw in series["samples"]:
|
||||
stale = raw == "stale"
|
||||
metrics.append(
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels=labels,
|
||||
timestamp=datetime.fromtimestamp((cursor + off_ms) / 1000, tz=UTC),
|
||||
value=0.0 if stale else (SPECIALS[raw] if isinstance(raw, str) else float(raw)),
|
||||
flags=1 if stale else 0,
|
||||
)
|
||||
)
|
||||
cursor += advances[ds["id"]]
|
||||
|
||||
insert_metrics(metrics)
|
||||
return corpus, bases
|
||||
@@ -1,138 +0,0 @@
|
||||
import json
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.promqltestcorpus import ingest_promqltest_corpus
|
||||
|
||||
# The same frozen corpus the promqlconformance package replays through
|
||||
# /api/v5/query_range, here replayed against the /prometheus/api/v1 endpoints
|
||||
# with clickhousev2 as the serving provider (see conftest.py) — the two paths
|
||||
# nothing else exercises. Range cases go to query_range, where a
|
||||
# RangeExecutor provider serves transpiled statements when the shape allows.
|
||||
# Instant cases go to /query with a real `time` parameter, so they need no
|
||||
# grid encoding.
|
||||
#
|
||||
# Prometheus API sample values are strings, "NaN"/"+Inf"/"-Inf" included.
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "+Inf": math.inf, "-Inf": -math.inf}
|
||||
QUERY_TIMEOUT = 30
|
||||
|
||||
|
||||
def test_prometheus_api_corpus(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
corpus, bases = ingest_promqltest_corpus(insert_metrics)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
failures: list[str] = []
|
||||
for case in corpus["cases"]:
|
||||
# instant-coarse variants encode an instant eval as a coarse-step
|
||||
# range because the v5 API cannot run true instants. This API can:
|
||||
# the [base] form of the same eval goes through /query below, and the
|
||||
# transpiled coarse-step serving the encoding exercises is covered
|
||||
# (and its known divergences ledgered) by promqlconformance's
|
||||
# clickhousev2 leg.
|
||||
if case["variant"] == "instant-coarse":
|
||||
continue
|
||||
|
||||
base = bases[case["dataset"]]
|
||||
start_ms = base + case["start_ms"]
|
||||
end_ms = base + case["end_ms"]
|
||||
step_s = max(1, case["step_ms"] // 1000)
|
||||
case_id = f"{case['source']}[{case['variant']}]"
|
||||
|
||||
if case["instant"]:
|
||||
path, params = "/prometheus/api/v1/query", {"query": case["expr"], "time": end_ms / 1000}
|
||||
else:
|
||||
path, params = (
|
||||
"/prometheus/api/v1/query_range",
|
||||
{
|
||||
"query": case["expr"],
|
||||
"start": start_ms / 1000,
|
||||
"end": end_ms / 1000,
|
||||
"step": step_s,
|
||||
},
|
||||
)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(path),
|
||||
params=params,
|
||||
timeout=QUERY_TIMEOUT,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
failures.append(f"{case_id}: HTTP {response.status_code} for {case['expr']!r}: {response.text[:200]}")
|
||||
continue
|
||||
body = response.json()
|
||||
if body.get("status") != "success":
|
||||
failures.append(f"{case_id}: status {body.get('status')!r} for {case['expr']!r}: {json.dumps(body)[:200]}")
|
||||
continue
|
||||
|
||||
result_type, result = body["data"]["resultType"], body["data"]["result"]
|
||||
actual: dict[tuple, dict[int, float]] = {}
|
||||
if result_type == "matrix":
|
||||
for series in result:
|
||||
points = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v) for ts, v in series.get("values") or []}
|
||||
actual[tuple(sorted((series.get("metric") or {}).items()))] = points
|
||||
elif result_type == "vector":
|
||||
for series in result:
|
||||
ts, v = series["value"]
|
||||
actual[tuple(sorted((series.get("metric") or {}).items()))] = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v)}
|
||||
elif result_type == "scalar":
|
||||
ts, v = result
|
||||
actual[()] = {round(float(ts) * 1000): SPECIALS[v] if v in SPECIALS else float(v)}
|
||||
|
||||
expected: dict[tuple, dict[int, float]] = {}
|
||||
for res in case["expected"]:
|
||||
points = {base + off_ms: SPECIALS[v] if isinstance(v, str) else float(v) for off_ms, v in res["points"]}
|
||||
expected[tuple(sorted(res["labels"].items()))] = points
|
||||
|
||||
if set(actual) != set(expected):
|
||||
missing = set(expected) - set(actual)
|
||||
extra = set(actual) - set(expected)
|
||||
failures.append(f"{case_id}: series mismatch for {case['expr']!r} (missing={sorted(missing)[:3]} extra={sorted(extra)[:3]})")
|
||||
continue
|
||||
|
||||
mismatch = None
|
||||
for lset, exp_points in expected.items():
|
||||
act_points = actual[lset]
|
||||
if set(act_points) != set(exp_points):
|
||||
mismatch = f"{case_id}: timestamp mismatch for {case['expr']!r} series {dict(lset)} (expected {len(exp_points)} points, got {len(act_points)})"
|
||||
break
|
||||
for ts, exp_v in exp_points.items():
|
||||
act_v = act_points[ts]
|
||||
if math.isnan(act_v) or math.isnan(exp_v):
|
||||
close = math.isnan(act_v) and math.isnan(exp_v)
|
||||
elif math.isinf(act_v) or math.isinf(exp_v):
|
||||
close = act_v == exp_v
|
||||
elif act_v == exp_v:
|
||||
close = True
|
||||
else:
|
||||
# Expected values carry the v5 API's rounding (>=1: three
|
||||
# decimal places; <1: three significant digits); this API
|
||||
# returns raw floats. One rounding quantum covers the
|
||||
# largest possible rounding difference.
|
||||
scale = max(abs(act_v), abs(exp_v))
|
||||
if scale >= 1:
|
||||
quantum = max(1e-3, scale * 1e-9)
|
||||
else:
|
||||
quantum = 10 ** (math.floor(math.log10(scale)) - 2)
|
||||
close = abs(act_v - exp_v) <= quantum + 1e-12
|
||||
if not close:
|
||||
mismatch = f"{case_id}: value mismatch for {case['expr']!r} series {dict(lset)} at {ts}: expected {exp_v}, got {act_v}"
|
||||
break
|
||||
if mismatch:
|
||||
break
|
||||
if mismatch:
|
||||
failures.append(mismatch)
|
||||
|
||||
for f_line in failures:
|
||||
print("DIVERGED", f_line)
|
||||
assert not failures, f"{len(failures)} corpus cases diverged:\n" + "\n".join(failures[:25])
|
||||
@@ -1,37 +0,0 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_promapi_v2(
|
||||
network: Network,
|
||||
migrator: types.Operation, # pylint: disable=unused-argument
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""
|
||||
SigNoz with clickhousev2 as the serving prometheus provider. The corpus
|
||||
replays against the /prometheus/api/v1 endpoints, so this package covers
|
||||
the two paths nothing else serves: v2 as the provider (range queries
|
||||
transpile when the shape allows), and the Prometheus HTTP API contract.
|
||||
"""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-promapi-v2",
|
||||
env_overrides={
|
||||
"SIGNOZ_PROMETHEUS_PROVIDER": "clickhousev2",
|
||||
},
|
||||
)
|
||||
@@ -2,21 +2,21 @@ import json
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.promqltestcorpus import ingest_promqltest_corpus
|
||||
from fixtures.querier import get_all_series, make_query_request
|
||||
|
||||
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "testdata")
|
||||
# The corpus (see fixtures/promqltestcorpus.py) is frozen from Prometheus' own
|
||||
# promql/promqltest testdata by scripts/promqltestcorpus (upstream load scripts
|
||||
# + the vendored reference engine). Unlike live-vs-live parity suites, the
|
||||
# oracle is a committed file, so the suite keeps working when the serving path
|
||||
# itself is the thing being changed — the one situation where comparing two
|
||||
# live paths against each other is blind.
|
||||
# Frozen corpus extracted from Prometheus' own promql/promqltest testdata by
|
||||
# scripts/promqltestcorpus (upstream load scripts + the vendored reference engine).
|
||||
# Unlike live-vs-live parity suites, the oracle is this committed file, so the suite
|
||||
# keeps working when the serving path itself is the thing being changed — the one
|
||||
# situation where comparing two live paths against each other is blind.
|
||||
CORPUS_FILE = os.path.join(TESTDATA_DIR, "promqltestcorpus", "corpus.json")
|
||||
|
||||
# One ledger per leg, enforced exactly in both directions. The default leg's
|
||||
# ledger is empty and pinned there; the clickhousev2 ledger is the rollout
|
||||
@@ -40,6 +40,9 @@ LEGS: list[tuple[str, dict | None]] = [
|
||||
("clickhousev2", {"X-SigNoz-PromQL-Provider": "clickhousev2"}),
|
||||
]
|
||||
|
||||
# Datasets sit on disjoint time windows (2h gaps, far beyond the 5m lookback) so
|
||||
# one bulk ingest serves every case without cross-talk.
|
||||
ISOLATION_GAP_MS = 2 * 3600 * 1000
|
||||
SPECIALS = {"NaN": math.nan, "Inf": math.inf, "-Inf": -math.inf}
|
||||
|
||||
|
||||
@@ -49,7 +52,51 @@ def test_upstream_promqltest_corpus(
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
corpus, bases = ingest_promqltest_corpus(insert_metrics)
|
||||
with open(CORPUS_FILE, encoding="utf-8") as f:
|
||||
corpus = json.load(f)
|
||||
|
||||
cases_by_dataset: dict[int, list[dict]] = {}
|
||||
for case in corpus["cases"]:
|
||||
cases_by_dataset.setdefault(case["dataset"], []).append(case)
|
||||
|
||||
# Lay datasets end to end on the timeline, newest last, ending safely in
|
||||
# the past; spans are per-dataset so the whole corpus stays within days.
|
||||
spans = {}
|
||||
for ds in corpus["datasets"]:
|
||||
sample_max = max((s["samples"][-1][0] for s in ds["series"] if s["samples"]), default=0)
|
||||
case_max = max((c["end_ms"] for c in cases_by_dataset.get(ds["id"], [])), default=0)
|
||||
spans[ds["id"]] = max(sample_max, case_max) + corpus["meta"]["lookback_ms"]
|
||||
|
||||
# Hour-aligned dataset bases: registration rows are hour-bucketed, so
|
||||
# behavior depends on where samples fall relative to hour boundaries —
|
||||
# the exact known-divergences enforcement needs that identical every run.
|
||||
hour_ms = 3_600_000
|
||||
advances = {ds["id"]: -(-(spans[ds["id"]] + ISOLATION_GAP_MS) // hour_ms) * hour_ms for ds in corpus["datasets"]}
|
||||
total = sum(advances.values())
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
cursor = (int((now - timedelta(hours=1)).timestamp() * 1000) - total) // hour_ms * hour_ms
|
||||
|
||||
bases: dict[int, int] = {}
|
||||
metrics: list[Metrics] = []
|
||||
for ds in corpus["datasets"]:
|
||||
bases[ds["id"]] = cursor
|
||||
for series in ds["series"]:
|
||||
labels = dict(series["labels"])
|
||||
metric_name = labels.pop("__name__")
|
||||
for off_ms, raw in series["samples"]:
|
||||
stale = raw == "stale"
|
||||
metrics.append(
|
||||
Metrics(
|
||||
metric_name=metric_name,
|
||||
labels=labels,
|
||||
timestamp=datetime.fromtimestamp((cursor + off_ms) / 1000, tz=UTC),
|
||||
value=0.0 if stale else (SPECIALS[raw] if isinstance(raw, str) else float(raw)),
|
||||
flags=1 if stale else 0,
|
||||
)
|
||||
)
|
||||
cursor += advances[ds["id"]]
|
||||
|
||||
insert_metrics(metrics)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
failures: dict[str, list[str]] = {leg: [] for leg, _ in LEGS}
|
||||
|
||||
Reference in New Issue
Block a user