mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-12 04:22:06 +00:00
Compare commits
99 Commits
sso-mappin
...
test/e2e/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fda1f5c5a | ||
|
|
53b9e9b202 | ||
|
|
23c27e47bd | ||
|
|
5917231a89 | ||
|
|
0e45da610e | ||
|
|
cb43371299 | ||
|
|
03f267b63e | ||
|
|
91a9330d6d | ||
|
|
43239df4e1 | ||
|
|
a7b6b59c0d | ||
|
|
66b4153a05 | ||
|
|
91611fef04 | ||
|
|
a90005c0a7 | ||
|
|
7fbf1ba77f | ||
|
|
455890f3a5 | ||
|
|
c3b00135d6 | ||
|
|
abe8e7b13d | ||
|
|
f799fe0069 | ||
|
|
42071bdcce | ||
|
|
808780af7e | ||
|
|
2657051bea | ||
|
|
433cc440b7 | ||
|
|
83b4e2afb6 | ||
|
|
c8e4029068 | ||
|
|
dc0fe05633 | ||
|
|
45644536a3 | ||
|
|
31d0f25458 | ||
|
|
e670e60a2d | ||
|
|
8e98eb8b5b | ||
|
|
a526714658 | ||
|
|
340c4ce034 | ||
|
|
5bc65a2c65 | ||
|
|
ee13ade1c6 | ||
|
|
9972298cd7 | ||
|
|
cafec691c5 | ||
|
|
3c747de5dd | ||
|
|
8489900262 | ||
|
|
93cafac1fb | ||
|
|
e6d5f2a600 | ||
|
|
9d15927a1c | ||
|
|
16736c2fdf | ||
|
|
13112ff701 | ||
|
|
91ebda6a45 | ||
|
|
692d0bd30b | ||
|
|
394afb5ca7 | ||
|
|
91ab056c2f | ||
|
|
c31bd6f011 | ||
|
|
6741d5d21f | ||
|
|
1bb8f0fd3f | ||
|
|
6ce7e1dab8 | ||
|
|
caa247cc5f | ||
|
|
6a349f7fce | ||
|
|
4d6ab70634 | ||
|
|
913558347c | ||
|
|
2418f78bbe | ||
|
|
2b1e35ce4e | ||
|
|
3f9177a12c | ||
|
|
5b4fb5d4c8 | ||
|
|
f452eb376c | ||
|
|
6b3f2675e9 | ||
|
|
a9588f0b2b | ||
|
|
c13f5381bd | ||
|
|
bfd992674f | ||
|
|
f17a4a5ff2 | ||
|
|
8d5477e919 | ||
|
|
7facf01374 | ||
|
|
6371f77040 | ||
|
|
02ea5eef77 | ||
|
|
c75720ccf2 | ||
|
|
268bce78ae | ||
|
|
484f1a7af9 | ||
|
|
701a7350e4 | ||
|
|
271f992e42 | ||
|
|
e7013434cb | ||
|
|
68b18ec0b9 | ||
|
|
5fefc9b00d | ||
|
|
391f13545b | ||
|
|
ea393d46b6 | ||
|
|
07b7f5d240 | ||
|
|
1b52f66c5c | ||
|
|
2627285a84 | ||
|
|
9de97c9ab4 | ||
|
|
9281dbfd07 | ||
|
|
4b90eda3c8 | ||
|
|
8e67d261fa | ||
|
|
d668a92465 | ||
|
|
2052f5606a | ||
|
|
4d817c67c4 | ||
|
|
e603f968bd | ||
|
|
92f50bdd18 | ||
|
|
5ae2edaff7 | ||
|
|
2bb40d79db | ||
|
|
f55e97f270 | ||
|
|
160436ae35 | ||
|
|
8049f794dd | ||
|
|
5a80411c6f | ||
|
|
a7b054db75 | ||
|
|
98887ecc92 | ||
|
|
0de2dbf674 |
@@ -4,6 +4,7 @@ services:
|
||||
container_name: signoz-otel-collector-dev
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
environment:
|
||||
|
||||
@@ -214,6 +214,7 @@ services:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
|
||||
@@ -155,6 +155,7 @@ services:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
configs:
|
||||
- source: otel-collector-config
|
||||
target: /etc/otel-collector-config.yaml
|
||||
|
||||
@@ -219,6 +219,7 @@ services:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
|
||||
@@ -150,6 +150,7 @@ services:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
|
||||
@@ -17,8 +17,6 @@ const config: Config.InitialOptions = {
|
||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^@signozhq/icons$':
|
||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||
},
|
||||
globals: {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"FORGOT_PASSWORD": "SigNoz | Forgot Password",
|
||||
"HOME": "SigNoz | Home",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
|
||||
@@ -194,10 +194,6 @@ export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
||||
export const ForgotPassword = Loadable(
|
||||
() => import(/* webpackChunkName: "ForgotPassword" */ 'pages/ForgotPassword'),
|
||||
);
|
||||
|
||||
export const UnAuthorized = Loadable(
|
||||
() => import(/* webpackChunkName: "UnAuthorized" */ 'pages/UnAuthorized'),
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
InstalledIntegrations,
|
||||
@@ -340,13 +339,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'LOGIN',
|
||||
},
|
||||
{
|
||||
path: ROUTES.FORGOT_PASSWORD,
|
||||
exact: true,
|
||||
component: ForgotPassword,
|
||||
isPrivate: false,
|
||||
key: 'FORGOT_PASSWORD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.UN_AUTHORIZED,
|
||||
exact: true,
|
||||
|
||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -18,7 +18,6 @@ import '@signozhq/checkbox';
|
||||
import '@signozhq/combobox';
|
||||
import '@signozhq/command';
|
||||
import '@signozhq/design-tokens';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
import '@signozhq/resizable';
|
||||
|
||||
@@ -648,13 +648,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
) : (
|
||||
<Typography.Text
|
||||
className="value-string"
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
placement: 'top',
|
||||
mouseEnterDelay: 0.2,
|
||||
mouseLeaveDelay: 0,
|
||||
},
|
||||
}}
|
||||
ellipsis={{ tooltip: { placement: 'top' } }}
|
||||
>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const ROUTES = {
|
||||
SIGN_UP: '/signup',
|
||||
LOGIN: '/login',
|
||||
FORGOT_PASSWORD: '/forgot-password',
|
||||
HOME: '/home',
|
||||
SERVICE_METRICS: '/services/:servicename',
|
||||
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
.forgot-password-title {
|
||||
font-family: var(--label-large-600-font-family);
|
||||
font-size: var(--label-large-600-font-size);
|
||||
font-weight: var(--label-large-600-font-weight);
|
||||
letter-spacing: var(--label-large-600-letter-spacing);
|
||||
line-height: 1.45;
|
||||
color: var(--l1-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.forgot-password-description {
|
||||
font-family: var(--paragraph-base-400-font-family);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l2-foreground);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
max-width: 317px;
|
||||
}
|
||||
|
||||
.forgot-password-form {
|
||||
width: 100%;
|
||||
|
||||
// Label styling
|
||||
.forgot-password-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l1-foreground);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
// Parent container for fields
|
||||
.forgot-password-field {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.ant-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
> .forgot-password-back-button,
|
||||
> .login-submit-btn {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password-back-button {
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--l3-border);
|
||||
border-color: var(--l3-border);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { ArrowLeft, Mail } from '@signozhq/icons';
|
||||
|
||||
interface SuccessScreenProps {
|
||||
onBackToLogin: () => void;
|
||||
}
|
||||
|
||||
function SuccessScreen({ onBackToLogin }: SuccessScreenProps): JSX.Element {
|
||||
return (
|
||||
<div className="login-form-container">
|
||||
<div className="forgot-password-form">
|
||||
<div className="login-form-header">
|
||||
<div className="login-form-emoji">
|
||||
<Mail size={32} />
|
||||
</div>
|
||||
<h4 className="forgot-password-title">Check your email</h4>
|
||||
<p className="forgot-password-description">
|
||||
We've sent a password reset link to your email. Please check your
|
||||
inbox and follow the instructions to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-actions forgot-password-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="button"
|
||||
data-testid="back-to-login"
|
||||
className="login-submit-btn"
|
||||
onClick={onBackToLogin}
|
||||
prefixIcon={<ArrowLeft size={12} />}
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SuccessScreen;
|
||||
@@ -1,402 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleInternalServerError,
|
||||
rest,
|
||||
server,
|
||||
} from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
|
||||
|
||||
import ForgotPassword, { ForgotPasswordRouteState } from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
>;
|
||||
|
||||
const FORGOT_PASSWORD_ENDPOINT = '*/api/v2/factor_password/forgot';
|
||||
|
||||
// Mock data
|
||||
const mockSingleOrg: OrgSessionContext[] = [
|
||||
{
|
||||
id: 'org-1',
|
||||
name: 'Test Organization',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockMultipleOrgs: OrgSessionContext[] = [
|
||||
{
|
||||
id: 'org-1',
|
||||
name: 'Organization One',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'org-2',
|
||||
name: 'Organization Two',
|
||||
authNSupport: {
|
||||
password: [{ provider: 'email_password' }],
|
||||
callback: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const TEST_EMAIL = 'jest.test@signoz.io';
|
||||
|
||||
const defaultProps: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
const multiOrgProps: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
describe('ForgotPassword Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Initial Render', () => {
|
||||
it('renders forgot password form with all required elements', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/forgot your password\?/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/send a reset link to your inbox/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('email')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('forgot-password-submit')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('forgot-password-back')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills email from props', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
expect(emailInput).toHaveValue(TEST_EMAIL);
|
||||
});
|
||||
|
||||
it('disables email input field', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
expect(emailInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not show organization dropdown for single org', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId('orgId')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Organization Name')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('enables submit button when email is provided with single org', () => {
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Organizations', () => {
|
||||
it('shows organization dropdown when multiple orgs exist', () => {
|
||||
render(<ForgotPassword {...multiOrgProps} />);
|
||||
|
||||
expect(screen.getByTestId('orgId')).toBeInTheDocument();
|
||||
expect(screen.getByText('Organization Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables submit button when org is not selected', () => {
|
||||
const propsWithoutOrgId: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithoutOrgId} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables submit button after selecting an organization', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ForgotPassword {...multiOrgProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Click on the dropdown to reveal the options
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
await user.click(screen.getByText('Organization One'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('pre-selects organization when orgId is provided', () => {
|
||||
const propsWithOrgId: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgId: 'org-1',
|
||||
orgs: mockMultipleOrgs,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithOrgId} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission - Success', () => {
|
||||
it('successfully submits forgot password request and shows success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/check your email/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/we've sent a password reset link/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows back to login button on success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByTestId('back-to-login')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to login when clicking back to login on success screen', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByTestId('back-to-login')).toBeInTheDocument();
|
||||
|
||||
const backToLoginButton = screen.getByTestId('back-to-login');
|
||||
await user.click(backToLoginButton);
|
||||
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission - Error Handling', () => {
|
||||
it('displays error message when forgot password API fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(
|
||||
FORGOT_PASSWORD_ENDPOINT,
|
||||
createErrorResponse(400, 'USER_NOT_FOUND', 'User not found'),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/user not found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error message when API returns server error', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(rest.post(FORGOT_PASSWORD_ENDPOINT, handleInternalServerError));
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/internal server error occurred/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears error message on new submission attempt', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let requestCount = 0;
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1) {
|
||||
return res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(await screen.findByText(/user not found/i)).toBeInTheDocument();
|
||||
|
||||
// Click submit again
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/user not found/i)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(await screen.findByText(/check your email/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('redirects to login when clicking back button on form', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const backButton = screen.getByTestId('forgot-password-back');
|
||||
await user.click(backButton);
|
||||
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
it('shows loading state during API call', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.delay(100), ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
// Button should show loading state
|
||||
expect(await screen.findByText(/sending\.\.\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables submit button during loading', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(FORGOT_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.delay(100), ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ForgotPassword {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty email gracefully', () => {
|
||||
const propsWithEmptyEmail: ForgotPasswordRouteState = {
|
||||
email: '',
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithEmptyEmail} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('handles whitespace-only email', () => {
|
||||
const propsWithWhitespaceEmail: ForgotPasswordRouteState = {
|
||||
email: ' ',
|
||||
orgs: mockSingleOrg,
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithWhitespaceEmail} />);
|
||||
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('handles empty orgs array by disabling submission', () => {
|
||||
const propsWithNoOrgs: ForgotPasswordRouteState = {
|
||||
email: TEST_EMAIL,
|
||||
orgs: [],
|
||||
};
|
||||
|
||||
render(<ForgotPassword {...propsWithNoOrgs} />);
|
||||
|
||||
// Should not show org dropdown
|
||||
expect(screen.queryByTestId('orgId')).not.toBeInTheDocument();
|
||||
// Submit should be disabled because no orgId can be determined
|
||||
const submitButton = screen.getByTestId('forgot-password-submit');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { ArrowLeft, ArrowRight } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Select } from 'antd';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { useForgotPassword } from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
|
||||
|
||||
import SuccessScreen from './SuccessScreen';
|
||||
|
||||
import './ForgotPassword.styles.scss';
|
||||
import 'container/Login/Login.styles.scss';
|
||||
|
||||
type FormValues = {
|
||||
email: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ForgotPasswordRouteState = {
|
||||
email: string;
|
||||
orgId?: string;
|
||||
orgs: OrgSessionContext[];
|
||||
};
|
||||
|
||||
function ForgotPassword({
|
||||
email,
|
||||
orgId,
|
||||
orgs,
|
||||
}: ForgotPasswordRouteState): JSX.Element {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const {
|
||||
mutate: forgotPasswordMutate,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
error: mutationError,
|
||||
} = useForgotPassword();
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (!mutationError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
ErrorResponseHandlerV2(mutationError as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
return apiError as APIError;
|
||||
}
|
||||
}, [mutationError]);
|
||||
|
||||
const initialOrgId = useMemo((): string | undefined => {
|
||||
if (orgId) {
|
||||
return orgId;
|
||||
}
|
||||
|
||||
if (orgs.length === 1) {
|
||||
return orgs[0]?.id;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [orgId, orgs]);
|
||||
|
||||
const watchedEmail = Form.useWatch('email', form);
|
||||
const selectedOrgId = Form.useWatch('orgId', form);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
email,
|
||||
orgId: initialOrgId,
|
||||
});
|
||||
}, [email, form, initialOrgId]);
|
||||
|
||||
const hasMultipleOrgs = orgs.length > 1;
|
||||
|
||||
const isSubmitEnabled = useMemo((): boolean => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!watchedEmail?.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have an orgId (either selected from dropdown or the initial one)
|
||||
const currentOrgId = hasMultipleOrgs ? selectedOrgId : initialOrgId;
|
||||
return Boolean(currentOrgId);
|
||||
}, [watchedEmail, selectedOrgId, isLoading, initialOrgId, hasMultipleOrgs]);
|
||||
|
||||
const handleSubmit = useCallback((): void => {
|
||||
const values = form.getFieldsValue();
|
||||
const currentOrgId = hasMultipleOrgs ? values.orgId : initialOrgId;
|
||||
|
||||
if (!currentOrgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the forgot password API
|
||||
forgotPasswordMutate({
|
||||
data: {
|
||||
email: values.email,
|
||||
orgId: currentOrgId,
|
||||
frontendBaseURL: window.location.origin,
|
||||
},
|
||||
});
|
||||
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);
|
||||
|
||||
const handleBackToLogin = useCallback((): void => {
|
||||
history.push(ROUTES.LOGIN);
|
||||
}, []);
|
||||
|
||||
// Success screen
|
||||
if (isSuccess) {
|
||||
return <SuccessScreen onBackToLogin={handleBackToLogin} />;
|
||||
}
|
||||
|
||||
// Form screen
|
||||
return (
|
||||
<div className="login-form-container">
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
className="forgot-password-form"
|
||||
initialValues={{
|
||||
email,
|
||||
orgId: initialOrgId,
|
||||
}}
|
||||
>
|
||||
<div className="login-form-header">
|
||||
<div className="login-form-emoji">
|
||||
<img src="/svgs/tv.svg" alt="TV" width="32" height="32" />
|
||||
</div>
|
||||
<h4 className="forgot-password-title">Forgot your password?</h4>
|
||||
<p className="forgot-password-description">
|
||||
Send a reset link to your inbox and get back to monitoring.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-card">
|
||||
<div className="forgot-password-field">
|
||||
<label className="forgot-password-label" htmlFor="forgotPasswordEmail">
|
||||
Email address
|
||||
</label>
|
||||
<Form.Item name="email">
|
||||
<Input
|
||||
type="email"
|
||||
id="forgotPasswordEmail"
|
||||
data-testid="email"
|
||||
required
|
||||
disabled
|
||||
className="login-form-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{hasMultipleOrgs && (
|
||||
<div className="forgot-password-field">
|
||||
<label className="forgot-password-label" htmlFor="orgId">
|
||||
Organization Name
|
||||
</label>
|
||||
<Form.Item
|
||||
name="orgId"
|
||||
rules={[{ required: true, message: 'Please select your organization' }]}
|
||||
>
|
||||
<Select
|
||||
id="orgId"
|
||||
data-testid="orgId"
|
||||
className="login-form-input login-form-select-no-border"
|
||||
placeholder="Select your organization"
|
||||
options={orgs.map((org) => ({
|
||||
value: org.id,
|
||||
label: org.name || 'default',
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errorMessage && <AuthError error={errorMessage} />}
|
||||
|
||||
<div className="login-form-actions forgot-password-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
type="button"
|
||||
data-testid="forgot-password-back"
|
||||
className="forgot-password-back-button"
|
||||
onClick={handleBackToLogin}
|
||||
prefixIcon={<ArrowLeft size={12} />}
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!isSubmitEnabled}
|
||||
loading={isLoading}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="forgot-password-submit"
|
||||
className="login-submit-btn"
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Send reset link'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -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';
|
||||
@@ -88,10 +86,6 @@ const { Option } = Select;
|
||||
|
||||
const BYTES = 1073741824;
|
||||
|
||||
const INITIAL_PAGE_SIZE = 10;
|
||||
const SEARCH_PAGE_SIZE = 100;
|
||||
const FIRST_PAGE = 1;
|
||||
|
||||
const COUNT_MULTIPLIER = {
|
||||
thousand: 1000,
|
||||
million: 1000000,
|
||||
@@ -117,8 +111,6 @@ export const showErrorNotification = (
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: err.message || SOMETHING_WENT_WRONG,
|
||||
description: (err as AxiosError<RenderErrorResponseDTO>).response?.data?.error
|
||||
?.message,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -171,20 +163,15 @@ 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>(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: FIRST_PAGE,
|
||||
per_page: INITIAL_PAGE_SIZE,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
});
|
||||
|
||||
const [totalIngestionKeys, setTotalIngestionKeys] = useState(0);
|
||||
@@ -199,7 +186,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const [
|
||||
createLimitForIngestionKeyError,
|
||||
setCreateLimitForIngestionKeyError,
|
||||
] = useState<string | null>(null);
|
||||
] = useState<ErrorResponse | null>(null);
|
||||
|
||||
const [
|
||||
hasUpdateLimitForIngestionKeyError,
|
||||
@@ -209,7 +196,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const [
|
||||
updateLimitForIngestionKeyError,
|
||||
setUpdateLimitForIngestionKeyError,
|
||||
] = useState<string | null>(null);
|
||||
] = useState<ErrorResponse | null>(null);
|
||||
|
||||
const { t } = useTranslation(['ingestionKeys']);
|
||||
|
||||
@@ -229,11 +216,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
handleFormReset();
|
||||
};
|
||||
|
||||
const showDeleteModal = (apiKey: GatewaytypesIngestionKeyDTO): void => {
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
setCreateLimitForIngestionKeyError(null);
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
setUpdateLimitForIngestionKeyError(null);
|
||||
const showDeleteModal = (apiKey: IngestionKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
@@ -250,11 +233,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
setIsAddModalOpen(false);
|
||||
};
|
||||
|
||||
const showEditModal = (apiKey: GatewaytypesIngestionKeyDTO): void => {
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
setCreateLimitForIngestionKeyError(null);
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
setUpdateLimitForIngestionKeyError(null);
|
||||
const showEditModal = (apiKey: IngestionKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
handleFormReset();
|
||||
setUpdatedTags(apiKey.tags || []);
|
||||
@@ -269,10 +248,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
};
|
||||
|
||||
const showAddModal = (): void => {
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
setCreateLimitForIngestionKeyError(null);
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
setUpdateLimitForIngestionKeyError(null);
|
||||
setUpdatedTags([]);
|
||||
setActiveAPIKey(null);
|
||||
setIsAddModalOpen(true);
|
||||
@@ -283,62 +258,27 @@ 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: ingestionKeysData,
|
||||
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: FIRST_PAGE,
|
||||
per_page: SEARCH_PAGE_SIZE,
|
||||
name: searchText,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: isSearching,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Use the appropriate data based on which API is active
|
||||
const ingestionKeys = isSearching
|
||||
? searchIngestionKeysData
|
||||
: ingestionKeysData;
|
||||
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(() => {
|
||||
setDataSource(ingestionKeys?.data.data?.keys || []);
|
||||
setTotalIngestionKeys(ingestionKeys?.data?.data?._pagination?.total || 0);
|
||||
setActiveAPIKey(IngestionKeys?.data.data[0]);
|
||||
}, [IngestionKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(IngestionKeys?.data.data || []);
|
||||
setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ingestionKeys?.data?.data]);
|
||||
}, [IngestionKeys?.data?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
@@ -357,7 +297,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const clearSearch = (): void => {
|
||||
setSearchValue('');
|
||||
setSearchText('');
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -370,54 +309,101 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const {
|
||||
mutate: createIngestionKey,
|
||||
isLoading: isLoadingCreateAPIKey,
|
||||
} = useCreateIngestionKey<AxiosError<RenderErrorResponseDTO>>();
|
||||
} = useMutation(createIngestionKeyApi, {
|
||||
onSuccess: (data) => {
|
||||
setActiveAPIKey(data.payload);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateAPIKey,
|
||||
isLoading: isLoadingUpdateAPIKey,
|
||||
} = useUpdateIngestionKey<AxiosError<RenderErrorResponseDTO>>();
|
||||
const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation(
|
||||
updateIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: deleteAPIKey,
|
||||
isLoading: isDeleteingAPIKey,
|
||||
} = useDeleteIngestionKey<AxiosError<RenderErrorResponseDTO>>();
|
||||
const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation(
|
||||
deleteIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: createLimitForIngestionKey,
|
||||
isLoading: isLoadingLimitForKey,
|
||||
} = useCreateIngestionKeyLimit<AxiosError<RenderErrorResponseDTO>>();
|
||||
} = 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<AxiosError<RenderErrorResponseDTO>>();
|
||||
} = 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<AxiosError<RenderErrorResponseDTO>>();
|
||||
const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation(
|
||||
deleteLimitsForIngestionKey,
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setIsDeleteLimitModalOpen(false);
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
clearSearch();
|
||||
|
||||
if (activeAPIKey && activeAPIKey.id) {
|
||||
deleteAPIKey(
|
||||
{
|
||||
pathParams: { keyId: activeAPIKey.id },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Ingestion key deleted successfully',
|
||||
});
|
||||
refetchAPIKeys();
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
if (activeAPIKey) {
|
||||
deleteAPIKey(activeAPIKey.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -425,31 +411,15 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
editForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (activeAPIKey && activeAPIKey.id) {
|
||||
updateAPIKey(
|
||||
{
|
||||
pathParams: { keyId: activeAPIKey.id },
|
||||
data: {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: new Date(
|
||||
dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
),
|
||||
},
|
||||
if (activeAPIKey) {
|
||||
updateAPIKey({
|
||||
id: activeAPIKey.id,
|
||||
data: {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Ingestion key updated successfully',
|
||||
});
|
||||
refetchAPIKeys();
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -465,30 +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,
|
||||
},
|
||||
{
|
||||
onSuccess: (_data) => {
|
||||
notifications.success({
|
||||
message: 'Ingestion key created successfully',
|
||||
});
|
||||
// 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);
|
||||
},
|
||||
},
|
||||
);
|
||||
createIngestionKey(requestPayload);
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -515,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);
|
||||
@@ -539,17 +489,9 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
const handleAddLimit = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signalName: string,
|
||||
): void => {
|
||||
if (!APIKey.id) {
|
||||
notifications.error({
|
||||
message: 'Invalid ingestion key',
|
||||
description: 'Cannot create limit for ingestion key without a valid ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
dailyLimit,
|
||||
secondsLimit,
|
||||
@@ -634,49 +576,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
createLimitForIngestionKey(
|
||||
{
|
||||
pathParams: { keyId: payload.keyID },
|
||||
data: {
|
||||
signal: payload.signal,
|
||||
config: payload.config,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Limit created successfully',
|
||||
});
|
||||
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',
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
createLimitForIngestionKey(payload);
|
||||
};
|
||||
|
||||
const handleUpdateLimit = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
if (!signal.id) {
|
||||
notifications.error({
|
||||
message: 'Invalid limit',
|
||||
description: 'Cannot update limit without a valid ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
dailyLimit,
|
||||
secondsLimit,
|
||||
@@ -738,34 +644,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
updateLimitForIngestionKey(
|
||||
{
|
||||
pathParams: { limitId: payload.limitID },
|
||||
data: {
|
||||
config: payload.config,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Limit updated successfully',
|
||||
});
|
||||
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',
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
updateLimitForIngestionKey(payload);
|
||||
};
|
||||
/* eslint-enable sonarjs/cognitive-complexity */
|
||||
|
||||
@@ -777,7 +656,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
};
|
||||
|
||||
const enableEditLimitMode = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
const dayCount = signal?.config?.day?.count;
|
||||
@@ -786,11 +665,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const dayCountConverted = countToUnit(dayCount || 0);
|
||||
const secondCountConverted = countToUnit(secondCount || 0);
|
||||
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
setCreateLimitForIngestionKeyError(null);
|
||||
setHasUpdateLimitForIngestionKeyError(false);
|
||||
setUpdateLimitForIngestionKeyError(null);
|
||||
|
||||
setActiveAPIKey(APIKey);
|
||||
setActiveSignal({
|
||||
...signal,
|
||||
@@ -829,31 +703,14 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const onDeleteLimitHandler = (): void => {
|
||||
if (activeSignal && activeSignal.id) {
|
||||
deleteLimitForKey(
|
||||
{
|
||||
pathParams: { limitId: activeSignal.id },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Limit deleted successfully',
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
setIsDeleteLimitModalOpen(false);
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
deleteLimitForKey(activeSignal.id);
|
||||
}
|
||||
};
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const handleCreateAlert = (
|
||||
APIKey: GatewaytypesIngestionKeyDTO,
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
let metricName = '';
|
||||
@@ -914,61 +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 onCopyKey = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (APIKey?.value) {
|
||||
handleCopyKey(APIKey.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onEditKey = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showEditModal(APIKey);
|
||||
};
|
||||
|
||||
const onDeleteKey = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteModal(APIKey);
|
||||
};
|
||||
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];
|
||||
@@ -985,25 +812,39 @@ 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 className="copy-key-btn" size={12} onClick={onCopyKey} />
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(APIKey.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-btn">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
onClick={onEditKey}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showEditModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={onDeleteKey}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1013,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>
|
||||
|
||||
@@ -1065,39 +906,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
limit?.config?.second?.size !== undefined ||
|
||||
limit?.config?.second?.count !== undefined;
|
||||
|
||||
const onEditSignalLimit = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, limit);
|
||||
};
|
||||
|
||||
const onDeleteSignalLimit = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteLimitModal(APIKey, limit);
|
||||
};
|
||||
|
||||
const onAddSignalLimit = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signalName,
|
||||
signal: signalName,
|
||||
config: {},
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveSignalLimit = (): void => {
|
||||
if (!hasLimits(signalName)) {
|
||||
handleAddLimit(APIKey, signalName);
|
||||
} else {
|
||||
handleUpdateLimit(APIKey, limitsDict[signalName]);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateSignalAlert = (): void =>
|
||||
handleCreateAlert(APIKey, limitsDict[signalName]);
|
||||
|
||||
return (
|
||||
<div className="signal" key={signalName}>
|
||||
<div className="header">
|
||||
@@ -1108,18 +916,22 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onEditSignalLimit}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, limit);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onDeleteSignalLimit}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteLimitModal(APIKey, limit);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -1128,8 +940,16 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
size="small"
|
||||
shape="round"
|
||||
icon={<PlusIcon size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey?.id && activeSignal)}
|
||||
onClick={onAddSignalLimit}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signalName,
|
||||
signal: signalName,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Limits
|
||||
</Button>
|
||||
@@ -1138,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
|
||||
@@ -1334,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">
|
||||
@@ -1368,7 +1188,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
loading={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={onSaveSignalLimit}
|
||||
onClick={(): void => {
|
||||
if (!hasLimits(signalName)) {
|
||||
handleAddLimit(APIKey, signalName);
|
||||
} else {
|
||||
handleUpdateLimit(APIKey, limitsDict[signalName]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
@@ -1449,7 +1275,9 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
className="set-alert-btn periscope-btn ghost"
|
||||
type="text"
|
||||
data-testid={`set-alert-btn-${signalName}`}
|
||||
onClick={onCreateSignalAlert}
|
||||
onClick={(): void =>
|
||||
handleCreateAlert(APIKey, limitsDict[signalName])
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -1564,7 +1392,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const handleTableChange = (pagination: TablePaginationConfig): void => {
|
||||
setPaginationParams({
|
||||
page: pagination?.current || 1,
|
||||
per_page: INITIAL_PAGE_SIZE,
|
||||
per_page: 10,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1662,7 +1490,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
showHeader={false}
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
pageSize: isSearching ? SEARCH_PAGE_SIZE : paginationParams?.per_page,
|
||||
pageSize: paginationParams?.per_page,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} Ingestion keys`,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { GatewaytypesGettableIngestionKeysDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -19,12 +18,6 @@ interface TestAllIngestionKeyProps extends Omit<AllIngestionKeyProps, 'data'> {
|
||||
data: TestIngestionKeyProps[];
|
||||
}
|
||||
|
||||
// Gateway API response type (uses actual schema types for contract safety)
|
||||
interface TestGatewayIngestionKeysResponse {
|
||||
status: string;
|
||||
data: GatewaytypesGettableIngestionKeysDTO;
|
||||
}
|
||||
|
||||
// Mock useHistory.push to capture navigation URL used by MultiIngestionSettings
|
||||
const mockPush = jest.fn() as jest.MockedFunction<(path: string) => void>;
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -93,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: TestGatewayIngestionKeysResponse = {
|
||||
const response: TestAllIngestionKeyProps = {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key One',
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(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)),
|
||||
),
|
||||
);
|
||||
@@ -266,95 +257,4 @@ describe('MultiIngestionSettings Page', () => {
|
||||
'signoz.meter.log.size',
|
||||
);
|
||||
});
|
||||
|
||||
it('switches to search API when search text is entered', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
const getResponse: TestGatewayIngestionKeysResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Regular',
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret1',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k1',
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const searchResponse: TestGatewayIngestionKeysResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: [
|
||||
{
|
||||
name: 'Key Search Result',
|
||||
expires_at: new Date(TEST_EXPIRES_AT),
|
||||
value: 'secret2',
|
||||
workspace_id: TEST_WORKSPACE_ID,
|
||||
id: 'k2',
|
||||
created_at: new Date(TEST_CREATED_UPDATED),
|
||||
updated_at: new Date(TEST_CREATED_UPDATED),
|
||||
tags: [],
|
||||
limits: [],
|
||||
},
|
||||
],
|
||||
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const getHandler = jest.fn();
|
||||
const searchHandler = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v2/gateway/ingestion_keys', (req, res, ctx) => {
|
||||
if (req.url.pathname.endsWith('/search')) {
|
||||
return undefined;
|
||||
}
|
||||
getHandler();
|
||||
return res(ctx.status(200), ctx.json(getResponse));
|
||||
}),
|
||||
rest.get('*/api/v2/gateway/ingestion_keys/search', (_req, res, ctx) => {
|
||||
searchHandler();
|
||||
return res(ctx.status(200), ctx.json(searchResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<MultiIngestionSettings />, undefined, {
|
||||
initialRoute: INGESTION_SETTINGS_ROUTE,
|
||||
});
|
||||
|
||||
await screen.findByText('Key Regular');
|
||||
expect(getHandler).toHaveBeenCalled();
|
||||
expect(searchHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Reset getHandler count to verify it's not called again during search
|
||||
getHandler.mockClear();
|
||||
|
||||
// Type in search box
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
'Search for ingestion key...',
|
||||
);
|
||||
await user.type(searchInput, 'test');
|
||||
|
||||
await screen.findByText('Key Search Result');
|
||||
expect(searchHandler).toHaveBeenCalled();
|
||||
expect(getHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Clear search
|
||||
searchHandler.mockClear();
|
||||
getHandler.mockClear();
|
||||
await user.clear(searchInput);
|
||||
|
||||
await screen.findByText('Key Regular');
|
||||
// Search API should be disabled when not searching
|
||||
expect(searchHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -407,10 +407,6 @@
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Form, Input, Select, Typography } from 'antd';
|
||||
import { Form, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import getVersion from 'api/v1/version/get';
|
||||
import get from 'api/v2/sessions/context/get';
|
||||
import post from 'api/v2/sessions/email_password/post';
|
||||
@@ -220,20 +220,6 @@ function Login(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPasswordClick = useCallback((): void => {
|
||||
const email = form.getFieldValue('email');
|
||||
|
||||
if (!email || !sessionsContext || !sessionsContext?.orgs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(ROUTES.FORGOT_PASSWORD, {
|
||||
email,
|
||||
orgId: sessionsOrgId,
|
||||
orgs: sessionsContext.orgs,
|
||||
});
|
||||
}, [form, sessionsContext, sessionsOrgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (callbackAuthError) {
|
||||
setErrorMessage(
|
||||
@@ -359,16 +345,11 @@ function Login(): JSX.Element {
|
||||
<ParentContainer>
|
||||
<div className="password-label-container">
|
||||
<Label htmlFor="Password">Password</Label>
|
||||
<Typography.Link
|
||||
className="forgot-password-link"
|
||||
href="#"
|
||||
onClick={(event): void => {
|
||||
event.preventDefault();
|
||||
handleForgotPasswordClick();
|
||||
}}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography.Link>
|
||||
<Tooltip title="Ask your admin to reset your password and send you a new invite link">
|
||||
<Typography.Link className="forgot-password-link">
|
||||
Forgot password?
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<FormContainer.Item name="password">
|
||||
<Input.Password
|
||||
|
||||
@@ -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,10 +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 ||
|
||||
(error as AxiosError)?.message ||
|
||||
'Something went wrong'}
|
||||
{(error as AxiosError)?.message || 'Something went wrong'}
|
||||
</Typography.Text>
|
||||
|
||||
<div className="ingestion-setup-details-links">
|
||||
@@ -185,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}
|
||||
@@ -195,9 +186,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INGESTION_KEY_COPIED}`,
|
||||
{},
|
||||
);
|
||||
if (firstIngestionKey?.value) {
|
||||
handleCopyKey(firstIngestionKey.value);
|
||||
}
|
||||
handleCopyKey(firstIngestionKey?.value);
|
||||
}}
|
||||
/>
|
||||
</Typography.Text>
|
||||
|
||||
@@ -7,10 +7,7 @@ import { defaultTo } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import {
|
||||
GettableAuthDomain,
|
||||
GoogleAuthConfig,
|
||||
} from 'types/api/v1/domains/list';
|
||||
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||
import { PostableAuthDomain } from 'types/api/v1/domains/post';
|
||||
|
||||
import AuthnProviderSelector from './AuthnProviderSelector';
|
||||
@@ -54,35 +51,9 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const samlEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.SSO)?.active || false;
|
||||
|
||||
const getGoogleAuthConfig = (): GoogleAuthConfig | undefined => {
|
||||
const config = form.getFieldValue('googleAuthConfig');
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Convert domainToAdminEmailList array to domainToAdminEmail Record
|
||||
const { domainToAdminEmailList, ...rest } = config;
|
||||
const domainToAdminEmail: Record<string, string> = {};
|
||||
|
||||
if (Array.isArray(domainToAdminEmailList)) {
|
||||
domainToAdminEmailList.forEach(
|
||||
(item: { domain?: string; adminEmail?: string }) => {
|
||||
if (item.domain && item.adminEmail) {
|
||||
domainToAdminEmail[item.domain] = item.adminEmail;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(Object.keys(domainToAdminEmail).length > 0 && { domainToAdminEmail }),
|
||||
};
|
||||
};
|
||||
|
||||
const onSubmitHandler = async (): Promise<void> => {
|
||||
const name = form.getFieldValue('name');
|
||||
const googleAuthConfig = getGoogleAuthConfig();
|
||||
const googleAuthConfig = form.getFieldValue('googleAuthConfig');
|
||||
const samlConfig = form.getFieldValue('samlConfig');
|
||||
const oidcConfig = form.getFieldValue('oidcConfig');
|
||||
|
||||
@@ -122,39 +93,14 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
footer={null}
|
||||
onCancel={onClose}
|
||||
width={authnProvider ? 980 : undefined}
|
||||
>
|
||||
<Modal open footer={null} onCancel={onClose}>
|
||||
<Form
|
||||
name="auth-domain"
|
||||
initialValues={defaultTo(
|
||||
record
|
||||
? {
|
||||
...record,
|
||||
googleAuthConfig: record.googleAuthConfig
|
||||
? {
|
||||
...record.googleAuthConfig,
|
||||
domainToAdminEmailList: record.googleAuthConfig.domainToAdminEmail
|
||||
? Object.entries(record.googleAuthConfig.domainToAdminEmail).map(
|
||||
([domain, adminEmail]) => ({
|
||||
domain,
|
||||
adminEmail,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
},
|
||||
)}
|
||||
initialValues={defaultTo(record, {
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
})}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import { ChevronDown, ChevronRight, CircleHelp } from 'lucide-react';
|
||||
|
||||
import DomainMappingList from './components/DomainMappingList';
|
||||
import EmailTagInput from './components/EmailTagInput';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
import { Form, Input, Typography } from 'antd';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'workspace-groups' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureGoogleAuthAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
const fetchGroups = Form.useWatch(['googleAuthConfig', 'fetchGroups'], form);
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleWorkspaceGroupsChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const isExpanding = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
setExpandedSection(isExpanding ? 'workspace-groups' : null);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="google-auth">
|
||||
<section className="google-auth__header">
|
||||
<h3 className="google-auth__title typography-label-medium-600">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit Google Authentication
|
||||
</h3>
|
||||
<p className="google-auth__description typography-paragraph-base-400">
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="description">
|
||||
Enter OAuth 2.0 credentials obtained from the Google API Console below.
|
||||
Read the{' '}
|
||||
<a
|
||||
@@ -53,227 +25,50 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</Typography.Paragraph>
|
||||
</section>
|
||||
|
||||
<div className="google-auth__columns">
|
||||
{/* Left Column - Core OAuth Settings */}
|
||||
<div className="google-auth__left">
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="google-domain"
|
||||
>
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item name="name" className="google-auth__form-item">
|
||||
<Input id="google-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="google-client-id"
|
||||
>
|
||||
Client ID
|
||||
<Tooltip title="ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'clientId']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="google-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Client ID"
|
||||
name={['googleAuthConfig', 'clientId']}
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="google-client-secret"
|
||||
>
|
||||
Client Secret
|
||||
<Tooltip title="It is the application's secret.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'clientSecret']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="google-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Client Secret"
|
||||
name={['googleAuthConfig', 'clientSecret']}
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `It is the application's secret.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-skip-email-verification"
|
||||
labelName="Skip Email Verification"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'insecureSkipEmailVerified'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title='Whether to skip email verification. Defaults to "false"'>
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="Google OAuth2 won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Google Workspace Groups (Advanced) */}
|
||||
<div className="google-auth__right">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={
|
||||
expandedSection === 'workspace-groups' ? ['workspace-groups'] : []
|
||||
}
|
||||
onChange={handleWorkspaceGroupsChange}
|
||||
className="google-auth__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="workspace-groups"
|
||||
header={
|
||||
<div className="google-auth__collapse-header">
|
||||
{expandedSection !== 'workspace-groups' ? (
|
||||
<ChevronRight size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
<div className="google-auth__collapse-header-text">
|
||||
<h4 className="google-auth__section-title typography-label-base-600">
|
||||
Google Workspace Groups (Advanced)
|
||||
</h4>
|
||||
<p className="google-auth__section-description typography-paragraph-small-400">
|
||||
Enable group fetching to retrieve user groups from Google Workspace.
|
||||
Requires a Service Account with domain-wide delegation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="google-auth__group-content">
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'fetchGroups']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-fetch-groups"
|
||||
labelName="Fetch Groups"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['googleAuthConfig', 'fetchGroups'], checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Enable fetching Google Workspace groups for the user. Requires service account configuration.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{fetchGroups && (
|
||||
<div className="google-auth__group-fields">
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="google-service-account-json"
|
||||
>
|
||||
Service Account JSON
|
||||
<Tooltip title="The JSON content of the Google Service Account credentials file. Required for group fetching.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'serviceAccountJson']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<TextArea
|
||||
id="google-service-account-json"
|
||||
rows={3}
|
||||
placeholder="Paste service account JSON"
|
||||
className="google-auth__textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<DomainMappingList
|
||||
fieldNamePrefix={['googleAuthConfig', 'domainToAdminEmailList']}
|
||||
/>
|
||||
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'fetchTransitiveGroupMembership']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-transitive-membership"
|
||||
labelName="Fetch Transitive Group Membership"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'fetchTransitiveGroupMembership'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="If enabled, recursively fetch groups that contain other groups (transitive membership).">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="google-allowed-groups"
|
||||
>
|
||||
Allowed Groups
|
||||
<Tooltip title="Optional list of allowed groups. If configured, only users belonging to one of these groups will be allowed to login.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'allowedGroups']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<EmailTagInput placeholder="Type a group email and press Enter" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['googleAuthConfig', 'roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="Google OAuth2 won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,209 +1,110 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Tooltip } from 'antd';
|
||||
import { CircleHelp } from 'lucide-react';
|
||||
|
||||
import ClaimMappingSection from './components/ClaimMappingSection';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'claim-mapping' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureOIDCAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleClaimMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'claim-mapping' : null);
|
||||
}, []);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="google-auth">
|
||||
<section className="google-auth__header">
|
||||
<h3 className="google-auth__title typography-label-medium-600">
|
||||
<div className="saml">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit OIDC Authentication
|
||||
</h3>
|
||||
<p className="google-auth__description typography-paragraph-base-400">
|
||||
Configure OpenID Connect Single Sign-On with your Identity Provider. Read
|
||||
the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
<div className="google-auth__columns">
|
||||
{/* Left Column - Core OIDC Settings */}
|
||||
<div className="google-auth__left">
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="oidc-domain"
|
||||
>
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item name="name" className="google-auth__form-item">
|
||||
<Input id="oidc-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="oidc-issuer"
|
||||
>
|
||||
Issuer URL
|
||||
<Tooltip title='The URL identifier for the OIDC provider. For example: "https://accounts.google.com" or "https://login.salesforce.com".'>
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'issuer']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="oidc-issuer" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Issuer URL"
|
||||
name={['oidcConfig', 'issuer']}
|
||||
tooltip={{
|
||||
title: `It is the URL identifier for the service. For example: "https://accounts.google.com" or "https://login.salesforce.com".`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="oidc-issuer-alias"
|
||||
>
|
||||
Issuer Alias
|
||||
<Tooltip title="Optional: Override the issuer URL from .well-known/openid-configuration for providers like Azure or Oracle IDCS.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'issuerAlias']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="oidc-issuer-alias" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Issuer Alias"
|
||||
name={['oidcConfig', 'issuerAlias']}
|
||||
tooltip={{
|
||||
title: `Some offspec providers like Azure, Oracle IDCS have oidc discovery url different from issuer url which causes issuerValidation to fail.
|
||||
This provides a way to override the Issuer url from the .well-known/openid-configuration issuer`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="oidc-client-id"
|
||||
>
|
||||
Client ID
|
||||
<Tooltip title="The application's client ID from your OIDC provider.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'clientId']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="oidc-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Client ID"
|
||||
name={['oidcConfig', 'clientId']}
|
||||
tooltip={{ title: `It is the application's ID.` }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="oidc-client-secret"
|
||||
>
|
||||
Client Secret
|
||||
<Tooltip title="The application's client secret from your OIDC provider.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'clientSecret']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="oidc-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Client Secret"
|
||||
name={['oidcConfig', 'clientSecret']}
|
||||
tooltip={{ title: `It is the application's secret.` }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="oidc-skip-email-verification"
|
||||
labelName="Skip Email Verification"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['oidcConfig', 'insecureSkipEmailVerified'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title='Whether to skip email verification. Defaults to "false"'>
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Email Claim Mapping"
|
||||
name={['oidcConfig', 'claimMapping', 'email']}
|
||||
tooltip={{
|
||||
title: `Mapping of email claims to the corresponding email field in the token.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'getUserInfo']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="oidc-get-user-info"
|
||||
labelName="Get User Info"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['oidcConfig', 'getUserInfo'], checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Use the userinfo endpoint to get additional claims. Useful when providers return thin ID tokens.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Skip Email Verification"
|
||||
name={['oidcConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Whether to skip email verification. Defaults to "false"`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="OIDC won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Get User Info"
|
||||
name={['oidcConfig', 'getUserInfo']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Uses the userinfo endpoint to get additional claims for the token. This is especially useful where upstreams return "thin" id tokens`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
|
||||
{/* Right Column - Advanced Settings */}
|
||||
<div className="google-auth__right">
|
||||
<ClaimMappingSection
|
||||
fieldNamePrefix={['oidcConfig', 'claimMapping']}
|
||||
isExpanded={expandedSection === 'claim-mapping'}
|
||||
onExpandChange={handleClaimMappingChange}
|
||||
/>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['oidcConfig', 'roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="OIDC won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,177 +1,82 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Tooltip } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import { CircleHelp } from 'lucide-react';
|
||||
|
||||
import AttributeMappingSection from './components/AttributeMappingSection';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'attribute-mapping' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureSAMLAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleAttributeMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'attribute-mapping' : null);
|
||||
}, []);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="google-auth">
|
||||
<section className="google-auth__header">
|
||||
<h3 className="google-auth__title typography-label-medium-600">
|
||||
<div className="saml">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit SAML Authentication
|
||||
</h3>
|
||||
<p className="google-auth__description typography-paragraph-base-400">
|
||||
Configure SAML 2.0 Single Sign-On with your Identity Provider. Read the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
<div className="google-auth__columns">
|
||||
{/* Left Column - Core SAML Settings */}
|
||||
<div className="google-auth__left">
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="saml-domain"
|
||||
>
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item name="name" className="google-auth__form-item">
|
||||
<Input id="saml-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="saml-acs-url"
|
||||
>
|
||||
SAML ACS URL
|
||||
<Tooltip title="The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="saml-acs-url" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="SAML ACS URL"
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
tooltip={{
|
||||
title: `The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="saml-entity-id"
|
||||
>
|
||||
SAML Entity ID
|
||||
<Tooltip title="The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<Input id="saml-entity-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="SAML Entity ID"
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
tooltip={{
|
||||
title: `The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__field-group">
|
||||
<label
|
||||
className="google-auth__label typography-label-base-500"
|
||||
htmlFor="saml-certificate"
|
||||
>
|
||||
SAML X.509 Certificate
|
||||
<Tooltip title="The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlCert']}
|
||||
className="google-auth__form-item"
|
||||
>
|
||||
<TextArea
|
||||
id="saml-certificate"
|
||||
rows={3}
|
||||
placeholder="Paste X.509 certificate"
|
||||
className="google-auth__textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="SAML X.509 Certificate"
|
||||
name={['samlConfig', 'samlCert']}
|
||||
tooltip={{
|
||||
title: `The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata of the identity provider. Example: <ds:X509Certificate><ds:X509Certificate>{samlCert}</ds:X509Certificate></ds:X509Certificate>`,
|
||||
}}
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="google-auth__checkbox-row">
|
||||
<Form.Item
|
||||
name={['samlConfig', 'insecureSkipAuthNRequestsSigned']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="saml-skip-signing"
|
||||
labelName="Skip Signing AuthN Requests"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['samlConfig', 'insecureSkipAuthNRequestsSigned'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Whether to skip signing the SAML requests. For providers like JumpCloud, this should be enabled.">
|
||||
<CircleHelp size={14} className="google-auth__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Skip Signing AuthN Requests"
|
||||
name={['samlConfig', 'insecureSkipAuthNRequestsSigned']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Whether to skip signing the SAML requests. It can typically be found in the WantAuthnRequestsSigned attribute of the IDPSSODescriptor element in the SAML metadata of the identity provider. Example: <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
For providers like jumpcloud, this should be set to true.Note: This is the reverse of WantAuthnRequestsSigned. If WantAuthnRequestsSigned is false, then InsecureSkipAuthNRequestsSigned should be true.`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="SAML won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Advanced Settings */}
|
||||
<div className="google-auth__right">
|
||||
<AttributeMappingSection
|
||||
fieldNamePrefix={['samlConfig', 'attributeMapping']}
|
||||
isExpanded={expandedSection === 'attribute-mapping'}
|
||||
onExpandChange={handleAttributeMappingChange}
|
||||
/>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['samlConfig', 'roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="SAML won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,248 +2,23 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__columns {
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__right {
|
||||
border-left: 1px solid var(--l3-border);
|
||||
padding-left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// --- Form field layout ---
|
||||
&__field-group {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// --- Checkbox row: label on left, checkbox on right ---
|
||||
&__checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// --- Input styles (matching auth flow standards) ---
|
||||
input,
|
||||
textarea {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
.description {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Textarea should not have fixed height
|
||||
textarea {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// --- Textarea specific styles ---
|
||||
&__textarea {
|
||||
min-height: 60px !important;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Checkbox border visibility (lighter for dark modal bg) ---
|
||||
button[role='checkbox'] {
|
||||
border: 1px solid var(--l2-foreground) !important;
|
||||
border-radius: 2px;
|
||||
|
||||
&[data-state='checked'] {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collapsible section ---
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
// --- Group fields that scroll when "Fetch Groups" is on ---
|
||||
&__group-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__group-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Remove bottom margins from children since we rely on gap
|
||||
.google-auth__field-group,
|
||||
.google-auth__checkbox-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox thin scrollbar
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
.callout {
|
||||
@@ -251,37 +26,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- Light mode overrides ---
|
||||
.lightMode {
|
||||
.google-auth {
|
||||
input,
|
||||
textarea {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.saml {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
.attribute-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// --- Collapsible section ---
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
// --- Content area with scroll ---
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox thin scrollbar
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
// --- Field group layout ---
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// --- Input styles ---
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Light mode overrides ---
|
||||
.lightMode {
|
||||
.attribute-mapping-section {
|
||||
input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
import { ChevronDown, ChevronRight, CircleHelp } from 'lucide-react';
|
||||
|
||||
import './AttributeMappingSection.styles.scss';
|
||||
|
||||
interface AttributeMappingSectionProps {
|
||||
/** The form field name prefix for the attribute mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function AttributeMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: AttributeMappingSectionProps): JSX.Element {
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['attribute-mapping'] : [];
|
||||
|
||||
return (
|
||||
<div className="attribute-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="attribute-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="attribute-mapping"
|
||||
header={
|
||||
<div className="attribute-mapping-section__collapse-header">
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="attribute-mapping-section__collapse-header-text">
|
||||
<h4 className="attribute-mapping-section__section-title typography-label-base-600">
|
||||
Attribute Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="attribute-mapping-section__section-description typography-paragraph-small-400">
|
||||
Configure how SAML assertion attributes from your Identity Provider map
|
||||
to SigNoz user attributes. Leave empty to use default values. Note:
|
||||
Email is always extracted from the NameID assertion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="attribute-mapping-section__content">
|
||||
{/* Name Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label typography-label-base-500"
|
||||
htmlFor="name-attribute"
|
||||
>
|
||||
Name Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's display name. Default: 'name'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'nameAttribute']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="name-attribute" placeholder="name" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Groups Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label typography-label-base-500"
|
||||
htmlFor="groups-attribute"
|
||||
>
|
||||
Groups Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's group memberships. Used for role mapping. Default: 'groups'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'groupsAttribute']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="groups-attribute" placeholder="groups" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Role Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label typography-label-base-500"
|
||||
htmlFor="role-attribute"
|
||||
>
|
||||
Role Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's role directly from the IDP. Default: 'role'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'roleAttribute']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="role-attribute" placeholder="role" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributeMappingSection;
|
||||
@@ -1,151 +0,0 @@
|
||||
.claim-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// --- Collapsible section ---
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
// --- Content area with scroll ---
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox thin scrollbar
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
// --- Field group layout ---
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// --- Input styles ---
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Light mode overrides ---
|
||||
.lightMode {
|
||||
.claim-mapping-section {
|
||||
input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
import { ChevronDown, ChevronRight, CircleHelp } from 'lucide-react';
|
||||
|
||||
import './ClaimMappingSection.styles.scss';
|
||||
|
||||
interface ClaimMappingSectionProps {
|
||||
/** The form field name prefix for the claim mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function ClaimMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: ClaimMappingSectionProps): JSX.Element {
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['claim-mapping'] : [];
|
||||
|
||||
return (
|
||||
<div className="claim-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="claim-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="claim-mapping"
|
||||
header={
|
||||
<div className="claim-mapping-section__collapse-header">
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="claim-mapping-section__collapse-header-text">
|
||||
<h4 className="claim-mapping-section__section-title typography-label-base-600">
|
||||
Claim Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="claim-mapping-section__section-description typography-paragraph-small-400">
|
||||
Configure how claims from your Identity Provider map to SigNoz user
|
||||
attributes. Leave empty to use default values.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="claim-mapping-section__content">
|
||||
{/* Email Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label
|
||||
className="claim-mapping-section__label typography-label-base-500"
|
||||
htmlFor="email-claim"
|
||||
>
|
||||
Email Claim
|
||||
<Tooltip title="The claim key that contains the user's email address. Default: 'email'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'email']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="email-claim" placeholder="email" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Name Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label
|
||||
className="claim-mapping-section__label typography-label-base-500"
|
||||
htmlFor="name-claim"
|
||||
>
|
||||
Name Claim
|
||||
<Tooltip title="The claim key that contains the user's display name. Default: 'name'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'name']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="name-claim" placeholder="name" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Groups Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label
|
||||
className="claim-mapping-section__label typography-label-base-500"
|
||||
htmlFor="groups-claim"
|
||||
>
|
||||
Groups Claim
|
||||
<Tooltip title="The claim key that contains the user's group memberships. Used for role mapping. Default: 'groups'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'groups']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="groups-claim" placeholder="groups" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Role Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label
|
||||
className="claim-mapping-section__label typography-label-base-500"
|
||||
htmlFor="role-claim"
|
||||
>
|
||||
Role Claim
|
||||
<Tooltip title="The claim key that contains the user's role directly from the IDP. Default: 'role'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'role']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="role-claim" placeholder="role" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimMappingSection;
|
||||
@@ -1,118 +0,0 @@
|
||||
.domain-mapping-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__remove-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: #e5484d !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
color: #e5484d !important;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7 !important;
|
||||
background: rgba(229, 72, 77, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode overrides
|
||||
.lightMode {
|
||||
.domain-mapping-list {
|
||||
&__row input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form } from 'antd';
|
||||
|
||||
import './DomainMappingList.styles.scss';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
interface DomainMappingListProps {
|
||||
/** The form field name prefix for the domain mapping list */
|
||||
fieldNamePrefix: string[];
|
||||
}
|
||||
|
||||
function DomainMappingList({
|
||||
fieldNamePrefix,
|
||||
}: DomainMappingListProps): JSX.Element {
|
||||
const validateEmail = useCallback(
|
||||
(_: unknown, value: string): Promise<void> => {
|
||||
if (!value) {
|
||||
return Promise.reject(new Error('Admin email is required'));
|
||||
}
|
||||
if (!EMAIL_REGEX.test(value)) {
|
||||
return Promise.reject(new Error('Please enter a valid email'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="domain-mapping-list">
|
||||
<div className="domain-mapping-list__header">
|
||||
<span className="domain-mapping-list__title">
|
||||
Domain to Admin Email Mapping
|
||||
</span>
|
||||
<p className="domain-mapping-list__description">
|
||||
Map workspace domains to admin emails for service account impersonation.
|
||||
Use "*" as a wildcard for any domain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form.List name={fieldNamePrefix}>
|
||||
{(fields, { add, remove }): JSX.Element => (
|
||||
<div className="domain-mapping-list__items">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="domain-mapping-list__row">
|
||||
<Form.Item
|
||||
name={[field.name, 'domain']}
|
||||
className="domain-mapping-list__field"
|
||||
rules={[{ required: true, message: 'Domain is required' }]}
|
||||
>
|
||||
<Input placeholder="Domain (e.g., example.com or *)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, 'adminEmail']}
|
||||
className="domain-mapping-list__field"
|
||||
rules={[{ validator: validateEmail }]}
|
||||
>
|
||||
<Input placeholder="Admin Email" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="domain-mapping-list__remove-btn"
|
||||
onClick={(): void => remove(field.name)}
|
||||
aria-label="Remove mapping"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="dashed"
|
||||
onClick={(): void => add({ domain: '', adminEmail: '' })}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
className="domain-mapping-list__add-btn"
|
||||
>
|
||||
Add Domain Mapping
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainMappingList;
|
||||
@@ -1,30 +0,0 @@
|
||||
.email-tag-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
.ant-select-selection-search {
|
||||
// margin-inline-start: 0 !important;
|
||||
|
||||
input {
|
||||
height: 32px !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
box-shadow: none !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin: 0;
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
|
||||
import './EmailTagInput.styles.scss';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
interface EmailTagInputProps {
|
||||
/** Current value (array of email strings) */
|
||||
value?: string[];
|
||||
/** Change handler */
|
||||
onChange?: (value: string[]) => void;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function EmailTagInput({
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = 'Type an email and press Enter',
|
||||
}: EmailTagInputProps): JSX.Element {
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValues: string[]): void => {
|
||||
const addedValues = newValues.filter((v) => !value.includes(v));
|
||||
const invalidEmail = addedValues.find((v) => !EMAIL_REGEX.test(v));
|
||||
|
||||
if (invalidEmail) {
|
||||
setValidationError(`"${invalidEmail}" is not a valid email`);
|
||||
return;
|
||||
}
|
||||
setValidationError('');
|
||||
onChange?.(newValues);
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="email-tag-input">
|
||||
<Tooltip
|
||||
title={validationError}
|
||||
open={!!validationError}
|
||||
placement="topRight"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
tokenSeparators={[',', ' ']}
|
||||
className="email-tag-input__select"
|
||||
allowClear
|
||||
status={validationError ? 'error' : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailTagInput;
|
||||
@@ -1,328 +0,0 @@
|
||||
.role-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 24px;
|
||||
|
||||
// --- Collapsible section ---
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
// --- Content area with scroll ---
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox thin scrollbar
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
// --- Field group layout ---
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// --- Checkbox row ---
|
||||
&__checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// --- Select styling ---
|
||||
&__select {
|
||||
width: 100%;
|
||||
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Group mappings section ---
|
||||
&__group-mappings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__group-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__group-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
&--group {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
&--role {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
&__remove-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: #e5484d !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
color: #e5484d !important;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7 !important;
|
||||
background: rgba(229, 72, 77, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Checkbox border visibility ---
|
||||
button[role='checkbox'] {
|
||||
border: 1px solid var(--l2-foreground) !important;
|
||||
border-radius: 2px;
|
||||
|
||||
&[data-state='checked'] {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Input styles ---
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Light mode overrides ---
|
||||
.lightMode {
|
||||
.role-mapping-section {
|
||||
input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__select {
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Select, Tooltip } from 'antd';
|
||||
import { ChevronDown, ChevronRight, CircleHelp } from 'lucide-react';
|
||||
|
||||
import './RoleMappingSection.styles.scss';
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: 'VIEWER', label: 'VIEWER' },
|
||||
{ value: 'EDITOR', label: 'EDITOR' },
|
||||
{ value: 'ADMIN', label: 'ADMIN' },
|
||||
];
|
||||
|
||||
interface RoleMappingSectionProps {
|
||||
/** The form field name prefix for the role mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function RoleMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: RoleMappingSectionProps): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
const useRoleAttributeDirectly = Form.useWatch(
|
||||
[...fieldNamePrefix, 'useRoleAttributeDirectly'],
|
||||
form,
|
||||
);
|
||||
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['role-mapping'] : [];
|
||||
|
||||
return (
|
||||
<div className="role-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="role-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="role-mapping"
|
||||
header={
|
||||
<div className="role-mapping-section__collapse-header">
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="role-mapping-section__collapse-header-text">
|
||||
<h4 className="role-mapping-section__section-title typography-label-base-600">
|
||||
Role Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="role-mapping-section__section-description typography-paragraph-small-400">
|
||||
Configure how user roles are determined from your Identity Provider.
|
||||
You can either use a direct role attribute or map IDP groups to SigNoz
|
||||
roles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="role-mapping-section__content">
|
||||
{/* Default Role */}
|
||||
<div className="role-mapping-section__field-group">
|
||||
<label
|
||||
className="role-mapping-section__label typography-label-base-500"
|
||||
htmlFor="default-role"
|
||||
>
|
||||
Default Role
|
||||
<Tooltip title='The default role assigned to new SSO users if no other role mapping applies. Default: "VIEWER"'>
|
||||
<CircleHelp size={14} className="role-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'defaultRole']}
|
||||
className="role-mapping-section__form-item"
|
||||
initialValue="VIEWER"
|
||||
>
|
||||
<Select
|
||||
id="default-role"
|
||||
options={ROLE_OPTIONS}
|
||||
className="role-mapping-section__select"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Use Role Attribute Directly */}
|
||||
<div className="role-mapping-section__checkbox-row">
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'useRoleAttributeDirectly']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="use-role-attribute-directly"
|
||||
labelName="Use Role Attribute Directly"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
[...fieldNamePrefix, 'useRoleAttributeDirectly'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="If enabled, the role claim/attribute from the IDP will be used directly instead of group mappings. The role value must match a SigNoz role (VIEWER, EDITOR, or ADMIN).">
|
||||
<CircleHelp size={14} className="role-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Group to Role Mappings - only show when useRoleAttributeDirectly is false */}
|
||||
{!useRoleAttributeDirectly && (
|
||||
<div className="role-mapping-section__group-mappings">
|
||||
<div className="role-mapping-section__group-header">
|
||||
<span className="role-mapping-section__group-title">
|
||||
Group to Role Mappings
|
||||
</span>
|
||||
<p className="role-mapping-section__group-description">
|
||||
Map IDP group names to SigNoz roles. If a user belongs to multiple
|
||||
groups, the highest privilege role will be assigned.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form.List name={[...fieldNamePrefix, 'groupMappings']}>
|
||||
{(fields, { add, remove }): JSX.Element => (
|
||||
<div className="role-mapping-section__items">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="role-mapping-section__row">
|
||||
<Form.Item
|
||||
name={[field.name, 'groupName']}
|
||||
className="role-mapping-section__field role-mapping-section__field--group"
|
||||
rules={[{ required: true, message: 'Group name is required' }]}
|
||||
>
|
||||
<Input placeholder="IDP Group Name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, 'role']}
|
||||
className="role-mapping-section__field role-mapping-section__field--role"
|
||||
rules={[{ required: true, message: 'Role is required' }]}
|
||||
initialValue="VIEWER"
|
||||
>
|
||||
<Select
|
||||
options={ROLE_OPTIONS}
|
||||
className="role-mapping-section__select"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="role-mapping-section__remove-btn"
|
||||
onClick={(): void => remove(field.name)}
|
||||
aria-label="Remove mapping"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="dashed"
|
||||
onClick={(): void => add({ groupName: '', role: 'VIEWER' })}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
className="role-mapping-section__add-btn"
|
||||
>
|
||||
Add Group Mapping
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleMappingSection;
|
||||
@@ -32,7 +32,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGIN]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.FORGOT_PASSWORD]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.LOGS_BASE]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
|
||||
|
||||
@@ -7,6 +7,4 @@ import { handlers } from './handlers';
|
||||
// This configures a request mocking server with the given request handlers.
|
||||
export const server = setupServer(...handlers);
|
||||
|
||||
export * from './utils';
|
||||
|
||||
export { rest };
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ResponseResolver, restContext, RestRequest } from 'msw';
|
||||
|
||||
export const createErrorResponse = (
|
||||
status: number,
|
||||
code: string,
|
||||
message: string,
|
||||
): ResponseResolver<RestRequest, typeof restContext> => (
|
||||
_req,
|
||||
res,
|
||||
ctx,
|
||||
): ReturnType<ResponseResolver<RestRequest, typeof restContext>> =>
|
||||
res(
|
||||
ctx.status(status),
|
||||
ctx.json({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const handleInternalServerError = createErrorResponse(
|
||||
500,
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
'Internal server error occurred',
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { render, waitFor } from 'tests/test-utils';
|
||||
|
||||
import ForgotPassword from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
>;
|
||||
|
||||
describe('ForgotPassword Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Route State Handling', () => {
|
||||
it('redirects to login when route state is missing', async () => {
|
||||
render(<ForgotPassword />, undefined, {
|
||||
initialRoute: '/forgot-password',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when route state is missing', () => {
|
||||
const { container } = render(<ForgotPassword />, undefined, {
|
||||
initialRoute: '/forgot-password',
|
||||
});
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ForgotPasswordContainer, {
|
||||
ForgotPasswordRouteState,
|
||||
} from 'container/ForgotPassword';
|
||||
import history from 'lib/history';
|
||||
|
||||
import '../Login/Login.styles.scss';
|
||||
|
||||
function ForgotPassword(): JSX.Element | null {
|
||||
const location = useLocation<ForgotPasswordRouteState | undefined>();
|
||||
const routeState = location.state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!routeState?.email) {
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
}, [routeState]);
|
||||
|
||||
if (!routeState?.email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPageContainer>
|
||||
<div className="auth-form-card">
|
||||
<ForgotPasswordContainer
|
||||
email={routeState.email}
|
||||
orgId={routeState.orgId}
|
||||
orgs={routeState.orgs}
|
||||
/>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -17,41 +17,17 @@ export interface GettableAuthDomain {
|
||||
oidcConfig?: OIDCConfig;
|
||||
}
|
||||
|
||||
export interface SAMLAttributeMapping {
|
||||
nameAttribute?: string;
|
||||
groupsAttribute?: string;
|
||||
roleAttribute?: string;
|
||||
}
|
||||
|
||||
export interface SAMLConfig {
|
||||
samlEntity: string;
|
||||
samlIdp: string;
|
||||
samlCert: string;
|
||||
insecureSkipAuthNRequestsSigned: boolean;
|
||||
attributeMapping?: SAMLAttributeMapping;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface RoleMappingConfig {
|
||||
defaultRole?: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
useRoleAttributeDirectly?: boolean;
|
||||
groupMappings?: Array<{
|
||||
groupName: string;
|
||||
role: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GoogleAuthConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectURI: string;
|
||||
insecureSkipEmailVerified?: boolean;
|
||||
fetchGroups?: boolean;
|
||||
serviceAccountJson?: string;
|
||||
domainToAdminEmail?: Record<string, string>;
|
||||
fetchTransitiveGroupMembership?: boolean;
|
||||
allowedGroups?: string[];
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface OIDCConfig {
|
||||
@@ -62,14 +38,10 @@ export interface OIDCConfig {
|
||||
claimMapping: ClaimMapping;
|
||||
insecureSkipEmailVerified: boolean;
|
||||
getUserInfo: boolean;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface ClaimMapping {
|
||||
email?: string;
|
||||
name?: string;
|
||||
groups?: string;
|
||||
role?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface AuthNProviderInfo {
|
||||
|
||||
@@ -11,41 +11,17 @@ export interface Config {
|
||||
oidcConfig?: OIDCConfig;
|
||||
}
|
||||
|
||||
export interface RoleMappingConfig {
|
||||
defaultRole?: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
useRoleAttributeDirectly?: boolean;
|
||||
groupMappings?: Array<{
|
||||
groupName: string;
|
||||
role: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SAMLAttributeMapping {
|
||||
nameAttribute?: string;
|
||||
groupsAttribute?: string;
|
||||
roleAttribute?: string;
|
||||
}
|
||||
|
||||
export interface SAMLConfig {
|
||||
samlEntity: string;
|
||||
samlIdp: string;
|
||||
samlCert: string;
|
||||
insecureSkipAuthNRequestsSigned: boolean;
|
||||
attributeMapping?: SAMLAttributeMapping;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface GoogleAuthConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectURI: string;
|
||||
insecureSkipEmailVerified?: boolean;
|
||||
fetchGroups?: boolean;
|
||||
serviceAccountJson?: string;
|
||||
domainToAdminEmail?: Record<string, string>;
|
||||
fetchTransitiveGroupMembership?: boolean;
|
||||
allowedGroups?: string[];
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface OIDCConfig {
|
||||
@@ -56,12 +32,8 @@ export interface OIDCConfig {
|
||||
claimMapping: ClaimMapping;
|
||||
insecureSkipEmailVerified: boolean;
|
||||
getUserInfo: boolean;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface ClaimMapping {
|
||||
email?: string;
|
||||
name?: string;
|
||||
groups?: string;
|
||||
role?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@@ -9,41 +9,17 @@ export interface UpdatableAuthDomain {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface RoleMappingConfig {
|
||||
defaultRole?: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
useRoleAttributeDirectly?: boolean;
|
||||
groupMappings?: Array<{
|
||||
groupName: string;
|
||||
role: 'VIEWER' | 'EDITOR' | 'ADMIN';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SAMLAttributeMapping {
|
||||
nameAttribute?: string;
|
||||
groupsAttribute?: string;
|
||||
roleAttribute?: string;
|
||||
}
|
||||
|
||||
export interface SAMLConfig {
|
||||
samlEntity: string;
|
||||
samlIdp: string;
|
||||
samlCert: string;
|
||||
insecureSkipAuthNRequestsSigned: boolean;
|
||||
attributeMapping?: SAMLAttributeMapping;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface GoogleAuthConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectURI: string;
|
||||
insecureSkipEmailVerified?: boolean;
|
||||
fetchGroups?: boolean;
|
||||
serviceAccountJson?: string;
|
||||
domainToAdminEmail?: Record<string, string>;
|
||||
fetchTransitiveGroupMembership?: boolean;
|
||||
allowedGroups?: string[];
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface OIDCConfig {
|
||||
@@ -54,12 +30,8 @@ export interface OIDCConfig {
|
||||
claimMapping: ClaimMapping;
|
||||
insecureSkipEmailVerified: boolean;
|
||||
getUserInfo: boolean;
|
||||
roleMapping?: RoleMappingConfig;
|
||||
}
|
||||
|
||||
export interface ClaimMapping {
|
||||
email?: string;
|
||||
name?: string;
|
||||
groups?: string;
|
||||
role?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
ALERT_HISTORY: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALERT_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGIN: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
FORGOT_PASSWORD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR'],
|
||||
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
@@ -5038,7 +5038,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.1.tgz#9c36d433fd264410713cc0c5ebdd75ce0ebecba3"
|
||||
integrity sha512-SdziCHg5Lwj+6oY6IRUPplaKZ+kTHjbrlhNj//UoAJ8aQLnRdR2F/miPzfSi4vrYw88LtXxNA9J9iJyacCp37A==
|
||||
|
||||
"@signozhq/icons@0.1.0", "@signozhq/icons@^0.1.0":
|
||||
"@signozhq/icons@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/icons/-/icons-0.1.0.tgz#00dfb430dbac423bfff715876f91a7b8a72509e4"
|
||||
integrity sha512-kGWDhCpQkFWaNwyWfy88AIbg902wBbgTFTBAtmo6DkHyLGoqWAf0Jcq8BX+7brFqJF9PnLoSJDj1lvCpUsI/Ig==
|
||||
|
||||
@@ -112,7 +112,7 @@ def verify_webhook_alert_expectation(
|
||||
break
|
||||
|
||||
# wait for some time before checking again
|
||||
time.sleep(10)
|
||||
time.sleep(1)
|
||||
|
||||
# We've waited but we didn't get the expected number of alerts
|
||||
|
||||
@@ -133,3 +133,15 @@ def verify_webhook_alert_expectation(
|
||||
)
|
||||
|
||||
return True # should not reach here
|
||||
|
||||
|
||||
def update_rule_channel_name(rule_data: dict, channel_name: str):
|
||||
"""
|
||||
updates the channel name in the thresholds
|
||||
so alert notification are sent to the given channel
|
||||
"""
|
||||
thresholds = rule_data["condition"]["thresholds"]
|
||||
if "kind" in thresholds and thresholds["kind"] == "basic":
|
||||
# loop over all the sepcs and update the channels
|
||||
for spec in thresholds["spec"]:
|
||||
spec["channels"] = [channel_name]
|
||||
|
||||
@@ -43,6 +43,10 @@ class MetricsTimeSeries(ABC):
|
||||
resource_attrs: dict[str, str] = {},
|
||||
scope_attrs: dict[str, str] = {},
|
||||
) -> None:
|
||||
# Create a copy of labels to avoid mutating the caller's dictionary
|
||||
labels = dict(labels)
|
||||
# Add metric_name to the labels to support promql queries
|
||||
labels["__name__"] = metric_name
|
||||
self.env = env
|
||||
self.metric_name = metric_name
|
||||
self.temporality = temporality
|
||||
|
||||
@@ -69,6 +69,10 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
"SIGNOZ_GLOBAL_INGESTION__URL": "https://ingest.test.signoz.cloud",
|
||||
"SIGNOZ_USER_PASSWORD_RESET_ALLOW__SELF": True,
|
||||
"SIGNOZ_USER_PASSWORD_RESET_MAX__TOKEN__LIFETIME": "6h",
|
||||
"RULES_EVAL_DELAY": "0s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_POLL__INTERVAL": "5s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__WAIT": "1s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__INTERVAL": "5s",
|
||||
}
|
||||
| sqlstore.env
|
||||
| clickhouse.env
|
||||
|
||||
@@ -191,3 +191,15 @@ class AlertExpectation:
|
||||
# seconds to wait for the alerts to be fired, if no
|
||||
# alerts are fired in the expected time, the test will fail
|
||||
wait_time_seconds: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AlertTestCase:
|
||||
# name of the test case
|
||||
name: str
|
||||
# path to the rule file in testdata directory
|
||||
rule_path: str
|
||||
# list of alert data that will be inserted into the database
|
||||
alert_data: List[AlertData]
|
||||
# list of alert expectations for the test case
|
||||
alert_expectation: AlertExpectation
|
||||
|
||||
@@ -8,6 +8,9 @@ from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingRespons
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def test_webhook_notification_channel(
|
||||
@@ -20,6 +23,7 @@ def test_webhook_notification_channel(
|
||||
"""
|
||||
Tests the creation and delivery of test alerts on the created notification channel
|
||||
"""
|
||||
logger.info("Setting up notification channel")
|
||||
|
||||
# Prepare notification channel name and webhook endpoint
|
||||
notification_channel_name = f"notification-channel-{uuid.uuid4()}"
|
||||
@@ -55,10 +59,10 @@ def test_webhook_notification_channel(
|
||||
)
|
||||
|
||||
# TODO: @abhishekhugetech # pylint: disable=W0511
|
||||
# Time required for Org to be registered
|
||||
# in the alertmanager, default 1m.
|
||||
# Time required for newly created Org to be registered in the alertmanager is 5 seconds in signoz.py
|
||||
# this will be fixed after [https://github.com/SigNoz/engineering-pod/issues/3800]
|
||||
time.sleep(65)
|
||||
# 10 seconds safe time for org to be registered in the alertmanager
|
||||
time.sleep(10)
|
||||
|
||||
# Call test API for the notification channel
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
669
tests/integration/src/alerts/02_basic_alert_conditions.py
Normal file
669
tests/integration/src/alerts/02_basic_alert_conditions.py
Normal file
@@ -0,0 +1,669 @@
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Callable, List
|
||||
|
||||
import pytest
|
||||
from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingResponse
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.alertutils import (
|
||||
update_rule_channel_name,
|
||||
verify_webhook_alert_expectation,
|
||||
)
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.utils import get_testdata_file_path
|
||||
|
||||
# test cases for match type and compare operators have wait time of 30 seconds to verify the alert expectation.
|
||||
# we've poistioned the alert data to fire the alert on first eval of rule manager, the eval frequency
|
||||
# for most alert rules are set of 15s so considering this delay plus some delay from alert manager's
|
||||
# group_wait and group_interval, even in worst case most alerts should be triggered in about 30 seconds
|
||||
TEST_RULES_MATCH_TYPE_AND_COMPARE_OPERATORS = [
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_above_at_least_once",
|
||||
rule_path="alerts/test_scenarios/threshold_above_at_least_once/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_above_at_least_once/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_above_at_least_once",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_above_all_the_time",
|
||||
rule_path="alerts/test_scenarios/threshold_above_all_the_time/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_above_all_the_time/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_above_all_the_time",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_above_in_total",
|
||||
rule_path="alerts/test_scenarios/threshold_above_in_total/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_above_in_total/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_above_in_total",
|
||||
"threshold.name": "critical",
|
||||
"service": "server",
|
||||
},
|
||||
),
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_above_in_total",
|
||||
"threshold.name": "critical",
|
||||
"service": "api",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_above_average",
|
||||
rule_path="alerts/test_scenarios/threshold_above_average/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="traces",
|
||||
data_path="alerts/test_scenarios/threshold_above_average/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_above_average",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
# TODO: @abhishekhugetech enable the test for matchType last, pylint: disable=W0511
|
||||
# after the [issue](https://github.com/SigNoz/engineering-pod/issues/3801) with matchType last is fixed
|
||||
# types.AlertTestCase(
|
||||
# name="test_threshold_above_last",
|
||||
# rule_path="alerts/test_scenarios/threshold_above_last/rule.json",
|
||||
# alert_data=[
|
||||
# types.AlertData(
|
||||
# type="metrics",
|
||||
# data_path="alerts/test_scenarios/threshold_above_last/alert_data.jsonl",
|
||||
# ),
|
||||
# ],
|
||||
# alert_expectation=types.AlertExpectation(
|
||||
# should_alert=True,
|
||||
# wait_time_seconds=30,
|
||||
# expected_alerts=[
|
||||
# types.FiringAlert(
|
||||
# labels={
|
||||
# "alertname": "threshold_above_last",
|
||||
# "threshold.name": "critical",
|
||||
# }
|
||||
# ),
|
||||
# ],
|
||||
# ),
|
||||
# ),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_below_at_least_once",
|
||||
rule_path="alerts/test_scenarios/threshold_below_at_least_once/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="logs",
|
||||
data_path="alerts/test_scenarios/threshold_below_at_least_once/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_below_at_least_once",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_below_all_the_time",
|
||||
rule_path="alerts/test_scenarios/threshold_below_all_the_time/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="logs",
|
||||
data_path="alerts/test_scenarios/threshold_below_all_the_time/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_below_all_the_time",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_below_in_total",
|
||||
rule_path="alerts/test_scenarios/threshold_below_in_total/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_below_in_total/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_below_in_total",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_below_average",
|
||||
rule_path="alerts/test_scenarios/threshold_below_average/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_below_average/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_below_average",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
# TODO: @abhishekhugetech enable the test for matchType last,
|
||||
# after the [issue](https://github.com/SigNoz/engineering-pod/issues/3801) with matchType last is fixed, pylint: disable=W0511
|
||||
# types.AlertTestCase(
|
||||
# name="test_threshold_below_last",
|
||||
# rule_path="alerts/test_scenarios/threshold_below_last/rule.json",
|
||||
# alert_data=[
|
||||
# types.AlertData(
|
||||
# type="metrics",
|
||||
# data_path="alerts/test_scenarios/threshold_below_last/alert_data.jsonl",
|
||||
# ),
|
||||
# ],
|
||||
# alert_expectation=types.AlertExpectation(
|
||||
# should_alert=True,
|
||||
# wait_time_seconds=30,
|
||||
# expected_alerts=[
|
||||
# types.FiringAlert(
|
||||
# labels={
|
||||
# "alertname": "threshold_below_last",
|
||||
# "threshold.name": "critical",
|
||||
# }
|
||||
# ),
|
||||
# ],
|
||||
# ),
|
||||
# ),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_equal_to_at_least_once",
|
||||
rule_path="alerts/test_scenarios/threshold_equal_to_at_least_once/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_equal_to_at_least_once/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_equal_to_at_least_once",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_equal_to_all_the_time",
|
||||
rule_path="alerts/test_scenarios/threshold_equal_to_all_the_time/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_equal_to_all_the_time/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_equal_to_all_the_time",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_equal_to_in_total",
|
||||
rule_path="alerts/test_scenarios/threshold_equal_to_in_total/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_equal_to_in_total/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_equal_to_in_total",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_equal_to_average",
|
||||
rule_path="alerts/test_scenarios/threshold_equal_to_average/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_equal_to_average/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_equal_to_average",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
# TODO: @abhishekhugetech enable the test for matchType last,
|
||||
# after the [issue](https://github.com/SigNoz/engineering-pod/issues/3801) with matchType last is fixed, pylint: disable=W0511
|
||||
# types.AlertTestCase(
|
||||
# name="test_threshold_equal_to_last",
|
||||
# rule_path="alerts/test_scenarios/threshold_equal_to_last/rule.json",
|
||||
# alert_data=[
|
||||
# types.AlertData(
|
||||
# type="metrics",
|
||||
# data_path="alerts/test_scenarios/threshold_equal_to_last/alert_data.jsonl",
|
||||
# ),
|
||||
# ],
|
||||
# alert_expectation=types.AlertExpectation(
|
||||
# should_alert=True,
|
||||
# wait_time_seconds=30,
|
||||
# expected_alerts=[
|
||||
# types.FiringAlert(
|
||||
# labels={
|
||||
# "alertname": "threshold_equal_to_last",
|
||||
# "threshold.name": "critical",
|
||||
# }
|
||||
# ),
|
||||
# ],
|
||||
# ),
|
||||
# ),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_not_equal_to_at_least_once",
|
||||
rule_path="alerts/test_scenarios/threshold_not_equal_to_at_least_once/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_not_equal_to_at_least_once/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_not_equal_to_at_least_once",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_not_equal_to_all_the_time",
|
||||
rule_path="alerts/test_scenarios/threshold_not_equal_to_all_the_time/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_not_equal_to_all_the_time/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_not_equal_to_all_the_time",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_not_equal_to_in_total",
|
||||
rule_path="alerts/test_scenarios/threshold_not_equal_to_in_total/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_not_equal_to_in_total/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_not_equal_to_in_total",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_threshold_not_equal_to_average",
|
||||
rule_path="alerts/test_scenarios/threshold_not_equal_to_average/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/threshold_not_equal_to_average/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "threshold_not_equal_to_average",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
# TODO: @abhishekhugetech enable the test for matchType last,
|
||||
# after the [issue](https://github.com/SigNoz/engineering-pod/issues/3801) with matchType last is fixed, pylint: disable=W0511
|
||||
# types.AlertTestCase(
|
||||
# name="test_threshold_not_equal_to_last",
|
||||
# rule_path="alerts/test_scenarios/threshold_not_equal_to_last/rule.json",
|
||||
# alert_data=[
|
||||
# types.AlertData(
|
||||
# type="metrics",
|
||||
# data_path="alerts/test_scenarios/threshold_not_equal_to_last/alert_data.jsonl",
|
||||
# ),
|
||||
# ],
|
||||
# alert_expectation=types.AlertExpectation(
|
||||
# should_alert=True,
|
||||
# wait_time_seconds=30,
|
||||
# expected_alerts=[
|
||||
# types.FiringAlert(
|
||||
# labels={
|
||||
# "alertname": "threshold_not_equal_to_last",
|
||||
# "threshold.name": "critical",
|
||||
# }
|
||||
# ),
|
||||
# ],
|
||||
# ),
|
||||
# ),
|
||||
]
|
||||
|
||||
# test cases unit conversion
|
||||
TEST_RULES_UNIT_CONVERSION = [
|
||||
types.AlertTestCase(
|
||||
name="test_unit_conversion_bytes_to_mb",
|
||||
rule_path="alerts/test_scenarios/unit_conversion_bytes_to_mb/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/unit_conversion_bytes_to_mb/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "unit_conversion_bytes_to_mb",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_unit_conversion_ms_to_second",
|
||||
rule_path="alerts/test_scenarios/unit_conversion_ms_to_second/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/unit_conversion_ms_to_second/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "unit_conversion_ms_to_second",
|
||||
"threshold.name": "critical",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
# test cases miscellaneous cases, no data and multi threshold
|
||||
TEST_RULES_MISCELLANEOUS = [
|
||||
types.AlertTestCase(
|
||||
name="test_no_data_rule_test",
|
||||
rule_path="alerts/test_scenarios/no_data_rule_test/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/no_data_rule_test/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
wait_time_seconds=30,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "[No data] no_data_rule_test",
|
||||
"nodata": "true",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertTestCase(
|
||||
name="test_multi_threshold_rule_test",
|
||||
rule_path="alerts/test_scenarios/multi_threshold_rule_test/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="metrics",
|
||||
data_path="alerts/test_scenarios/multi_threshold_rule_test/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
alert_expectation=types.AlertExpectation(
|
||||
should_alert=True,
|
||||
# the second alert will be fired with some delay from alert manager's group_interval
|
||||
# so taking this in consideration, the wait time is 90 seconds (30s + 30s for next alert + 30s buffer)
|
||||
wait_time_seconds=90,
|
||||
expected_alerts=[
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "multi_threshold_rule_test",
|
||||
"threshold.name": "info",
|
||||
}
|
||||
),
|
||||
types.FiringAlert(
|
||||
labels={
|
||||
"alertname": "multi_threshold_rule_test",
|
||||
"threshold.name": "warning",
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alert_test_case",
|
||||
TEST_RULES_MATCH_TYPE_AND_COMPARE_OPERATORS
|
||||
+ TEST_RULES_UNIT_CONVERSION
|
||||
+ TEST_RULES_MISCELLANEOUS,
|
||||
ids=lambda alert_test_case: alert_test_case.name,
|
||||
)
|
||||
def test_basic_alert_rule_conditions(
|
||||
# Notification channel related fixtures
|
||||
notification_channel: types.TestContainerDocker,
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, List[Mapping]], None],
|
||||
create_webhook_notification_channel: Callable[[str, str, dict, bool], str],
|
||||
# Alert rule related fixtures
|
||||
create_alert_rule: Callable[[dict], str],
|
||||
# Alert data insertion related fixtures
|
||||
insert_alert_data: Callable[[List[types.AlertData], datetime], None],
|
||||
alert_test_case: types.AlertTestCase,
|
||||
):
|
||||
# Prepare notification channel name and webhook endpoint
|
||||
notification_channel_name = str(uuid.uuid4())
|
||||
webhook_endpoint_path = f"/alert/{notification_channel_name}"
|
||||
notification_url = notification_channel.container_configs["8080"].get(
|
||||
webhook_endpoint_path
|
||||
)
|
||||
|
||||
logger.info("notification_url: %s", {"notification_url": notification_url})
|
||||
|
||||
# register the mock endpoint in notification channel
|
||||
make_http_mocks(
|
||||
notification_channel,
|
||||
[
|
||||
Mapping(
|
||||
request=MappingRequest(
|
||||
method=HttpMethods.POST,
|
||||
url=webhook_endpoint_path,
|
||||
),
|
||||
response=MappingResponse(
|
||||
status=200,
|
||||
json_body={},
|
||||
),
|
||||
persistent=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create an alert channel using the given route
|
||||
create_webhook_notification_channel(
|
||||
channel_name=notification_channel_name,
|
||||
webhook_url=notification_url,
|
||||
http_config={},
|
||||
send_resolved=False,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"alert channel created with name: %s",
|
||||
{"notification_channel_name": notification_channel_name},
|
||||
)
|
||||
|
||||
# Insert alert data
|
||||
insert_alert_data(
|
||||
alert_test_case.alert_data,
|
||||
base_time=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
|
||||
)
|
||||
|
||||
# Create Alert Rule
|
||||
rule_path = get_testdata_file_path(alert_test_case.rule_path)
|
||||
with open(rule_path, "r", encoding="utf-8") as f:
|
||||
rule_data = json.loads(f.read())
|
||||
# Update the channel name in the rule data
|
||||
update_rule_channel_name(rule_data, notification_channel_name)
|
||||
rule_id = create_alert_rule(rule_data)
|
||||
logger.info(
|
||||
"rule created with id: %s",
|
||||
{"rule_id": rule_id, "rule_name": rule_data["alert"]},
|
||||
)
|
||||
|
||||
# Verify alert expectation
|
||||
verify_webhook_alert_expectation(
|
||||
notification_channel,
|
||||
notification_channel_name,
|
||||
alert_test_case.alert_expectation,
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import sql
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_managed_roles_create_on_register(
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
|
||||
# get the list of all roles.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
@@ -32,18 +32,22 @@ def test_managed_roles_create_on_register(
|
||||
# since this check happens immediately post registeration, all the managed roles should be present.
|
||||
assert len(data) == 4
|
||||
role_names = {role["name"] for role in data}
|
||||
expected_names = {"signoz-admin", "signoz-viewer", "signoz-editor", "signoz-anonymous"}
|
||||
expected_names = {
|
||||
"signoz-admin",
|
||||
"signoz-viewer",
|
||||
"signoz-editor",
|
||||
"signoz-anonymous",
|
||||
}
|
||||
# do the set mapping as this is order insensitive, direct list match is order-sensitive.
|
||||
assert set(role_names) == expected_names
|
||||
|
||||
|
||||
|
||||
def test_root_user_signoz_admin_assignment(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Get the user from the /user/me endpoint and extract the id
|
||||
@@ -64,14 +68,16 @@ def test_root_user_signoz_admin_assignment(
|
||||
# this validates to some extent that the role assignment is complete under the assumption that middleware is functioning as expected.
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
|
||||
# Loop over the roles and get the org_id and id for signoz-admin role
|
||||
roles = response.json()["data"]
|
||||
admin_role_entry = next((role for role in roles if role["name"] == "signoz-admin"), None)
|
||||
admin_role_entry = next(
|
||||
(role for role in roles if role["name"] == "signoz-admin"), None
|
||||
)
|
||||
assert admin_role_entry is not None
|
||||
org_id = admin_role_entry["orgId"]
|
||||
|
||||
# to be super sure of authorization server, let's validate the tuples in DB as well.
|
||||
# to be super sure of authorization server, let's validate the tuples in DB as well.
|
||||
# todo[@vikrantgupta25]: replace this with role memebers handler once built.
|
||||
with signoz.sqlstore.conn.connect() as conn:
|
||||
# verify the entry present for role assignment
|
||||
@@ -80,15 +86,14 @@ def test_root_user_signoz_admin_assignment(
|
||||
sql.text("SELECT * FROM tuple WHERE object_id = :object_id"),
|
||||
{"object_id": tuple_object_id},
|
||||
)
|
||||
|
||||
|
||||
tuple_row = tuple_result.mappings().fetchone()
|
||||
assert tuple_row is not None
|
||||
# check that the tuple if for role assignment
|
||||
assert tuple_row['object_type'] == "role"
|
||||
assert tuple_row['relation'] == "assignee"
|
||||
assert tuple_row["object_type"] == "role"
|
||||
assert tuple_row["relation"] == "assignee"
|
||||
|
||||
|
||||
if request.config.getoption("--sqlstore-provider") == 'sqlite':
|
||||
if request.config.getoption("--sqlstore-provider") == "sqlite":
|
||||
user_object_id = f"organization/{org_id}/user/{user_id}"
|
||||
assert tuple_row["user_object_type"] == "user"
|
||||
assert tuple_row["user_object_id"] == user_object_id
|
||||
@@ -148,4 +153,4 @@ def test_anonymous_user_signoz_anonymous_assignment(
|
||||
assert tuple_row["user_type"] == "user"
|
||||
assert tuple_row["_user"] == _user
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import pytest
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import sql
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, USER_EDITOR_EMAIL, USER_EDITOR_PASSWORD
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
USER_EDITOR_EMAIL,
|
||||
USER_EDITOR_PASSWORD,
|
||||
)
|
||||
from fixtures.types import Operation, SigNoz
|
||||
|
||||
|
||||
@@ -16,7 +21,7 @@ def test_user_invite_accept_role_grant(
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
|
||||
# invite a user as editor
|
||||
invite_payload = {
|
||||
"email": USER_EDITOR_EMAIL,
|
||||
@@ -30,7 +35,7 @@ def test_user_invite_accept_role_grant(
|
||||
)
|
||||
assert invite_response.status_code == HTTPStatus.CREATED
|
||||
invite_token = invite_response.json()["data"]["token"]
|
||||
|
||||
|
||||
# accept the invite for editor
|
||||
accept_payload = {
|
||||
"token": invite_token,
|
||||
@@ -40,7 +45,7 @@ def test_user_invite_accept_role_grant(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||
json=accept_payload,
|
||||
timeout=2,
|
||||
)
|
||||
)
|
||||
assert accept_response.status_code == HTTPStatus.CREATED
|
||||
|
||||
# Login with editor email and password
|
||||
@@ -53,7 +58,6 @@ def test_user_invite_accept_role_grant(
|
||||
assert user_me_response.status_code == HTTPStatus.OK
|
||||
editor_id = user_me_response.json()["data"]["id"]
|
||||
|
||||
|
||||
# check the forbidden response for admin api for editor user
|
||||
admin_roles_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
@@ -79,11 +83,11 @@ def test_user_invite_accept_role_grant(
|
||||
)
|
||||
tuple_row = tuple_result.mappings().fetchone()
|
||||
assert tuple_row is not None
|
||||
assert tuple_row['object_type'] == "role"
|
||||
assert tuple_row['relation'] == "assignee"
|
||||
assert tuple_row["object_type"] == "role"
|
||||
assert tuple_row["relation"] == "assignee"
|
||||
|
||||
# verify the user tuple details depending on db provider
|
||||
if request.config.getoption("--sqlstore-provider") == 'sqlite':
|
||||
if request.config.getoption("--sqlstore-provider") == "sqlite":
|
||||
user_object_id = f"organization/{org_id}/user/{editor_id}"
|
||||
assert tuple_row["user_object_type"] == "user"
|
||||
assert tuple_row["user_object_id"] == user_object_id
|
||||
@@ -93,7 +97,6 @@ def test_user_invite_accept_role_grant(
|
||||
assert tuple_row["_user"] == _user
|
||||
|
||||
|
||||
|
||||
def test_user_update_role_grant(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
@@ -122,9 +125,7 @@ def test_user_update_role_grant(
|
||||
org_id = roles_data[0]["orgId"]
|
||||
|
||||
# Update the user's role to viewer
|
||||
update_payload = {
|
||||
"role": "VIEWER"
|
||||
}
|
||||
update_payload = {"role": "VIEWER"}
|
||||
update_response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{editor_id}"),
|
||||
json=update_payload,
|
||||
@@ -139,7 +140,9 @@ def test_user_update_role_grant(
|
||||
viewer_tuple_object_id = f"organization/{org_id}/role/signoz-viewer"
|
||||
# Check there is no tuple for signoz-editor assignment
|
||||
editor_tuple_result = conn.execute(
|
||||
sql.text("SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"),
|
||||
sql.text(
|
||||
"SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"
|
||||
),
|
||||
{"object_id": editor_tuple_object_id},
|
||||
)
|
||||
for row in editor_tuple_result.mappings().fetchall():
|
||||
@@ -152,13 +155,15 @@ def test_user_update_role_grant(
|
||||
|
||||
# Check that a tuple exists for signoz-viewer assignment
|
||||
viewer_tuple_result = conn.execute(
|
||||
sql.text("SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"),
|
||||
sql.text(
|
||||
"SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"
|
||||
),
|
||||
{"object_id": viewer_tuple_object_id},
|
||||
)
|
||||
row = viewer_tuple_result.mappings().fetchone()
|
||||
assert row is not None
|
||||
assert row['object_type'] == "role"
|
||||
assert row['relation'] == "assignee"
|
||||
assert row["object_type"] == "role"
|
||||
assert row["relation"] == "assignee"
|
||||
if request.config.getoption("--sqlstore-provider") == "sqlite":
|
||||
user_object_id = f"organization/{org_id}/user/{editor_id}"
|
||||
assert row["user_object_type"] == "user"
|
||||
@@ -168,6 +173,7 @@ def test_user_update_role_grant(
|
||||
assert row["user_type"] == "user"
|
||||
assert row["_user"] == _user
|
||||
|
||||
|
||||
def test_user_delete_role_revoke(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
@@ -205,10 +211,12 @@ def test_user_delete_role_revoke(
|
||||
|
||||
with signoz.sqlstore.conn.connect() as conn:
|
||||
tuple_result = conn.execute(
|
||||
sql.text("SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"),
|
||||
sql.text(
|
||||
"SELECT * FROM tuple WHERE object_id = :object_id AND relation = 'assignee'"
|
||||
),
|
||||
{"object_id": tuple_object_id},
|
||||
)
|
||||
|
||||
|
||||
# there should NOT be any tuple for the current user assignment
|
||||
tuple_rows = tuple_result.mappings().fetchall()
|
||||
for row in tuple_rows:
|
||||
@@ -217,4 +225,4 @@ def test_user_delete_role_revoke(
|
||||
assert row["user_object_id"] != user_object_id
|
||||
else:
|
||||
_user = f"user:organization/{org_id}/user/{editor_id}"
|
||||
assert row["_user"] != _user
|
||||
assert row["_user"] != _user
|
||||
|
||||
12
tests/integration/testdata/alerts/test_scenarios/multi_threshold_rule_test/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/multi_threshold_rule_test/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":14,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":3,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":25,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":25,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":25,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":12,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":8,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_multi_threshold_rule_test","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
83
tests/integration/testdata/alerts/test_scenarios/multi_threshold_rule_test/rule.json
vendored
Normal file
83
tests/integration/testdata/alerts/test_scenarios/multi_threshold_rule_test/rule.json
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"alert": "multi_threshold_rule_test",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 30,
|
||||
"matchType": "1",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "warning",
|
||||
"target": 20,
|
||||
"matchType": "1",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"target": 10,
|
||||
"matchType": "1",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_multi_threshold_rule_test",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/no_data_rule_test/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/no_data_rule_test/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:01:00+00:00","value":12,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:02:00+00:00","value":26,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:03:00+00:00","value":41,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:04:00+00:00","value":56,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:05:00+00:00","value":71,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:06:00+00:00","value":86,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:07:00+00:00","value":101,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:08:00+00:00","value":116,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:09:00+00:00","value":131,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:10:00+00:00","value":146,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:11:00+00:00","value":161,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_no_data_rule_test","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:12:00+00:00","value":176,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
70
tests/integration/testdata/alerts/test_scenarios/no_data_rule_test/rule.json
vendored
Normal file
70
tests/integration/testdata/alerts/test_scenarios/no_data_rule_test/rule.json
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"alert": "no_data_rule_test",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "1",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"filter": {
|
||||
"expression": "service = 'server'"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "request_total_no_data_rule_test",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"alertOnAbsent": true,
|
||||
"absentFor": 1
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_all_the_time/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_all_the_time/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":12,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":14,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_above_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
58
tests/integration/testdata/alerts/test_scenarios/threshold_above_all_the_time/rule.json
vendored
Normal file
58
tests/integration/testdata/alerts/test_scenarios/threshold_above_all_the_time/rule.json
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"alert": "threshold_above_all_the_time",
|
||||
"ruleType": "promql_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "2",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "promql",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "promql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "{\"cpu_percent_threshold_above_all_the_time\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_at_least_once/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_at_least_once/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:01:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:02:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:03:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:04:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:05:00+00:00","value":19,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:06:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:07:00+00:00","value":35,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:08:00+00:00","value":36,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:09:00+00:00","value":37,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:10:00+00:00","value":38,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:11:00+00:00","value":39,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:12:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
58
tests/integration/testdata/alerts/test_scenarios/threshold_above_at_least_once/rule.json
vendored
Normal file
58
tests/integration/testdata/alerts/test_scenarios/threshold_above_at_least_once/rule.json
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"alert": "threshold_above_at_least_once",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "1",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "clickhouse_sql",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "WITH __temporal_aggregation_cte AS (\n SELECT \n fingerprint, \n toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts, \n avg(value) AS per_series_value \n FROM signoz_metrics.distributed_samples_v4 AS points \n INNER JOIN (\n SELECT fingerprint \n FROM signoz_metrics.time_series_v4 \n WHERE metric_name IN ('request_total_threshold_above_at_least_once') \n AND LOWER(temporality) LIKE LOWER('cumulative') \n AND __normalized = false \n GROUP BY fingerprint\n ) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint \n WHERE metric_name IN ('request_total_threshold_above_at_least_once') \n AND unix_milli >= {{.start_timestamp_ms}} \n AND unix_milli < {{.end_timestamp_ms}} \n GROUP BY fingerprint, ts \n ORDER BY fingerprint, ts\n), \n__spatial_aggregation_cte AS (\n SELECT \n ts, \n avg(per_series_value) AS value \n FROM __temporal_aggregation_cte \n WHERE isNaN(per_series_value) = 0 \n GROUP BY ts\n) \nSELECT * FROM __spatial_aggregation_cte \nORDER BY ts"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
20
tests/integration/testdata/alerts/test_scenarios/threshold_above_average/alert_data.jsonl
vendored
Normal file
20
tests/integration/testdata/alerts/test_scenarios/threshold_above_average/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{ "timestamp": "2026-01-29T10:00:00.000000Z", "duration": "PT0.8S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a1", "span_id": "a1b2c3d4e5f6g7h8", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:00:30.000000Z", "duration": "PT1.2S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a2", "span_id": "a2b3c4d5e6f7g8h9", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:01:00.000000Z", "duration": "PT0.9S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a3", "span_id": "a3b4c5d6e7f8g9h0", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:01:30.000000Z", "duration": "PT1.5S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a4", "span_id": "a4b5c6d7e8f9g0h1", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:02:00.000000Z", "duration": "PT1.1S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a5", "span_id": "a5b6c7d8e9f0g1h2", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:02:30.000000Z", "duration": "PT0.7S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a6", "span_id": "a6b7c8d9e0f1g2h3", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:03:00.000000Z", "duration": "PT1.8S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a7", "span_id": "a7b8c9d0e1f2g3h4", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:03:30.000000Z", "duration": "PT1.3S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a8", "span_id": "a8b9c0d1e2f3g4h5", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:04:00.000000Z", "duration": "PT0.6S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6a9", "span_id": "a9b0c1d2e3f4g5h6", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:04:30.000000Z", "duration": "PT1.4S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b1", "span_id": "b1c2d3e4f5g6h7i8", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:05:00.000000Z", "duration": "PT1.6S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b2", "span_id": "b2c3d4e5f6g7h8i9", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" },"attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:05:30.000000Z", "duration": "PT0.85S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b3", "span_id": "b3c4d5e6f7g8h9i0", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:06:00.000000Z", "duration": "PT1.7S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b4", "span_id": "b4c5d6e7f8g9h0i1", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:06:30.000000Z", "duration": "PT1.25S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b5", "span_id": "b5c6d7e8f9g0h1i2", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:07:00.000000Z", "duration": "PT0.95S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b6", "span_id": "b6c7d8e9f0g1h2i3", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:07:30.000000Z", "duration": "PT1.9S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b7", "span_id": "b7c8d9e0f1g2h3i4", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:08:00.000000Z", "duration": "PT1.35S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b8", "span_id": "b8c9d0e1f2g3h4i5", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:08:30.000000Z", "duration": "PT0.75S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6b9", "span_id": "b9c0d1e2f3g4h5i6", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:09:00.000000Z", "duration": "PT1.55S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6c1", "span_id": "c1d2e3f4g5h6i7j8", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
{ "timestamp": "2026-01-29T10:10:00.000000Z", "duration": "PT1.65S", "trace_id": "491f6d3d6b0a1f9e8a71b2c3d4e5f6c2", "span_id": "c2d3e4f5g6h7i8j9", "parent_span_id": "", "name": "POST /order", "kind": 2, "status_code": 1, "status_message": "", "resources": { "deployment.environment": "production", "service.name": "order-service", "os.type": "linux", "host.name": "linux-000" }, "attributes": { "net.transport": "IP.TCP", "http.scheme": "http", "http.user_agent": "Integration Test", "http.request.method": "POST", "http.response.status_code": "200", "http.request.path": "/order" } }
|
||||
68
tests/integration/testdata/alerts/test_scenarios/threshold_above_average/rule.json
vendored
Normal file
68
tests/integration/testdata/alerts/test_scenarios/threshold_above_average/rule.json
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"alert": "threshold_above_average",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "TRACES_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 1,
|
||||
"matchType": "3",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
],
|
||||
"targetUnit": "s"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"unit": "ns",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"filter": {
|
||||
"expression": "http.request.path = '/order'"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "p90(duration_nano)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
24
tests/integration/testdata/alerts/test_scenarios/threshold_above_in_total/alert_data.jsonl
vendored
Normal file
24
tests/integration/testdata/alerts/test_scenarios/threshold_above_in_total/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:01:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:01:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:02:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:02:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:03:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:03:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:04:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:04:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:05:00+00:00","value":305,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:05:00+00:00","value":305,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:06:00+00:00","value":306,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:06:00+00:00","value":306,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:07:00+00:00","value":307,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:07:00+00:00","value":307,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:08:00+00:00","value":308,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:08:00+00:00","value":308,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:09:00+00:00","value":309,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:09:00+00:00","value":309,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:10:00+00:00","value":310,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:10:00+00:00","value":310,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:11:00+00:00","value":311,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:11:00+00:00","value":311,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:12:00+00:00","value":312,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_above_in_total","labels":{"service":"server","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:12:00+00:00","value":312,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
74
tests/integration/testdata/alerts/test_scenarios/threshold_above_in_total/rule.json
vendored
Normal file
74
tests/integration/testdata/alerts/test_scenarios/threshold_above_in_total/rule.json
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"alert": "threshold_above_in_total",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 5,
|
||||
"matchType": "4",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service",
|
||||
"fieldDataType": "",
|
||||
"fieldContext": ""
|
||||
}
|
||||
],
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "request_total_threshold_above_in_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [
|
||||
"service"
|
||||
],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_last/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_above_last/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:01:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:03:00+00:00","value":15,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:04:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:05:00+00:00","value":31,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:06:00+00:00","value":46,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:07:00+00:00","value":58,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:08:00+00:00","value":71,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:09:00+00:00","value":76,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:10:00+00:00","value":81,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:11:00+00:00","value":86,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_threshold_above_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:12:00+00:00","value":91,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_above_last/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_above_last/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_above_last",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "5",
|
||||
"op": "1",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "disk_usage_threshold_above_last",
|
||||
"timeAggregation": "latest",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
18
tests/integration/testdata/alerts/test_scenarios/threshold_below_all_the_time/alert_data.jsonl
vendored
Normal file
18
tests/integration/testdata/alerts/test_scenarios/threshold_below_all_the_time/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{ "timestamp": "2026-01-29T10:00:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:00:02.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Database connection established", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "API request received", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Request validation completed", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:04:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Cache updated successfully", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:04:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:05:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Rate limit check passed", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:05:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:06:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Authentication token validated", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:06:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:07:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Query executed successfully", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:08:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Response sent to client", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:09:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Metrics collected", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:10:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Background job started", "severity_text": "INFO" }
|
||||
66
tests/integration/testdata/alerts/test_scenarios/threshold_below_all_the_time/rule.json
vendored
Normal file
66
tests/integration/testdata/alerts/test_scenarios/threshold_below_all_the_time/rule.json
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"alert": "threshold_below_all_the_time",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "2",
|
||||
"op": "2",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"filter": {
|
||||
"expression": "body CONTAINS 'payment success'"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
20
tests/integration/testdata/alerts/test_scenarios/threshold_below_at_least_once/alert_data.jsonl
vendored
Normal file
20
tests/integration/testdata/alerts/test_scenarios/threshold_below_at_least_once/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{ "timestamp": "2026-01-29T10:00:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "User login successful", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:00:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Database connection established", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "API request received", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:04:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:04:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Cache updated successfully", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:05:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:05:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Request validation completed", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:06:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:06:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Authentication token validated", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:07:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:07:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Query executed successfully", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:08:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Response sent to client", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:08:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Health check endpoint called", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:09:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Metrics collected", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:10:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "Background job started", "severity_text": "INFO" }
|
||||
66
tests/integration/testdata/alerts/test_scenarios/threshold_below_at_least_once/rule.json
vendored
Normal file
66
tests/integration/testdata/alerts/test_scenarios/threshold_below_at_least_once/rule.json
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"alert": "threshold_below_at_least_once",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "1",
|
||||
"op": "2",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"filter": {
|
||||
"expression": "body CONTAINS 'payment success'"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_average/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_average/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_below_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_average/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_average/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_below_average",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "3",
|
||||
"op": "2",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_below_average",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_in_total/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_in_total/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:01:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:02:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:03:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:04:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:05:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:06:00+00:00","value":301,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:07:00+00:00","value":302,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:08:00+00:00","value":303,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:09:00+00:00","value":304,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:10:00+00:00","value":305,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:11:00+00:00","value":306,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_below_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:12:00+00:00","value":307,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_in_total/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_in_total/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_below_in_total",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "4",
|
||||
"op": "2",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "bytes_per_second_threshold_below_in_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_last/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_below_last/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:01:00+00:00","value":100,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:02:00+00:00","value":95,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:03:00+00:00","value":90,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:04:00+00:00","value":85,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:05:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:06:00+00:00","value":3,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:07:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:08:00+00:00","value":78,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:09:00+00:00","value":72,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:10:00+00:00","value":80,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:11:00+00:00","value":76,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_left_threshold_below_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:12:00+00:00","value":82,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_last/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_below_last/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_below_last",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "5",
|
||||
"op": "2",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "disk_usage_mb_left_threshold_below_last",
|
||||
"timeAggregation": "latest",
|
||||
"spaceAggregation": "min"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_all_the_time/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_all_the_time/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_equal_to_all_the_time",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "2",
|
||||
"op": "3",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_equal_to_all_the_time",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":15,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":8,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":12,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":7,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":6,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":13,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":8,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_at_least_once","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":14,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_at_least_once/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_at_least_once/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_equal_to_at_least_once",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "1",
|
||||
"op": "3",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_equal_to_at_least_once",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_average/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_average/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_average/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_average/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_equal_to_average",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "3",
|
||||
"op": "3",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_equal_to_average",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_in_total/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_in_total/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:01:00+00:00","value":0,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:02:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:03:00+00:00","value":120,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:04:00+00:00","value":180,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:05:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:06:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:07:00+00:00","value":720,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:08:00+00:00","value":780,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:09:00+00:00","value":840,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:10:00+00:00","value":900,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:11:00+00:00","value":960,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:12:00+00:00","value":1020,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_in_total/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_in_total/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_equal_to_in_total",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "4",
|
||||
"op": "3",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "bytes_per_second_threshold_equal_to_in_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_last/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_last/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:01:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:03:00+00:00","value":15,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:04:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:05:00+00:00","value":30,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:06:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:07:00+00:00","value":50,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:08:00+00:00","value":55,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:09:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:10:00+00:00","value":65,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:11:00+00:00","value":70,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_mb_threshold_equal_to_last","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:12:00+00:00","value":75,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_last/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_equal_to_last/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_equal_to_last",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "5",
|
||||
"op": "3",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "disk_usage_mb_threshold_equal_to_last",
|
||||
"timeAggregation": "latest",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":2,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_all_the_time","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":5,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_all_the_time/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_all_the_time/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_not_equal_to_all_the_time",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "2",
|
||||
"op": "4",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_not_equal_to_all_the_time",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:01:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:02:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:03:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:04:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:05:00+00:00","value":430,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:06:00+00:00","value":450,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:07:00+00:00","value":500,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:08:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:09:00+00:00","value":700,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:10:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:11:00+00:00","value":900,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"request_total_threshold_not_equal_to_at_least_once","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2026-01-29T10:12:00+00:00","value":1000,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_at_least_once/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_at_least_once/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_not_equal_to_at_least_once",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 5,
|
||||
"matchType": "1",
|
||||
"op": "4",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "request_total_threshold_not_equal_to_at_least_once",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_average/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_average/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:01:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:03:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:04:00+00:00","value":3,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:05:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:06:00+00:00","value":16,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:07:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:08:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:09:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:10:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:11:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"cpu_percent_threshold_not_equal_to_average","labels":{"host":"server-01","cpu":"cpu0"},"timestamp":"2026-01-29T10:12:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_average/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_average/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_not_equal_to_average",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "3",
|
||||
"op": "4",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cpu_percent_threshold_not_equal_to_average",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:01:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:02:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:03:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:04:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:05:00+00:00","value":605,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:06:00+00:00","value":606,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:07:00+00:00","value":607,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:08:00+00:00","value":608,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:09:00+00:00","value":609,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:10:00+00:00","value":610,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:11:00+00:00","value":611,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"bytes_per_second_threshold_not_equal_to_in_total","labels":{"interface":"eth0","protocol":"tcp"},"timestamp":"2026-01-29T10:12:00+00:00","value":612,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_in_total/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_in_total/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_not_equal_to_in_total",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "4",
|
||||
"op": "4",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "bytes_per_second_threshold_not_equal_to_in_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_last/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_last/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:01:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:02:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:03:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:04:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:05:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:06:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:07:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:08:00+00:00","value":10,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:09:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:10:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:11:00+00:00","value":9,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"seats_left_threshold_not_equal_to_last","labels":{"venue":"conference_hall","section":"main"},"timestamp":"2026-01-29T10:12:00+00:00","value":11,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_last/rule.json
vendored
Normal file
65
tests/integration/testdata/alerts/test_scenarios/threshold_not_equal_to_last/rule.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"alert": "threshold_not_equal_to_last",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 10,
|
||||
"matchType": "5",
|
||||
"op": "4",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "seats_left_threshold_not_equal_to_last",
|
||||
"timeAggregation": "latest",
|
||||
"spaceAggregation": "min"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
12
tests/integration/testdata/alerts/test_scenarios/unit_conversion_bytes_to_mb/alert_data.jsonl
vendored
Normal file
12
tests/integration/testdata/alerts/test_scenarios/unit_conversion_bytes_to_mb/alert_data.jsonl
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:01:00+00:00","value":524288,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:02:00+00:00","value":1048576,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:03:00+00:00","value":1572864,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:04:00+00:00","value":2097152,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:05:00+00:00","value":3770016,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:06:00+00:00","value":5642880,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:07:00+00:00","value":10515744,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:08:00+00:00","value":11038632,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:09:00+00:00","value":11561520,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:10:00+00:00","value":12084408,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:11:00+00:00","value":12607296,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"disk_usage_unit_conversion_bytes_to_mb","labels":{"device":"/dev/sda1","mountpoint":"/"},"timestamp":"2026-01-29T10:12:00+00:00","value":13130184,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user