mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-12 20:42:07 +00:00
Compare commits
1 Commits
test/uplot
...
feat/gatew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842a53b6d8 |
@@ -1,17 +1,17 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
CreatedIngestionKey,
|
||||
CreateIngestionKeyProps,
|
||||
IngestionKeyProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
|
||||
const createIngestionKey = async (
|
||||
props: CreateIngestionKeyProps,
|
||||
): Promise<SuccessResponse<IngestionKeyProps> | ErrorResponse> => {
|
||||
): Promise<SuccessResponse<CreatedIngestionKey> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.post('/workspaces/me/keys', {
|
||||
const response = await ApiV2Instance.post('/gateway/ingestion_keys', {
|
||||
...props,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -8,9 +8,7 @@ const deleteIngestionKey = async (
|
||||
id: string,
|
||||
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.delete(
|
||||
`/workspaces/me/keys/${id}`,
|
||||
);
|
||||
const response = await ApiV2Instance.delete(`/gateway/ingestion_keys/${id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
AllIngestionKeyProps,
|
||||
@@ -11,11 +11,11 @@ export const getAllIngestionKeys = (
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { search, per_page, page } = props;
|
||||
|
||||
const BASE_URL = '/workspaces/me/keys';
|
||||
const BASE_URL = '/gateway/ingestion_keys';
|
||||
const URL_QUERY_PARAMS =
|
||||
search && search.length > 0
|
||||
? `/search?name=${search}&page=1&per_page=100`
|
||||
: `?page=${page}&per_page=${per_page}`;
|
||||
|
||||
return GatewayApiV1Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`);
|
||||
return ApiV2Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-throw-literal */
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
AddLimitProps,
|
||||
@@ -24,8 +24,8 @@ const createLimitForIngestionKey = async (
|
||||
props: AddLimitProps,
|
||||
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.post(
|
||||
`/workspaces/me/keys/${props.keyID}/limits`,
|
||||
const response = await ApiV2Instance.post(
|
||||
`/gateway/ingestion_keys/${props.keyID}/limits`,
|
||||
{
|
||||
...props,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -8,8 +8,8 @@ const deleteLimitsForIngestionKey = async (
|
||||
id: string,
|
||||
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.delete(
|
||||
`/workspaces/me/limits/${id}`,
|
||||
const response = await ApiV2Instance.delete(
|
||||
`/gateway/ingestion_keys/limits/${id}`,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-throw-literal */
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
LimitSuccessProps,
|
||||
@@ -24,8 +24,8 @@ const updateLimitForIngestionKey = async (
|
||||
props: UpdateLimitProps,
|
||||
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.patch(
|
||||
`/workspaces/me/limits/${props.limitID}`,
|
||||
const response = await ApiV2Instance.patch(
|
||||
`/gateway/ingestion_keys/limits/${props.limitID}`,
|
||||
{
|
||||
config: props.config,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GatewayApiV1Instance } from 'api';
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -11,8 +11,8 @@ const updateIngestionKey = async (
|
||||
props: UpdateIngestionKeyProps,
|
||||
): Promise<SuccessResponse<IngestionKeysPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV1Instance.patch(
|
||||
`/workspaces/me/keys/${props.id}`,
|
||||
const response = await ApiV2Instance.patch(
|
||||
`/gateway/ingestion_keys/${props.id}`,
|
||||
{
|
||||
...props.data,
|
||||
},
|
||||
|
||||
@@ -2,19 +2,27 @@ import './IngestionSettings.styles.scss';
|
||||
|
||||
import { Skeleton, Table, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import { IngestionDataType } from 'types/api/settings/ingestion';
|
||||
|
||||
export default function IngestionSettings(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const {
|
||||
data: globalConfig,
|
||||
isFetching: isFetchingGlobalConfig,
|
||||
} = useGetGlobalConfig();
|
||||
|
||||
const { data: ingestionData, isFetching } = useQuery({
|
||||
queryFn: getIngestionData,
|
||||
queryKey: ['getIngestionData', user?.id],
|
||||
const {
|
||||
data: ingestionKeys,
|
||||
isFetching: isFetchingIngestionKeys,
|
||||
} = useGetAllIngestionsKeys({
|
||||
search: '',
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const isFetching = isFetchingGlobalConfig || isFetchingIngestionKeys;
|
||||
|
||||
const columns: ColumnsType<IngestionDataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
@@ -40,27 +48,19 @@ export default function IngestionSettings(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const injectionDataPayload =
|
||||
ingestionData &&
|
||||
ingestionData.payload &&
|
||||
Array.isArray(ingestionData.payload) &&
|
||||
ingestionData?.payload[0];
|
||||
const ingestionKey = ingestionKeys?.data?.data?.keys?.[0]?.value || '';
|
||||
const ingestionURL = globalConfig?.data?.ingestion_url || '';
|
||||
|
||||
const data: IngestionDataType[] = [
|
||||
{
|
||||
key: '1',
|
||||
name: 'Ingestion URL',
|
||||
value: injectionDataPayload?.ingestionURL,
|
||||
value: ingestionURL,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: 'Ingestion Key',
|
||||
value: injectionDataPayload?.ingestionKey,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: 'Ingestion Region',
|
||||
value: injectionDataPayload?.dataRegion,
|
||||
value: ingestionKey,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -114,6 +114,13 @@ export const showErrorNotification = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getErrorMessage = (err: any): string => {
|
||||
if (err?.error?.message) return err.error.message;
|
||||
if (typeof err?.error === 'string') return err.error;
|
||||
if (err?.message) return err.message;
|
||||
return 'Something went wrong';
|
||||
};
|
||||
|
||||
type ExpiryOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
@@ -271,14 +278,14 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setActiveAPIKey(IngestionKeys?.data.data[0]);
|
||||
setActiveAPIKey(IngestionKeys?.data.data.keys[0]);
|
||||
}, [IngestionKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(IngestionKeys?.data.data || []);
|
||||
setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0);
|
||||
setDataSource(IngestionKeys?.data.data.keys || []);
|
||||
setTotalIngestionKeys(IngestionKeys?.data?.data._pagination?.total || 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [IngestionKeys?.data?.data]);
|
||||
}, [IngestionKeys?.data?.data?.keys]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
@@ -311,7 +318,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
isLoading: isLoadingCreateAPIKey,
|
||||
} = useMutation(createIngestionKeyApi, {
|
||||
onSuccess: (data) => {
|
||||
setActiveAPIKey(data.payload);
|
||||
// setActiveAPIKey(data.payload); // New API returns partial data (created key id/value), and we close modal anyway.
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
@@ -1007,6 +1014,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
{activeSignal?.config?.day?.enabled ? (
|
||||
<Form.Item name="dailyLimit" key="dailyLimit">
|
||||
<InputNumber
|
||||
min={0}
|
||||
disabled={!activeSignal?.config?.day?.enabled}
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
@@ -1030,6 +1038,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
{activeSignal?.config?.day?.enabled ? (
|
||||
<Form.Item name="dailyCount" key="dailyCount">
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="Enter max # of samples/day"
|
||||
addonAfter={
|
||||
<Form.Item
|
||||
@@ -1097,6 +1106,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
{activeSignal?.config?.second?.enabled ? (
|
||||
<Form.Item name="secondsLimit" key="secondsLimit">
|
||||
<InputNumber
|
||||
min={0}
|
||||
disabled={!activeSignal?.config?.second?.enabled}
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
@@ -1120,6 +1130,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
{activeSignal?.config?.second?.enabled ? (
|
||||
<Form.Item name="secondsCount" key="secondsCount">
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="Enter max # of samples/s"
|
||||
addonAfter={
|
||||
<Form.Item
|
||||
@@ -1153,20 +1164,18 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasCreateLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError?.error && (
|
||||
hasCreateLimitForIngestionKeyError && (
|
||||
<div className="error">
|
||||
{createLimitForIngestionKeyError?.error}
|
||||
{getErrorMessage(createLimitForIngestionKeyError)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasUpdateLimitForIngestionKeyError &&
|
||||
updateLimitForIngestionKeyError?.error && (
|
||||
hasUpdateLimitForIngestionKeyError && (
|
||||
<div className="error">
|
||||
{updateLimitForIngestionKeyError?.error}
|
||||
{getErrorMessage(updateLimitForIngestionKeyError)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LimitProps } from 'types/api/ingestionKeys/limits/types';
|
||||
import {
|
||||
AllIngestionKeyProps,
|
||||
IngestionKeyProps,
|
||||
PaginationProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
|
||||
import MultiIngestionSettings from '../MultiIngestionSettings';
|
||||
@@ -15,7 +16,10 @@ interface TestIngestionKeyProps extends Omit<IngestionKeyProps, 'limits'> {
|
||||
}
|
||||
|
||||
interface TestAllIngestionKeyProps extends Omit<AllIngestionKeyProps, 'data'> {
|
||||
data: TestIngestionKeyProps[];
|
||||
data: {
|
||||
keys: TestIngestionKeyProps[];
|
||||
_pagination: PaginationProps;
|
||||
};
|
||||
}
|
||||
|
||||
// Mock useHistory.push to capture navigation URL used by MultiIngestionSettings
|
||||
@@ -88,26 +92,28 @@ describe('MultiIngestionSettings Page', () => {
|
||||
// Arrange API response with a metrics daily count limit so the alert button is visible
|
||||
const response: TestAllIngestionKeyProps = {
|
||||
status: 'success',
|
||||
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 },
|
||||
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 },
|
||||
},
|
||||
};
|
||||
|
||||
server.use(
|
||||
@@ -177,26 +183,28 @@ describe('MultiIngestionSettings Page', () => {
|
||||
// Arrange API response with a logs daily size limit so the alert button is visible
|
||||
const response: TestAllIngestionKeyProps = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
name: 'Key Two',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k2',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
id: 'l2',
|
||||
signal: 'logs',
|
||||
config: { day: { size: 2048 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
data: {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Two',
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k2',
|
||||
created_at: TEST_CREATED_UPDATED,
|
||||
updated_at: TEST_CREATED_UPDATED,
|
||||
tags: [],
|
||||
limits: [
|
||||
{
|
||||
id: 'l2',
|
||||
signal: 'logs',
|
||||
config: { day: { size: 2048 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
server.use(
|
||||
|
||||
@@ -5,13 +5,14 @@ import './Onboarding.styles.scss';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Form, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import cx from 'classnames';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -128,29 +129,41 @@ export default function Onboarding(): JSX.Element {
|
||||
logEvent('Onboarding V2 Started', {});
|
||||
});
|
||||
|
||||
const { status, data: ingestionData } = useQuery({
|
||||
queryFn: () => getIngestionData(),
|
||||
const {
|
||||
status: globalConfigStatus,
|
||||
data: globalConfig,
|
||||
} = useGetGlobalConfig();
|
||||
const {
|
||||
status: ingestionKeysStatus,
|
||||
data: ingestionKeys,
|
||||
} = useGetAllIngestionsKeys({
|
||||
search: '',
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
status === 'success' &&
|
||||
ingestionData &&
|
||||
ingestionData &&
|
||||
Array.isArray(ingestionData.payload)
|
||||
globalConfigStatus === 'success' &&
|
||||
ingestionKeysStatus === 'success' &&
|
||||
ingestionKeys?.data.data.keys
|
||||
) {
|
||||
const payload = ingestionData.payload[0] || {
|
||||
ingestionKey: '',
|
||||
dataRegion: '',
|
||||
};
|
||||
const ingestionKey = ingestionKeys.data.data.keys[0]?.value || '';
|
||||
const ingestionURL = globalConfig?.data?.ingestion_url || '';
|
||||
const region = ''; // Region not available in GlobalConfig
|
||||
|
||||
updateIngestionData({
|
||||
SIGNOZ_INGESTION_KEY: payload?.ingestionKey,
|
||||
REGION: payload?.dataRegion,
|
||||
SIGNOZ_INGESTION_KEY: ingestionKey,
|
||||
REGION: region,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status, ingestionData?.payload]);
|
||||
}, [
|
||||
globalConfigStatus,
|
||||
ingestionKeysStatus,
|
||||
ingestionKeys?.data.data.keys,
|
||||
globalConfig?.data,
|
||||
]);
|
||||
|
||||
const setModuleStepsBasedOnSelectedDataSource = (
|
||||
selectedDataSource: DataSourceType | null,
|
||||
|
||||
@@ -69,8 +69,11 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (ingestionKeys?.data.data && ingestionKeys?.data.data.length > 0) {
|
||||
setFirstIngestionKey(ingestionKeys?.data.data[0]);
|
||||
if (
|
||||
ingestionKeys?.data.data.keys &&
|
||||
ingestionKeys?.data.data.keys.length > 0
|
||||
) {
|
||||
setFirstIngestionKey(ingestionKeys?.data.data.keys[0]);
|
||||
}
|
||||
}, [ingestionKeys]);
|
||||
|
||||
|
||||
@@ -54,8 +54,10 @@ export interface PaginationProps {
|
||||
|
||||
export interface AllIngestionKeyProps {
|
||||
status: string;
|
||||
data: IngestionKeyProps[];
|
||||
_pagination: PaginationProps;
|
||||
data: {
|
||||
keys: IngestionKeyProps[];
|
||||
_pagination: PaginationProps;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateIngestionKeyProp {
|
||||
@@ -83,3 +85,8 @@ export type IngestionKeysPayloadProps = {
|
||||
export type GetIngestionKeyPayloadProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface CreatedIngestionKey {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ type LimitConfig struct {
|
||||
}
|
||||
|
||||
type LimitValue struct {
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Count int64 `json:"count,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type LimitMetric struct {
|
||||
|
||||
Reference in New Issue
Block a user