mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-09 03:02:20 +00:00
Compare commits
1 Commits
gateway-ap
...
feat/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
824c2c53a7 |
@@ -3928,9 +3928,19 @@ components:
|
||||
isMonotonic:
|
||||
type: boolean
|
||||
temporality:
|
||||
$ref: '#/components/schemas/MetrictypesTemporality'
|
||||
enum:
|
||||
- delta
|
||||
- cumulative
|
||||
- unspecified
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/MetrictypesType'
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
@@ -3953,7 +3963,13 @@ components:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type:
|
||||
$ref: '#/components/schemas/MetrictypesType'
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
@@ -4014,11 +4030,6 @@ components:
|
||||
- percentage
|
||||
- totalValue
|
||||
type: object
|
||||
MetricsexplorertypesTreemapMode:
|
||||
enum:
|
||||
- timeseries
|
||||
- samples
|
||||
type: string
|
||||
MetricsexplorertypesTreemapRequest:
|
||||
properties:
|
||||
end:
|
||||
@@ -4029,7 +4040,10 @@ components:
|
||||
limit:
|
||||
type: integer
|
||||
mode:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapMode'
|
||||
enum:
|
||||
- timeseries
|
||||
- samples
|
||||
type: string
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
@@ -4064,9 +4078,19 @@ components:
|
||||
metricName:
|
||||
type: string
|
||||
temporality:
|
||||
$ref: '#/components/schemas/MetrictypesTemporality'
|
||||
enum:
|
||||
- delta
|
||||
- cumulative
|
||||
- unspecified
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/MetrictypesType'
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
@@ -4077,20 +4101,6 @@ components:
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
MetrictypesTemporality:
|
||||
enum:
|
||||
- delta
|
||||
- cumulative
|
||||
- unspecified
|
||||
type: string
|
||||
MetrictypesType:
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
PreferencetypesPreference:
|
||||
properties:
|
||||
allowedScopes:
|
||||
|
||||
@@ -762,6 +762,18 @@ export interface MetricsexplorertypesMetricHighlightsResponseDTO {
|
||||
totalTimeSeries: number;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesMetricMetadataDTOTemporality {
|
||||
delta = 'delta',
|
||||
cumulative = 'cumulative',
|
||||
unspecified = 'unspecified',
|
||||
}
|
||||
export enum MetricsexplorertypesMetricMetadataDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesMetricMetadataDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -771,14 +783,29 @@ export interface MetricsexplorertypesMetricMetadataDTO {
|
||||
* @type boolean
|
||||
*/
|
||||
isMonotonic: boolean;
|
||||
temporality: MetrictypesTemporalityDTO;
|
||||
type: MetrictypesTypeDTO;
|
||||
/**
|
||||
* @enum delta,cumulative,unspecified
|
||||
* @type string
|
||||
*/
|
||||
temporality: MetricsexplorertypesMetricMetadataDTOTemporality;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesMetricMetadataDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesStatDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesStatDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -798,7 +825,11 @@ export interface MetricsexplorertypesStatDTO {
|
||||
* @minimum 0
|
||||
*/
|
||||
timeseries: number;
|
||||
type: MetrictypesTypeDTO;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesStatDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -858,7 +889,7 @@ export interface MetricsexplorertypesTreemapEntryDTO {
|
||||
totalValue: number;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesTreemapModeDTO {
|
||||
export enum MetricsexplorertypesTreemapRequestDTOMode {
|
||||
timeseries = 'timeseries',
|
||||
samples = 'samples',
|
||||
}
|
||||
@@ -873,7 +904,11 @@ export interface MetricsexplorertypesTreemapRequestDTO {
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
mode: MetricsexplorertypesTreemapModeDTO;
|
||||
/**
|
||||
* @enum timeseries,samples
|
||||
* @type string
|
||||
*/
|
||||
mode: MetricsexplorertypesTreemapRequestDTOMode;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
@@ -894,6 +929,18 @@ export interface MetricsexplorertypesTreemapResponseDTO {
|
||||
timeseries: MetricsexplorertypesTreemapEntryDTO[] | null;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality {
|
||||
delta = 'delta',
|
||||
cumulative = 'cumulative',
|
||||
unspecified = 'unspecified',
|
||||
}
|
||||
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -907,26 +954,22 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
* @type string
|
||||
*/
|
||||
metricName: string;
|
||||
temporality: MetrictypesTemporalityDTO;
|
||||
type: MetrictypesTypeDTO;
|
||||
/**
|
||||
* @enum delta,cumulative,unspecified
|
||||
* @type string
|
||||
*/
|
||||
temporality: MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesUpdateMetricMetadataRequestDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export enum MetrictypesTemporalityDTO {
|
||||
delta = 'delta',
|
||||
cumulative = 'cumulative',
|
||||
unspecified = 'unspecified',
|
||||
}
|
||||
export enum MetrictypesTypeDTO {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface PreferencetypesPreferenceDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -1345,7 +1388,7 @@ export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
email?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -1353,7 +1396,7 @@ export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
orgId?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableInviteDTO {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
@@ -26,20 +27,12 @@ import {
|
||||
} from 'antd';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { CollapseProps } from 'antd/lib';
|
||||
import {
|
||||
useCreateIngestionKey,
|
||||
useCreateIngestionKeyLimit,
|
||||
useDeleteIngestionKey,
|
||||
useDeleteIngestionKeyLimit,
|
||||
useGetIngestionKeys,
|
||||
useSearchIngestionKeys,
|
||||
useUpdateIngestionKey,
|
||||
useUpdateIngestionKeyLimit,
|
||||
} from 'api/generated/services/gateway';
|
||||
import {
|
||||
GatewaytypesIngestionKeyDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import createIngestionKeyApi from 'api/IngestionKeys/createIngestionKey';
|
||||
import deleteIngestionKey from 'api/IngestionKeys/deleteIngestionKey';
|
||||
import createLimitForIngestionKeyApi from 'api/IngestionKeys/limits/createLimitsForKey';
|
||||
import deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsForIngestionKey';
|
||||
import updateLimitForIngestionKeyApi from 'api/IngestionKeys/limits/updateLimitsForIngestionKey';
|
||||
import updateIngestionKey from 'api/IngestionKeys/updateIngestionKey';
|
||||
import { AxiosError } from 'axios';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import Tags from 'components/Tags/Tags';
|
||||
@@ -51,6 +44,7 @@ import ROUTES from 'constants/routes';
|
||||
import { INITIAL_ALERT_THRESHOLD_STATE } from 'container/CreateAlertV2/context/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
|
||||
@@ -72,12 +66,16 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ErrorResponse } from 'types/api';
|
||||
import {
|
||||
AddLimitProps,
|
||||
LimitProps,
|
||||
UpdateLimitProps,
|
||||
} from 'types/api/ingestionKeys/limits/types';
|
||||
import { PaginationProps } from 'types/api/ingestionKeys/types';
|
||||
import {
|
||||
IngestionKeyProps,
|
||||
PaginationProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
import { MeterAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { getDaysUntilExpiry } from 'utils/timeUtils';
|
||||
@@ -113,8 +111,6 @@ export const showErrorNotification = (
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: err.message || SOMETHING_WENT_WRONG,
|
||||
description: (err as AxiosError<RenderErrorResponseDTO>).response?.data?.error
|
||||
?.message,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,17 +163,12 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const [updatedTags, setUpdatedTags] = useState<string[]>([]);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isEditAddLimitOpen, setIsEditAddLimitOpen] = useState(false);
|
||||
const [
|
||||
activeAPIKey,
|
||||
setActiveAPIKey,
|
||||
] = useState<GatewaytypesIngestionKeyDTO | null>();
|
||||
const [activeAPIKey, setActiveAPIKey] = useState<IngestionKeyProps | null>();
|
||||
const [activeSignal, setActiveSignal] = useState<LimitProps | null>(null);
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [dataSource, setDataSource] = useState<GatewaytypesIngestionKeyDTO[]>(
|
||||
[],
|
||||
);
|
||||
const [dataSource, setDataSource] = useState<IngestionKeyProps[]>([]);
|
||||
const [paginationParams, setPaginationParams] = useState<PaginationProps>({
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
@@ -195,7 +186,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const [
|
||||
createLimitForIngestionKeyError,
|
||||
setCreateLimitForIngestionKeyError,
|
||||
] = useState<string | null>(null);
|
||||
] = useState<ErrorResponse | null>(null);
|
||||
|
||||
const [
|
||||
hasUpdateLimitForIngestionKeyError,
|
||||
@@ -205,7 +196,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const [
|
||||
updateLimitForIngestionKeyError,
|
||||
setUpdateLimitForIngestionKeyError,
|
||||
] = useState<string | null>(null);
|
||||
] = useState<ErrorResponse | null>(null);
|
||||
|
||||
const { t } = useTranslation(['ingestionKeys']);
|
||||
|
||||
@@ -225,7 +216,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
handleFormReset();
|
||||
};
|
||||
|
||||
const showDeleteModal = (apiKey: GatewaytypesIngestionKeyDTO): void => {
|
||||
const showDeleteModal = (apiKey: IngestionKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
@@ -242,7 +233,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
setIsAddModalOpen(false);
|
||||
};
|
||||
|
||||
const showEditModal = (apiKey: GatewaytypesIngestionKeyDTO): void => {
|
||||
const showEditModal = (apiKey: IngestionKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
handleFormReset();
|
||||
setUpdatedTags(apiKey.tags || []);
|
||||
@@ -267,64 +258,25 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
setActiveSignal(null);
|
||||
};
|
||||
|
||||
// Use search API when searchText is present, otherwise use normal get API
|
||||
const isSearching = searchText.length > 0;
|
||||
|
||||
const {
|
||||
data: getIngestionKeysData,
|
||||
isLoading: isLoadingGet,
|
||||
isRefetching: isRefetchingGet,
|
||||
refetch: refetchGetAPIKeys,
|
||||
error: getError,
|
||||
isError: isGetError,
|
||||
} = useGetIngestionKeys(
|
||||
{
|
||||
...paginationParams,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !isSearching,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: searchIngestionKeysData,
|
||||
isLoading: isLoadingSearch,
|
||||
isRefetching: isRefetchingSearch,
|
||||
refetch: refetchSearchAPIKeys,
|
||||
error: searchError,
|
||||
isError: isSearchError,
|
||||
} = useSearchIngestionKeys(
|
||||
{
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
name: searchText,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: isSearching,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Use the appropriate data based on which API is active
|
||||
const IngestionKeys = isSearching
|
||||
? searchIngestionKeysData
|
||||
: getIngestionKeysData;
|
||||
const isLoading = isSearching ? isLoadingSearch : isLoadingGet;
|
||||
const isRefetching = isSearching ? isRefetchingSearch : isRefetchingGet;
|
||||
const refetchAPIKeys = isSearching ? refetchSearchAPIKeys : refetchGetAPIKeys;
|
||||
const error = isSearching ? searchError : getError;
|
||||
const isError = isSearching ? isSearchError : isGetError;
|
||||
data: IngestionKeys,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
refetch: refetchAPIKeys,
|
||||
error,
|
||||
isError,
|
||||
} = useGetAllIngestionsKeys({
|
||||
search: searchText,
|
||||
...paginationParams,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setActiveAPIKey(IngestionKeys?.data.data?.keys?.[0]);
|
||||
setActiveAPIKey(IngestionKeys?.data.data[0]);
|
||||
}, [IngestionKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(IngestionKeys?.data.data?.keys || []);
|
||||
setTotalIngestionKeys(IngestionKeys?.data?.data?._pagination?.total || 0);
|
||||
setDataSource(IngestionKeys?.data.data || []);
|
||||
setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [IngestionKeys?.data?.data]);
|
||||
|
||||
@@ -345,7 +297,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const clearSearch = (): void => {
|
||||
setSearchValue('');
|
||||
setSearchText('');
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -358,27 +309,21 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const {
|
||||
mutate: createIngestionKey,
|
||||
isLoading: isLoadingCreateAPIKey,
|
||||
} = useCreateIngestionKey({
|
||||
mutation: {
|
||||
onSuccess: (_data) => {
|
||||
// The new API returns GatewaytypesGettableCreatedIngestionKeyDTO with only id and value
|
||||
// We rely on refetchAPIKeys to get the full key object
|
||||
setActiveAPIKey(null);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
} = useMutation(createIngestionKeyApi, {
|
||||
onSuccess: (data) => {
|
||||
setActiveAPIKey(data.payload);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateAPIKey,
|
||||
isLoading: isLoadingUpdateAPIKey,
|
||||
} = useUpdateIngestionKey({
|
||||
mutation: {
|
||||
const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation(
|
||||
updateIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsEditModalOpen(false);
|
||||
@@ -387,13 +332,11 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: deleteAPIKey,
|
||||
isLoading: isDeleteingAPIKey,
|
||||
} = useDeleteIngestionKey({
|
||||
mutation: {
|
||||
const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation(
|
||||
deleteIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsDeleteModalOpen(false);
|
||||
@@ -402,59 +345,49 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: createLimitForIngestionKey,
|
||||
isLoading: isLoadingLimitForKey,
|
||||
} = useCreateIngestionKeyLimit({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
},
|
||||
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
|
||||
setHasCreateLimitForIngestionKeyError(true);
|
||||
setCreateLimitForIngestionKeyError(
|
||||
error.response?.data?.error?.message || 'Failed to create limit',
|
||||
);
|
||||
},
|
||||
} = useMutation(createLimitForIngestionKeyApi, {
|
||||
onSuccess: () => {
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
},
|
||||
onError: (error: ErrorResponse) => {
|
||||
setHasCreateLimitForIngestionKeyError(true);
|
||||
setCreateLimitForIngestionKeyError(error);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateLimitForIngestionKey,
|
||||
isLoading: isLoadingUpdatedLimitForKey,
|
||||
} = useUpdateIngestionKeyLimit({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
},
|
||||
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
|
||||
setHasUpdateLimitForIngestionKeyError(true);
|
||||
setUpdateLimitForIngestionKeyError(
|
||||
error.response?.data?.error?.message || 'Failed to update limit',
|
||||
);
|
||||
},
|
||||
} = useMutation(updateLimitForIngestionKeyApi, {
|
||||
onSuccess: () => {
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
},
|
||||
onError: (error: ErrorResponse) => {
|
||||
setHasUpdateLimitForIngestionKeyError(true);
|
||||
setUpdateLimitForIngestionKeyError(error);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: deleteLimitForKey,
|
||||
isLoading: isDeletingLimit,
|
||||
} = useDeleteIngestionKeyLimit({
|
||||
mutation: {
|
||||
const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation(
|
||||
deleteLimitsForIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setIsDeleteLimitModalOpen(false);
|
||||
@@ -464,15 +397,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
clearSearch();
|
||||
|
||||
if (activeAPIKey && activeAPIKey.id) {
|
||||
deleteAPIKey({
|
||||
pathParams: { keyId: activeAPIKey.id },
|
||||
});
|
||||
if (activeAPIKey) {
|
||||
deleteAPIKey(activeAPIKey.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -480,15 +411,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
editForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (activeAPIKey && activeAPIKey.id) {
|
||||
if (activeAPIKey) {
|
||||
updateAPIKey({
|
||||
pathParams: { keyId: activeAPIKey.id },
|
||||
id: activeAPIKey.id,
|
||||
data: {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: new Date(
|
||||
dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
),
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -506,12 +435,10 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const requestPayload = {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: new Date(dayjs(values.expires_at).endOf('day').toISOString()),
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
};
|
||||
|
||||
createIngestionKey({
|
||||
data: requestPayload,
|
||||
});
|
||||
createIngestionKey(requestPayload);
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -538,7 +465,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
formatTimezoneAdjustedTimestamp(date, DATE_TIME_FORMATS.UTC_MONTH_COMPACT);
|
||||
|
||||
const showDeleteLimitModal = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
limit: LimitProps,
|
||||
): void => {
|
||||
setActiveAPIKey(APIKey);
|
||||
@@ -562,7 +489,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
const handleAddLimit = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signalName: string,
|
||||
): void => {
|
||||
const {
|
||||
@@ -575,7 +502,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
} = addEditLimitForm.getFieldsValue();
|
||||
|
||||
const payload: AddLimitProps = {
|
||||
keyID: APIKey.id || '',
|
||||
keyID: APIKey.id,
|
||||
signal: signalName,
|
||||
config: {},
|
||||
};
|
||||
@@ -649,17 +576,11 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
createLimitForIngestionKey({
|
||||
pathParams: { keyId: payload.keyID },
|
||||
data: {
|
||||
signal: payload.signal,
|
||||
config: payload.config,
|
||||
},
|
||||
});
|
||||
createLimitForIngestionKey(payload);
|
||||
};
|
||||
|
||||
const handleUpdateLimit = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
const {
|
||||
@@ -723,12 +644,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
updateLimitForIngestionKey({
|
||||
pathParams: { limitId: payload.limitID },
|
||||
data: {
|
||||
config: payload.config,
|
||||
},
|
||||
});
|
||||
updateLimitForIngestionKey(payload);
|
||||
};
|
||||
/* eslint-enable sonarjs/cognitive-complexity */
|
||||
|
||||
@@ -740,7 +656,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
};
|
||||
|
||||
const enableEditLimitMode = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
const dayCount = signal?.config?.day?.count;
|
||||
@@ -787,16 +703,14 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const onDeleteLimitHandler = (): void => {
|
||||
if (activeSignal && activeSignal.id) {
|
||||
deleteLimitForKey({
|
||||
pathParams: { limitId: activeSignal.id },
|
||||
});
|
||||
deleteLimitForKey(activeSignal.id);
|
||||
}
|
||||
};
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const handleCreateAlert = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
let metricName = '';
|
||||
@@ -857,41 +771,31 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
history.push(URL);
|
||||
};
|
||||
|
||||
const columns: AntDTableProps<GatewaytypesIngestionKeyDTO>['columns'] = [
|
||||
const columns: AntDTableProps<IngestionKeyProps>['columns'] = [
|
||||
{
|
||||
title: 'Ingestion Key',
|
||||
key: 'ingestion-key',
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
render: (APIKey: GatewaytypesIngestionKeyDTO): JSX.Element => {
|
||||
const createdOn = APIKey?.created_at
|
||||
? getFormattedTime(
|
||||
dayjs(APIKey.created_at).toISOString(),
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
)
|
||||
: '';
|
||||
render: (APIKey: IngestionKeyProps): JSX.Element => {
|
||||
const createdOn = getFormattedTime(
|
||||
APIKey.created_at,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
);
|
||||
|
||||
const expiresOn =
|
||||
!APIKey?.expires_at ||
|
||||
dayjs(APIKey?.expires_at).toISOString() === '0001-01-01T00:00:00.000Z'
|
||||
!APIKey?.expires_at || APIKey?.expires_at === '0001-01-01T00:00:00Z'
|
||||
? 'No Expiry'
|
||||
: getFormattedTime(
|
||||
dayjs(APIKey?.expires_at).toISOString(),
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
);
|
||||
: getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp);
|
||||
|
||||
const updatedOn = APIKey?.updated_at
|
||||
? getFormattedTime(
|
||||
dayjs(APIKey.updated_at).toISOString(),
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
)
|
||||
: '';
|
||||
const updatedOn = getFormattedTime(
|
||||
APIKey?.updated_at,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
);
|
||||
|
||||
// Convert array of limits to a dictionary for quick access
|
||||
const limitsDict: Record<string, LimitProps> = {};
|
||||
APIKey.limits?.forEach((limitItem) => {
|
||||
if (limitItem.signal && limitItem.id) {
|
||||
limitsDict[limitItem.signal] = limitItem as LimitProps;
|
||||
}
|
||||
APIKey.limits?.forEach((limitItem: LimitProps) => {
|
||||
limitsDict[limitItem.signal] = limitItem;
|
||||
});
|
||||
|
||||
const hasLimits = (signalName: string): boolean => !!limitsDict[signalName];
|
||||
@@ -908,10 +812,8 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="ingestion-key-value">
|
||||
<Typography.Text>
|
||||
{APIKey?.value?.substring(0, 2)}********
|
||||
{APIKey?.value
|
||||
?.substring(APIKey?.value?.length ? APIKey.value.length - 2 : 0)
|
||||
.trim()}
|
||||
{APIKey?.value.substring(0, 2)}********
|
||||
{APIKey?.value.substring(APIKey.value.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
@@ -920,9 +822,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (APIKey?.value) {
|
||||
handleCopyKey(APIKey.value);
|
||||
}
|
||||
handleCopyKey(APIKey.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -954,7 +854,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<Row>
|
||||
<Col span={6}> ID </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{APIKey?.id}</Typography.Text>
|
||||
<Typography.Text>{APIKey.id}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -1016,9 +916,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -1028,9 +926,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -1044,7 +940,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
size="small"
|
||||
shape="round"
|
||||
icon={<PlusIcon size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey?.id && activeSignal)}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -1062,7 +958,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="signal-limit-values">
|
||||
{activeAPIKey?.id === APIKey?.id &&
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal?.signal === signalName &&
|
||||
isEditAddLimitOpen ? (
|
||||
<Form
|
||||
@@ -1258,27 +1154,27 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeAPIKey?.id === APIKey?.id &&
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasCreateLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError && (
|
||||
createLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{createLimitForIngestionKeyError}
|
||||
{createLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey?.id &&
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasUpdateLimitForIngestionKeyError &&
|
||||
updateLimitForIngestionKeyError && (
|
||||
updateLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{updateLimitForIngestionKeyError}
|
||||
{updateLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey?.id &&
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signalName &&
|
||||
isEditAddLimitOpen && (
|
||||
<div className="signal-limit-save-discard">
|
||||
@@ -1594,7 +1490,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
showHeader={false}
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
pageSize: isSearching ? 100 : paginationParams?.per_page,
|
||||
pageSize: paginationParams?.per_page,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} Ingestion keys`,
|
||||
|
||||
@@ -86,34 +86,32 @@ describe('MultiIngestionSettings Page', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
// Arrange API response with a metrics daily count limit so the alert button is visible
|
||||
const response = {
|
||||
const response: TestAllIngestionKeyProps = {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key One',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
id: 'l1',
|
||||
signal: 'metrics',
|
||||
config: { day: { count: 1000 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: 'Key One',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
id: 'l1',
|
||||
signal: 'metrics',
|
||||
config: { day: { count: 1000 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
};
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v2/gateway/ingestion_keys*', (_req, res, ctx) =>
|
||||
rest.get('*/workspaces/me/keys*', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(response)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,16 +2,13 @@ import { useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetIngestionKeys } from 'api/generated/services/gateway';
|
||||
import {
|
||||
GatewaytypesIngestionKeyDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DOCS_BASE_URL } from 'constants/app';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowUpRight, Copy, Info, Key, TriangleAlert } from 'lucide-react';
|
||||
import { IngestionKeyProps } from 'types/api/ingestionKeys/types';
|
||||
|
||||
import './IngestionDetails.styles.scss';
|
||||
|
||||
@@ -42,17 +39,17 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const [
|
||||
firstIngestionKey,
|
||||
setFirstIngestionKey,
|
||||
] = useState<GatewaytypesIngestionKeyDTO>({} as GatewaytypesIngestionKeyDTO);
|
||||
const [firstIngestionKey, setFirstIngestionKey] = useState<IngestionKeyProps>(
|
||||
{} as IngestionKeyProps,
|
||||
);
|
||||
|
||||
const {
|
||||
data: ingestionKeys,
|
||||
isLoading: isIngestionKeysLoading,
|
||||
error,
|
||||
isError,
|
||||
} = useGetIngestionKeys({
|
||||
} = useGetAllIngestionsKeys({
|
||||
search: '',
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
});
|
||||
@@ -72,11 +69,8 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
ingestionKeys?.data?.data?.keys &&
|
||||
ingestionKeys?.data.data.keys.length > 0
|
||||
) {
|
||||
setFirstIngestionKey(ingestionKeys?.data.data.keys[0]);
|
||||
if (ingestionKeys?.data.data && ingestionKeys?.data.data.length > 0) {
|
||||
setFirstIngestionKey(ingestionKeys?.data.data[0]);
|
||||
}
|
||||
}, [ingestionKeys]);
|
||||
|
||||
@@ -86,8 +80,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
<div className="ingestion-endpoint-section-error-container">
|
||||
<Typography.Text className="ingestion-endpoint-section-error-text error">
|
||||
<TriangleAlert size={14} />{' '}
|
||||
{(error as AxiosError<RenderErrorResponseDTO>)?.response?.data?.error
|
||||
?.message || 'Something went wrong'}
|
||||
{(error as AxiosError)?.message || 'Something went wrong'}
|
||||
</Typography.Text>
|
||||
|
||||
<div className="ingestion-setup-details-links">
|
||||
@@ -183,7 +176,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="ingestion-key-value-copy">
|
||||
{maskKey(firstIngestionKey?.value || '')}
|
||||
{maskKey(firstIngestionKey?.value)}
|
||||
|
||||
<Copy
|
||||
size={14}
|
||||
@@ -193,7 +186,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INGESTION_KEY_COPIED}`,
|
||||
{},
|
||||
);
|
||||
handleCopyKey(firstIngestionKey?.value || '');
|
||||
handleCopyKey(firstIngestionKey?.value);
|
||||
}}
|
||||
/>
|
||||
</Typography.Text>
|
||||
|
||||
@@ -151,7 +151,8 @@ func (c *AccountConfig) Value() (driver.Value, error) {
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't serialize cloud account config to JSON")
|
||||
}
|
||||
return serialized, nil
|
||||
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
|
||||
return string(serialized), nil
|
||||
}
|
||||
|
||||
type AgentReport struct {
|
||||
@@ -186,7 +187,8 @@ func (r *AgentReport) Value() (driver.Value, error) {
|
||||
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
|
||||
)
|
||||
}
|
||||
return serialized, nil
|
||||
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
|
||||
return string(serialized), nil
|
||||
}
|
||||
|
||||
type CloudIntegrationService struct {
|
||||
@@ -240,5 +242,6 @@ func (c *CloudServiceConfig) Value() (driver.Value, error) {
|
||||
err, errors.CodeInternal, "couldn't serialize cloud service config to JSON",
|
||||
)
|
||||
}
|
||||
return serialized, nil
|
||||
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytea
|
||||
return string(serialized), nil
|
||||
}
|
||||
|
||||
@@ -31,13 +31,6 @@ var (
|
||||
TreemapModeSamples = TreemapMode{valuer.NewString("samples")}
|
||||
)
|
||||
|
||||
func (TreemapMode) Enum() []any {
|
||||
return []any{
|
||||
TreemapModeTimeSeries,
|
||||
TreemapModeSamples,
|
||||
}
|
||||
}
|
||||
|
||||
// StatsRequest represents the payload accepted by the metrics stats endpoint.
|
||||
type StatsRequest struct {
|
||||
Filter *qbtypes.Filter `json:"filter,omitempty"`
|
||||
@@ -105,7 +98,7 @@ func (req *StatsRequest) UnmarshalJSON(data []byte) error {
|
||||
type Stat struct {
|
||||
MetricName string `json:"metricName" required:"true"`
|
||||
Description string `json:"description" required:"true"`
|
||||
MetricType metrictypes.Type `json:"type" required:"true"`
|
||||
MetricType metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
|
||||
MetricUnit string `json:"unit" required:"true"`
|
||||
TimeSeries uint64 `json:"timeseries" required:"true"`
|
||||
Samples uint64 `json:"samples" required:"true"`
|
||||
@@ -119,9 +112,9 @@ type StatsResponse struct {
|
||||
|
||||
type MetricMetadata struct {
|
||||
Description string `json:"description" required:"true"`
|
||||
MetricType metrictypes.Type `json:"type" required:"true"`
|
||||
MetricType metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
|
||||
MetricUnit string `json:"unit" required:"true"`
|
||||
Temporality metrictypes.Temporality `json:"temporality" required:"true"`
|
||||
Temporality metrictypes.Temporality `json:"temporality" required:"true" enum:"delta,cumulative,unspecified"`
|
||||
IsMonotonic bool `json:"isMonotonic" required:"true"`
|
||||
}
|
||||
|
||||
@@ -138,10 +131,10 @@ func (m *MetricMetadata) UnmarshalBinary(data []byte) error {
|
||||
// UpdateMetricMetadataRequest represents the payload for updating metric metadata.
|
||||
type UpdateMetricMetadataRequest struct {
|
||||
MetricName string `json:"metricName" required:"true"`
|
||||
Type metrictypes.Type `json:"type" required:"true"`
|
||||
Type metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
|
||||
Description string `json:"description" required:"true"`
|
||||
Unit string `json:"unit" required:"true"`
|
||||
Temporality metrictypes.Temporality `json:"temporality" required:"true"`
|
||||
Temporality metrictypes.Temporality `json:"temporality" required:"true" enum:"delta,cumulative,unspecified"`
|
||||
IsMonotonic bool `json:"isMonotonic" required:"true"`
|
||||
}
|
||||
|
||||
@@ -151,7 +144,7 @@ type TreemapRequest struct {
|
||||
Start int64 `json:"start" required:"true"`
|
||||
End int64 `json:"end" required:"true"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
Mode TreemapMode `json:"mode" required:"true"`
|
||||
Mode TreemapMode `json:"mode" required:"true" enum:"timeseries,samples"`
|
||||
}
|
||||
|
||||
// Validate enforces basic constraints on TreemapRequest.
|
||||
|
||||
@@ -36,7 +36,7 @@ func (t Temporality) Value() (driver.Value, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Temporality) Scan(src any) error {
|
||||
func (t *Temporality) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
*t = Unknown
|
||||
return nil
|
||||
@@ -66,14 +66,6 @@ func (t *Temporality) Scan(src any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Temporality) Enum() []any {
|
||||
return []any{
|
||||
Delta,
|
||||
Cumulative,
|
||||
Unspecified,
|
||||
}
|
||||
}
|
||||
|
||||
// Type is the type of the metric in OTLP data model
|
||||
// Read more here https://opentelemetry.io/docs/specs/otel/metrics/data-model/#metric-points
|
||||
type Type struct {
|
||||
@@ -142,16 +134,6 @@ var (
|
||||
UnspecifiedType = Type{valuer.NewString("")}
|
||||
)
|
||||
|
||||
func (Type) Enum() []any {
|
||||
return []any{
|
||||
GaugeType,
|
||||
SumType,
|
||||
HistogramType,
|
||||
SummaryType,
|
||||
ExpHistogramType,
|
||||
}
|
||||
}
|
||||
|
||||
type TimeAggregation struct {
|
||||
valuer.String
|
||||
}
|
||||
@@ -169,21 +151,6 @@ var (
|
||||
TimeAggregationIncrease = TimeAggregation{valuer.NewString("increase")}
|
||||
)
|
||||
|
||||
func (TimeAggregation) Enum() []any {
|
||||
return []any{
|
||||
TimeAggregationUnspecified,
|
||||
TimeAggregationLatest,
|
||||
TimeAggregationSum,
|
||||
TimeAggregationAvg,
|
||||
TimeAggregationMin,
|
||||
TimeAggregationMax,
|
||||
TimeAggregationCount,
|
||||
TimeAggregationCountDistinct,
|
||||
TimeAggregationRate,
|
||||
TimeAggregationIncrease,
|
||||
}
|
||||
}
|
||||
|
||||
type SpaceAggregation struct {
|
||||
valuer.String
|
||||
}
|
||||
@@ -202,22 +169,6 @@ var (
|
||||
SpaceAggregationPercentile99 = SpaceAggregation{valuer.NewString("p99")}
|
||||
)
|
||||
|
||||
func (SpaceAggregation) Enum() []any {
|
||||
return []any{
|
||||
SpaceAggregationUnspecified,
|
||||
SpaceAggregationSum,
|
||||
SpaceAggregationAvg,
|
||||
SpaceAggregationMin,
|
||||
SpaceAggregationMax,
|
||||
SpaceAggregationCount,
|
||||
SpaceAggregationPercentile50,
|
||||
SpaceAggregationPercentile75,
|
||||
SpaceAggregationPercentile90,
|
||||
SpaceAggregationPercentile95,
|
||||
SpaceAggregationPercentile99,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SpaceAggregation) IsPercentile() bool {
|
||||
return s == SpaceAggregationPercentile50 ||
|
||||
s == SpaceAggregationPercentile75 ||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import requests
|
||||
from sqlalchemy import text
|
||||
from wiremock.client import (
|
||||
HttpMethods,
|
||||
Mapping,
|
||||
MappingRequest,
|
||||
MappingResponse,
|
||||
WireMockMatchers,
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def cleanup_cloud_accounts(postgres: types.TestContainerSQL) -> None:
|
||||
try:
|
||||
with postgres.conn.connect() as conn:
|
||||
conn.execute(text("TRUNCATE TABLE cloud_integration CASCADE"))
|
||||
conn.commit()
|
||||
logger.info("Cleaned up cloud_integration table")
|
||||
except Exception as e:
|
||||
logger.info(f"Cleanup skipped: {str(e)[:100]}")
|
||||
|
||||
|
||||
def test_generate_connection_url(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test to generate connection URL for AWS CloudFormation stack deployment."""
|
||||
# Clean up any corrupted data from previous test runs
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
# Get authentication token for admin user
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
|
||||
# Mock the deployment info query (for license validation)
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts/generate-connection-url"
|
||||
|
||||
# Prepare request payload
|
||||
request_payload = {
|
||||
"account_config": {"regions": ["us-east-1", "us-west-2"]},
|
||||
"agent_config": {
|
||||
"region": "us-east-1",
|
||||
"ingestion_url": "https://ingest.test.signoz.cloud",
|
||||
"ingestion_key": "test-ingestion-key-123456",
|
||||
"signoz_api_url": "https://test-deployment.test.signoz.cloud",
|
||||
"signoz_api_key": "test-api-key-789",
|
||||
"version": "v0.0.8",
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=request_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
# Assert successful response
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}: {response.text}"
|
||||
|
||||
# Parse response JSON
|
||||
response_data = response.json()
|
||||
|
||||
# Assert response structure contains expected data
|
||||
assert "data" in response_data, "Response should contain 'data' field"
|
||||
|
||||
# Assert required fields in the response data
|
||||
expected_fields = ["account_id", "connection_url"]
|
||||
|
||||
for field in expected_fields:
|
||||
assert (
|
||||
field in response_data["data"]
|
||||
), f"Response data should contain '{field}' field"
|
||||
|
||||
data = response_data["data"]
|
||||
|
||||
# Assert account_id is a valid UUID format
|
||||
assert (
|
||||
len(data["account_id"]) > 0
|
||||
), "account_id should be a non-empty string (UUID)"
|
||||
|
||||
# Assert connection_url contains expected CloudFormation parameters
|
||||
connection_url = data["connection_url"]
|
||||
|
||||
# Verify it's an AWS CloudFormation URL
|
||||
assert (
|
||||
"console.aws.amazon.com/cloudformation" in connection_url
|
||||
), "connection_url should be an AWS CloudFormation URL"
|
||||
|
||||
# Verify region is included
|
||||
assert (
|
||||
"region=us-east-1" in connection_url
|
||||
), "connection_url should contain the specified region"
|
||||
|
||||
# Verify required parameters are in the URL
|
||||
required_params = [
|
||||
"param_SigNozIntegrationAgentVersion=v0.0.8",
|
||||
"param_SigNozApiUrl=https%3A%2F%2Ftest-deployment.test.signoz.cloud",
|
||||
"param_SigNozApiKey=test-api-key-789",
|
||||
"param_SigNozAccountId=", # Will be a UUID
|
||||
"param_IngestionUrl=https%3A%2F%2Fingest.test.signoz.cloud",
|
||||
"param_IngestionKey=test-ingestion-key-123456",
|
||||
"stackName=signoz-integration",
|
||||
"templateURL=https%3A%2F%2Fsignoz-integrations.s3.us-east-1.amazonaws.com%2Faws-quickcreate-template-v0.0.8.json",
|
||||
]
|
||||
|
||||
for param in required_params:
|
||||
assert (
|
||||
param in connection_url
|
||||
), f"connection_url should contain parameter: {param}"
|
||||
|
||||
logger.info("Connection URL generated successfully")
|
||||
logger.info(f"Account ID: {data['account_id']}")
|
||||
logger.info(f"Connection URL length: {len(connection_url)} characters")
|
||||
|
||||
def test_generate_connection_url_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test that unsupported cloud providers return an error."""
|
||||
# Get authentication token for admin user
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Try with GCP (unsupported)
|
||||
cloud_provider = "gcp"
|
||||
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts/generate-connection-url"
|
||||
|
||||
request_payload = {
|
||||
"account_config": {"regions": ["us-central1"]},
|
||||
"agent_config": {
|
||||
"region": "us-central1",
|
||||
"ingestion_url": "https://ingest.test.signoz.cloud",
|
||||
"ingestion_key": "test-ingestion-key-123456",
|
||||
"signoz_api_url": "https://test-deployment.test.signoz.cloud",
|
||||
"signoz_api_key": "test-api-key-789",
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=request_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
# Should return Bad Request for unsupported provider
|
||||
assert (
|
||||
response.status_code == HTTPStatus.BAD_REQUEST
|
||||
), f"Expected 400 for unsupported provider, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
assert "error" in response_data, "Response should contain 'error' field"
|
||||
assert (
|
||||
"unsupported cloud provider" in response_data["error"].lower()
|
||||
), "Error message should indicate unsupported provider"
|
||||
|
||||
logger.info("Unsupported provider correctly rejected with 400 Bad Request")
|
||||
590
tests/integration/src/cloudintegrations/03_accounts.py
Normal file
590
tests/integration/src/cloudintegrations/03_accounts.py
Normal file
@@ -0,0 +1,590 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import text
|
||||
from wiremock.client import (
|
||||
HttpMethods,
|
||||
Mapping,
|
||||
MappingRequest,
|
||||
MappingResponse,
|
||||
WireMockMatchers,
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def cleanup_cloud_accounts(postgres: types.TestContainerSQL) -> None:
|
||||
try:
|
||||
with postgres.conn.connect() as conn:
|
||||
# Try to delete all records instead of truncate in case table exists
|
||||
conn.execute(text("DELETE FROM cloud_integration"))
|
||||
conn.commit()
|
||||
logger.info("Cleaned up cloud_integration table")
|
||||
except Exception as e:
|
||||
# Table might not exist, which is fine
|
||||
logger.info(f"Cleanup skipped or partial: {str(e)[:100]}")
|
||||
|
||||
|
||||
def generate_unique_cloud_account_id() -> str:
|
||||
"""Generate a unique cloud account ID for testing."""
|
||||
# Use last 12 digits of UUID to simulate AWS account ID format
|
||||
return str(uuid.uuid4().int)[:12]
|
||||
|
||||
|
||||
def simulate_agent_checkin(
|
||||
signoz: types.SigNoz,
|
||||
admin_token: str,
|
||||
cloud_provider: str,
|
||||
account_id: str,
|
||||
cloud_account_id: str,
|
||||
) -> dict:
|
||||
"""Simulate an agent check-in to mark the account as connected.
|
||||
|
||||
Returns:
|
||||
dict with the response from check-in
|
||||
"""
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/agent-check-in"
|
||||
)
|
||||
|
||||
checkin_payload = {
|
||||
"account_id": account_id,
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"data": {},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=checkin_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
logger.error(f"Agent check-in failed: {response.status_code}, response: {response.text}")
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Agent check-in failed: {response.status_code}"
|
||||
|
||||
logger.info(f"Agent check-in completed for account: {account_id}")
|
||||
|
||||
response_data = response.json()
|
||||
return response_data.get("data", response_data)
|
||||
|
||||
|
||||
def create_test_account(
|
||||
signoz: types.SigNoz,
|
||||
admin_token: str,
|
||||
cloud_provider: str = "aws",
|
||||
) -> dict:
|
||||
"""Create a test account via generate-connection-url.
|
||||
|
||||
Returns the data as-is from the API response. Caller is responsible for
|
||||
doing agent check-in if needed to mark the account as connected.
|
||||
|
||||
Returns:
|
||||
dict with account_id and connection_url from the API
|
||||
"""
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/generate-connection-url"
|
||||
)
|
||||
|
||||
request_payload = {
|
||||
"account_config": {"regions": ["us-east-1"]},
|
||||
"agent_config": {
|
||||
"region": "us-east-1",
|
||||
"ingestion_url": "https://ingest.test.signoz.cloud",
|
||||
"ingestion_key": "test-ingestion-key-123456",
|
||||
"signoz_api_url": "https://test-deployment.test.signoz.cloud",
|
||||
"signoz_api_key": "test-api-key-789",
|
||||
"version": "v0.0.8",
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=request_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Failed to create test account: {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
# API returns data wrapped in {'status': 'success', 'data': {...}}
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def test_list_connected_accounts_empty(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing connected accounts when there are none."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
# API returns data wrapped in {'status': 'success', 'data': {...}}
|
||||
data = response_data.get("data", response_data)
|
||||
assert "accounts" in data, "Response should contain 'accounts' field"
|
||||
assert isinstance(data["accounts"], list), "Accounts should be a list"
|
||||
|
||||
# Note: If table doesn't exist yet, cleanup won't work and there might be leftover data
|
||||
# This is acceptable for integration tests with --reuse flag
|
||||
initial_count = len(data["accounts"])
|
||||
logger.info(f"Accounts list returned successfully with {initial_count} existing account(s)")
|
||||
|
||||
|
||||
def test_list_connected_accounts_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing connected accounts after creating one."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
# Simulate agent check-in to mark as connected
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(signoz, admin_token, cloud_provider, account_id, cloud_account_id)
|
||||
|
||||
# List accounts
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
assert "accounts" in data, "Response should contain 'accounts' field"
|
||||
assert isinstance(data["accounts"], list), "Accounts should be a list"
|
||||
|
||||
# Find our account in the list (there may be leftover accounts from previous test runs)
|
||||
account = next((a for a in data["accounts"] if a["id"] == account_id), None)
|
||||
assert account is not None, f"Account {account_id} should be found in list"
|
||||
assert account["id"] == account_id, "Account ID should match"
|
||||
assert "config" in account, "Account should have config field"
|
||||
assert "status" in account, "Account should have status field"
|
||||
|
||||
logger.info(f"Found account in list: {account_id}")
|
||||
|
||||
|
||||
def test_get_account_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test getting the status of a specific account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account (no check-in needed for status check)
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
# Get account status
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/{account_id}/status"
|
||||
)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
assert "id" in data, "Response should contain 'id' field"
|
||||
assert data["id"] == account_id, "Account ID should match"
|
||||
assert "status" in data, "Response should contain 'status' field"
|
||||
assert "integration" in data["status"], "Status should contain 'integration' field"
|
||||
|
||||
logger.info(f"Retrieved status for account: {account_id}")
|
||||
|
||||
|
||||
def test_get_account_status_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test getting status for a non-existent account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
fake_account_id = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/{fake_account_id}/status"
|
||||
)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.NOT_FOUND
|
||||
), f"Expected 404, got {response.status_code}"
|
||||
|
||||
logger.info("Non-existent account correctly returned 404")
|
||||
|
||||
|
||||
def test_update_account_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test updating account configuration."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
# Simulate agent check-in to mark as connected
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(signoz, admin_token, cloud_provider, account_id, cloud_account_id)
|
||||
|
||||
# Update account configuration
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/{account_id}/config"
|
||||
)
|
||||
|
||||
updated_config = {
|
||||
"config": {"regions": ["us-east-1", "us-west-2", "eu-west-1"]}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=updated_config,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
assert "id" in data, "Response should contain 'id' field"
|
||||
assert data["id"] == account_id, "Account ID should match"
|
||||
|
||||
# Verify the update by listing accounts
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
list_response_data = list_response.json()
|
||||
list_data = list_response_data.get("data", list_response_data)
|
||||
account = next((a for a in list_data["accounts"] if a["id"] == account_id), None)
|
||||
assert account is not None, "Account should be found in list"
|
||||
assert "config" in account, "Account should have config"
|
||||
assert "regions" in account["config"], "Config should have regions"
|
||||
assert len(account["config"]["regions"]) == 3, "Should have 3 regions"
|
||||
assert set(account["config"]["regions"]) == {
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1",
|
||||
}, "Regions should match updated config"
|
||||
|
||||
logger.info(f"Updated account configuration: {account_id}")
|
||||
|
||||
|
||||
def test_disconnect_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test disconnecting an account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
# Simulate agent check-in to mark as connected
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(signoz, admin_token, cloud_provider, account_id, cloud_account_id)
|
||||
|
||||
# Disconnect the account
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/{account_id}/disconnect"
|
||||
)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
# Verify account is no longer in the connected list
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_response_data = list_response.json()
|
||||
list_data = list_response_data.get("data", list_response_data)
|
||||
assert len(list_data["accounts"]) == 0, "Should have no connected accounts"
|
||||
|
||||
logger.info(f"Disconnected account: {account_id}")
|
||||
|
||||
|
||||
def test_disconnect_account_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test disconnecting a non-existent account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
fake_account_id = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/{fake_account_id}/disconnect"
|
||||
)
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.NOT_FOUND
|
||||
), f"Expected 404, got {response.status_code}"
|
||||
|
||||
logger.info("Disconnect non-existent account correctly returned 404")
|
||||
|
||||
|
||||
def test_list_accounts_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing accounts for an unsupported cloud provider."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "gcp" # Unsupported provider
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.BAD_REQUEST
|
||||
), f"Expected 400, got {response.status_code}"
|
||||
|
||||
logger.info("Unsupported provider correctly rejected with 400 Bad Request")
|
||||
787
tests/integration/src/cloudintegrations/04_services.py
Normal file
787
tests/integration/src/cloudintegrations/04_services.py
Normal file
@@ -0,0 +1,787 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from sqlalchemy import text
|
||||
from wiremock.client import (
|
||||
HttpMethods,
|
||||
Mapping,
|
||||
MappingRequest,
|
||||
MappingResponse,
|
||||
WireMockMatchers,
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def cleanup_cloud_accounts(postgres: types.TestContainerSQL) -> None:
|
||||
"""Clean up cloud_integration table to avoid corrupted data issues."""
|
||||
try:
|
||||
with postgres.conn.connect() as conn:
|
||||
# Try to delete all records instead of truncate in case table exists
|
||||
conn.execute(text("DELETE FROM cloud_integration"))
|
||||
conn.commit()
|
||||
logger.info("Cleaned up cloud_integration table")
|
||||
except Exception as e:
|
||||
# Table might not exist, which is fine
|
||||
logger.info(f"Cleanup skipped or partial: {str(e)[:100]}")
|
||||
|
||||
|
||||
def generate_unique_cloud_account_id() -> str:
|
||||
"""Generate a unique cloud account ID for testing."""
|
||||
# Use last 12 digits of UUID to simulate AWS account ID format
|
||||
return str(uuid.uuid4().int)[:12]
|
||||
|
||||
|
||||
def simulate_agent_checkin(
|
||||
signoz: types.SigNoz,
|
||||
admin_token: str,
|
||||
cloud_provider: str,
|
||||
account_id: str,
|
||||
cloud_account_id: str,
|
||||
) -> dict:
|
||||
"""Simulate an agent check-in to mark the account as connected.
|
||||
|
||||
Returns:
|
||||
dict with the response from check-in
|
||||
"""
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/agent-check-in"
|
||||
|
||||
checkin_payload = {
|
||||
"account_id": account_id,
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"data": {},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=checkin_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
logger.error(
|
||||
f"Agent check-in failed: {response.status_code}, response: {response.text}"
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Agent check-in failed: {response.status_code}"
|
||||
|
||||
logger.info(f"Agent check-in completed for account: {account_id}")
|
||||
|
||||
response_data = response.json()
|
||||
return response_data.get("data", response_data)
|
||||
|
||||
|
||||
def create_test_account(
|
||||
signoz: types.SigNoz,
|
||||
admin_token: str,
|
||||
cloud_provider: str = "aws",
|
||||
) -> dict:
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts/generate-connection-url"
|
||||
|
||||
request_payload = {
|
||||
"account_config": {"regions": ["us-east-1"]},
|
||||
"agent_config": {
|
||||
"region": "us-east-1",
|
||||
"ingestion_url": "https://ingest.test.signoz.cloud",
|
||||
"ingestion_key": "test-ingestion-key-123456",
|
||||
"signoz_api_url": "https://test-deployment.test.signoz.cloud",
|
||||
"signoz_api_key": "test-api-key-789",
|
||||
"version": "v0.0.8",
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=request_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Failed to create test account: {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
# API returns data wrapped in {'status': 'success', 'data': {...}}
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def test_list_services_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing available services without specifying an account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
assert "services" in data, "Response should contain 'services' field"
|
||||
assert isinstance(data["services"], list), "Services should be a list"
|
||||
assert len(data["services"]) > 0, "Should have at least one service available"
|
||||
|
||||
# Verify service structure
|
||||
service = data["services"][0]
|
||||
assert "id" in service, "Service should have 'id' field"
|
||||
assert "title" in service, "Service should have 'title' field"
|
||||
assert "icon" in service, "Service should have 'icon' field"
|
||||
|
||||
|
||||
def test_list_services_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing services for a specific connected account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(
|
||||
signoz, admin_token, cloud_provider, account_id, cloud_account_id
|
||||
)
|
||||
|
||||
# List services for the account
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services?cloud_account_id={cloud_account_id}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
assert "services" in data, "Response should contain 'services' field"
|
||||
assert isinstance(data["services"], list), "Services should be a list"
|
||||
assert len(data["services"]) > 0, "Should have at least one service available"
|
||||
|
||||
# Services should include config field (may be null if not configured)
|
||||
service = data["services"][0]
|
||||
assert "id" in service, "Service should have 'id' field"
|
||||
assert "title" in service, "Service should have 'title' field"
|
||||
assert "icon" in service, "Service should have 'icon' field"
|
||||
|
||||
logger.info(
|
||||
f"Listed {len(data['services'])} services for account {cloud_account_id}"
|
||||
)
|
||||
|
||||
|
||||
def test_get_service_details_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test getting service details without specifying an account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
# First get the list of services to get a valid service ID
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_data = list_response.json().get("data", list_response.json())
|
||||
assert len(list_data["services"]) > 0, "Should have at least one service"
|
||||
service_id = list_data["services"][0]["id"]
|
||||
|
||||
# Get service details
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{service_id}"
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
# Verify service details structure
|
||||
assert "id" in data, "Service details should have 'id' field"
|
||||
assert data["id"] == service_id, "Service ID should match requested ID"
|
||||
assert "title" in data, "Service details should have 'name' field"
|
||||
assert "overview" in data, "Service details should have 'overview' field"
|
||||
# assert assets to had list of dashboards
|
||||
assert "assets" in data, "Service details should have 'assets' field"
|
||||
assert isinstance(data["assets"], dict), "Assets should be a dictionary"
|
||||
|
||||
logger.info(f"Retrieved details for service: {service_id}")
|
||||
|
||||
|
||||
def test_get_service_details_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test getting service details for a specific connected account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(
|
||||
signoz, admin_token, cloud_provider, account_id, cloud_account_id
|
||||
)
|
||||
|
||||
# Get list of services first
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_data = list_response.json().get("data", list_response.json())
|
||||
assert len(list_data["services"]) > 0, "Should have at least one service"
|
||||
service_id = list_data["services"][0]["id"]
|
||||
|
||||
# Get service details with account
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{service_id}?cloud_account_id={cloud_account_id}"
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
# Verify service details structure
|
||||
assert "id" in data, "Service details should have 'id' field"
|
||||
assert data["id"] == service_id, "Service ID should match requested ID"
|
||||
assert "title" in data, "Service details should have 'title' field"
|
||||
assert "overview" in data, "Service details should have 'overview' field"
|
||||
assert "assets" in data, "Service details should have 'assets' field"
|
||||
assert "config" in data, "Service details should have 'config' field"
|
||||
assert "status" in data, "Config should have 'status' field"
|
||||
|
||||
logger.info(
|
||||
f"Retrieved details for service {service_id} with account {cloud_account_id}"
|
||||
)
|
||||
|
||||
|
||||
def test_get_service_details_invalid_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test getting details for a non-existent service."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
fake_service_id = "non-existent-service"
|
||||
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{fake_service_id}"
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.NOT_FOUND
|
||||
), f"Expected 404, got {response.status_code}"
|
||||
|
||||
logger.info("Non-existent service correctly returned 404")
|
||||
|
||||
|
||||
def test_list_services_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test listing services for an unsupported cloud provider."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "gcp" # Unsupported provider
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.BAD_REQUEST
|
||||
), f"Expected 400, got {response.status_code}"
|
||||
|
||||
logger.info(
|
||||
"Unsupported provider correctly rejected with 400 Bad Request for services"
|
||||
)
|
||||
|
||||
|
||||
def test_update_service_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test updating service configuration for a connected account."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(
|
||||
signoz, admin_token, cloud_provider, account_id, cloud_account_id
|
||||
)
|
||||
|
||||
# Get list of services to pick a valid service ID
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_data = list_response.json().get("data", list_response.json())
|
||||
assert len(list_data["services"]) > 0, "Should have at least one service"
|
||||
service_id = list_data["services"][0]["id"]
|
||||
|
||||
# Update service configuration
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{service_id}/config"
|
||||
|
||||
config_payload = {
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"config": {
|
||||
"metrics": {"enabled": True},
|
||||
"logs": {"enabled": True},
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=config_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {response.status_code}"
|
||||
|
||||
response_data = response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
# Verify response structure
|
||||
assert "id" in data, "Response should contain 'id' field"
|
||||
assert data["id"] == service_id, "Service ID should match"
|
||||
assert "config" in data, "Response should contain 'config' field"
|
||||
assert "metrics" in data["config"], "Config should contain 'metrics' field"
|
||||
assert "logs" in data["config"], "Config should contain 'logs' field"
|
||||
|
||||
logger.info(f"Updated service config for {service_id} on account {cloud_account_id}")
|
||||
|
||||
|
||||
def test_update_service_config_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test updating service config without a connected account should fail."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
cloud_provider = "aws"
|
||||
|
||||
# Get a valid service ID
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_data = list_response.json().get("data", list_response.json())
|
||||
service_id = list_data["services"][0]["id"]
|
||||
|
||||
# Try to update config with non-existent account
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{service_id}/config"
|
||||
|
||||
fake_cloud_account_id = generate_unique_cloud_account_id()
|
||||
config_payload = {
|
||||
"cloud_account_id": fake_cloud_account_id,
|
||||
"config": {
|
||||
"metrics": {"enabled": True},
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=config_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
# todo: improve the handler logic to return 500
|
||||
assert (
|
||||
response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
), f"Expected 500 for non-existent account, got {response.status_code}"
|
||||
|
||||
logger.info("Update service config correctly rejected for non-existent account")
|
||||
|
||||
|
||||
def test_update_service_config_invalid_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test updating config for a non-existent service should fail."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(
|
||||
signoz, admin_token, cloud_provider, account_id, cloud_account_id
|
||||
)
|
||||
|
||||
# Try to update config for invalid service
|
||||
fake_service_id = "non-existent-service"
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{fake_service_id}/config"
|
||||
|
||||
config_payload = {
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"config": {
|
||||
"metrics": {"enabled": True},
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=config_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status_code == HTTPStatus.NOT_FOUND
|
||||
), f"Expected 404 for invalid service, got {response.status_code}"
|
||||
|
||||
logger.info("Update service config correctly rejected for invalid service")
|
||||
|
||||
|
||||
def test_update_service_config_disable_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
postgres: types.TestContainerSQL,
|
||||
) -> None:
|
||||
"""Test disabling a service by updating config with enabled=false."""
|
||||
cleanup_cloud_accounts(postgres)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
# Mock the deployment info query
|
||||
make_http_mocks(
|
||||
signoz.zeus,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.GET,
|
||||
url="/v2/deployments/me",
|
||||
headers={
|
||||
"X-Signoz-Cloud-Api-Key": {
|
||||
WireMockMatchers.EQUAL_TO: "secret-key"
|
||||
}
|
||||
},
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={
|
||||
"status": "success",
|
||||
"data": {
|
||||
"name": "test-deployment",
|
||||
"cluster": {"region": {"dns": "test.signoz.cloud"}},
|
||||
},
|
||||
},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
account_data = create_test_account(signoz, admin_token, cloud_provider)
|
||||
account_id = account_data["account_id"]
|
||||
|
||||
cloud_account_id = generate_unique_cloud_account_id()
|
||||
simulate_agent_checkin(
|
||||
signoz, admin_token, cloud_provider, account_id, cloud_account_id
|
||||
)
|
||||
|
||||
# Get a valid service
|
||||
list_endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(list_endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
list_data = list_response.json().get("data", list_response.json())
|
||||
service_id = list_data["services"][0]["id"]
|
||||
|
||||
# First enable the service
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services/{service_id}/config"
|
||||
|
||||
enable_payload = {
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"config": {
|
||||
"metrics": {"enabled": True},
|
||||
},
|
||||
}
|
||||
|
||||
enable_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=enable_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert enable_response.status_code == HTTPStatus.OK, "Failed to enable service"
|
||||
|
||||
# Now disable the service
|
||||
disable_payload = {
|
||||
"cloud_account_id": cloud_account_id,
|
||||
"config": {
|
||||
"metrics": {"enabled": False},
|
||||
"logs": {"enabled": False},
|
||||
},
|
||||
}
|
||||
|
||||
disable_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(endpoint),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json=disable_payload,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert (
|
||||
disable_response.status_code == HTTPStatus.OK
|
||||
), f"Expected 200, got {disable_response.status_code}"
|
||||
|
||||
response_data = disable_response.json()
|
||||
data = response_data.get("data", response_data)
|
||||
|
||||
# Verify service is disabled
|
||||
assert data["config"]["metrics"]["enabled"] == False, "Metrics should be disabled"
|
||||
assert data["config"]["logs"]["enabled"] == False, "Logs should be disabled"
|
||||
|
||||
logger.info(f"Successfully disabled service {service_id}")
|
||||
|
||||
Reference in New Issue
Block a user