diff --git a/apps/analyzer/metadata_analyzer/backend.py b/apps/analyzer/metadata_analyzer/backend.py index c13a42c1..4a9ef951 100644 --- a/apps/analyzer/metadata_analyzer/backend.py +++ b/apps/analyzer/metadata_analyzer/backend.py @@ -10,6 +10,6 @@ def send_backup_data_batched(self, batch): r.raise_for_status() def create_alert(self, alert): - url = self.backend_url + "alerting" + url = self.backend_url + "alerting/size" r = requests.post(url, json=alert) r.raise_for_status() diff --git a/apps/analyzer/metadata_analyzer/simple_rule_based_analyzer.py b/apps/analyzer/metadata_analyzer/simple_rule_based_analyzer.py index 091d3203..588698a1 100644 --- a/apps/analyzer/metadata_analyzer/simple_rule_based_analyzer.py +++ b/apps/analyzer/metadata_analyzer/simple_rule_based_analyzer.py @@ -19,9 +19,8 @@ def _analyze_pair(self, result1, result2, bound): return [] alert = { - "type": 0 if relative_change > 0 else 1, - "value": result2.data_size / 1_000_000, - "referenceValue": result1.data_size / 1_000_000, + "size": result2.data_size / 1_000_000, + "referenceSize": result1.data_size / 1_000_000, "backupId": result2.uuid, } return [alert] @@ -47,9 +46,8 @@ def _analyze_pair_diff(self, result1, result2): return [] alert = { - "type": 0 if relative_change > 0 else 1, - "value": result2.data_size / 1_000_000, - "referenceValue": result1.data_size / 1_000_000, + "size": result2.data_size / 1_000_000, + "referenceSize": result1.data_size / 1_000_000, "backupId": result2.uuid, } diff --git a/apps/analyzer/tests/test_simple_rule_based_analyzer.py b/apps/analyzer/tests/test_simple_rule_based_analyzer.py index f3cc00e8..e2b7d7b7 100644 --- a/apps/analyzer/tests/test_simple_rule_based_analyzer.py +++ b/apps/analyzer/tests/test_simple_rule_based_analyzer.py @@ -1,9 +1,11 @@ -from metadata_analyzer.simple_rule_based_analyzer import SimpleRuleBasedAnalyzer +from datetime import datetime + from metadata_analyzer.analyzer import Analyzer from metadata_analyzer.models import Result +from metadata_analyzer.simple_rule_based_analyzer import SimpleRuleBasedAnalyzer from tests.mock_backend import MockBackend from tests.mock_database import MockDatabase -from datetime import datetime + def _create_mock_result(task, uuid, fdi_type, data_size, start_time): mock_result = Result() @@ -14,6 +16,7 @@ def _create_mock_result(task, uuid, fdi_type, data_size, start_time): mock_result.start_time = start_time return mock_result + def test_alert(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "F", 121_000_000, datetime.fromisoformat("2000-01-02")) @@ -25,12 +28,12 @@ def test_alert(): Analyzer.simple_rule_based_analysis(-1) assert backend.alerts == [{ - "type": 0, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] + def test_alerts_different_tasks(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "F", 121_000_000, datetime.fromisoformat("2000-01-02")) @@ -44,17 +47,16 @@ def test_alerts_different_tasks(): Analyzer.simple_rule_based_analysis(-1) assert backend.alerts == [{ - "type": 0, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }, { - "type": 1, - "value": mock_result4.data_size / 1_000_000, - "referenceValue": mock_result3.data_size / 1_000_000, + "size": mock_result4.data_size / 1_000_000, + "referenceSize": mock_result3.data_size / 1_000_000, "backupId": mock_result4.uuid }] + def test_alert_backup_size_zero(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "F", 0, datetime.fromisoformat("2000-01-02")) @@ -66,12 +68,12 @@ def test_alert_backup_size_zero(): Analyzer.simple_rule_based_analysis(-1) assert backend.alerts == [{ - "type": 1, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] + def test_no_alert_size_diff_too_small(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "F", 120_000_000, datetime.fromisoformat("2000-01-02")) @@ -84,6 +86,7 @@ def test_no_alert_size_diff_too_small(): assert backend.alerts == [] + def test_no_alert_wrong_type(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "D", 121_000_000, datetime.fromisoformat("2000-01-02")) @@ -96,6 +99,7 @@ def test_no_alert_wrong_type(): assert backend.alerts == [] + def test_no_alert_different_tasks(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("bar", "2", "F", 121_000_000, datetime.fromisoformat("2000-01-02")) @@ -108,6 +112,7 @@ def test_no_alert_different_tasks(): assert backend.alerts == [] + def test_alert_limit(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "F", 150_000_000, datetime.fromisoformat("2000-01-02")) @@ -121,6 +126,7 @@ def test_alert_limit(): assert len(backend.alerts) == 1 + # extremely large difference def test_alert_backup_size_zero_diff(): mock_result1 = _create_mock_result("foo", "1", "D", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -133,12 +139,12 @@ def test_alert_backup_size_zero_diff(): Analyzer.simple_rule_based_analysis_diff(1) assert backend.alerts == [{ - "type": 1, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] + # two decreasing diff backups (in the accepted range) with different full backups as base def test_alert_backup_size_decrease_ok_diff(): mock_result1 = _create_mock_result("foo", "1", "D", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -153,6 +159,7 @@ def test_alert_backup_size_decrease_ok_diff(): assert backend.alerts == [] + # two decreasing diff backups (in the accepted range) with same full backup as base def test_alert_backup_size_decrease_nok_diff(): mock_result1 = _create_mock_result("foo", "1", "D", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -165,13 +172,13 @@ def test_alert_backup_size_decrease_nok_diff(): Analyzer.simple_rule_based_analysis_diff(1) assert backend.alerts == [{ - "type": 1, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] - # two decreasing diff backups (not in the accepted range) with same full backup as base + +# two decreasing diff backups (not in the accepted range) with same full backup as base def test_alert_backup_size_decrease_large_nok_diff(): mock_result1 = _create_mock_result("foo", "1", "D", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "D", 1_000_000, datetime.fromisoformat("2000-01-03")) @@ -183,12 +190,12 @@ def test_alert_backup_size_decrease_large_nok_diff(): Analyzer.simple_rule_based_analysis_diff(1) assert backend.alerts == [{ - "type": 1, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] + # two decreasing diff backups (not in the accepted range) with different full backups as base def test_alert_backup_size_decrease_large_ok_diff(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -204,6 +211,7 @@ def test_alert_backup_size_decrease_large_ok_diff(): assert backend.alerts == [] + # two increasing diff backups (not in the accepted range) with same full backups as base def test_alert_backup_size_increase_large_nok_diff(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -217,12 +225,12 @@ def test_alert_backup_size_increase_large_nok_diff(): Analyzer.simple_rule_based_analysis_diff(1) assert backend.alerts == [{ - "type": 0, - "value": mock_result3.data_size / 1_000_000, - "referenceValue": mock_result2.data_size / 1_000_000, + "size": mock_result3.data_size / 1_000_000, + "referenceSize": mock_result2.data_size / 1_000_000, "backupId": mock_result3.uuid }] + # two increasing diff backups (not in the accepted range) with different full backups as base def test_alert_backup_size_increase_large_ok_diff(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -239,6 +247,8 @@ def test_alert_backup_size_increase_large_ok_diff(): assert backend.alerts == [] # multiple decreasing diff backups (not in the accepted range) with same full backups as base + + def test_alert_backup_size_complex_nok_diff(): mock_result1 = _create_mock_result("foo", "1", "F", 100_000_000, datetime.fromisoformat("2000-01-01")) mock_result2 = _create_mock_result("foo", "2", "D", 1_000_000, datetime.fromisoformat("2000-01-02")) @@ -248,19 +258,20 @@ def test_alert_backup_size_complex_nok_diff(): mock_result6 = _create_mock_result("foo", "6", "D", 101_000_000, datetime.fromisoformat("2000-01-06")) mock_result7 = _create_mock_result("foo", "7", "D", 1_000_000, datetime.fromisoformat("2000-01-07")) - database = MockDatabase([mock_result1, mock_result2, mock_result3, mock_result4, mock_result5, mock_result6, mock_result7]) + database = MockDatabase( + [mock_result1, mock_result2, mock_result3, mock_result4, mock_result5, mock_result6, mock_result7]) backend = MockBackend() simple_rule_based_analyzer = SimpleRuleBasedAnalyzer(backend, 0.2, 0.2, 0.2, 0.2) Analyzer.init(database, backend, None, simple_rule_based_analyzer) Analyzer.simple_rule_based_analysis_diff(1) assert backend.alerts == [{ - "type": 1, - "value": mock_result7.data_size / 1_000_000, - "referenceValue": mock_result6.data_size / 1_000_000, + "size": mock_result7.data_size / 1_000_000, + "referenceSize": mock_result6.data_size / 1_000_000, "backupId": mock_result7.uuid }] + # large increase of inc size def test_alert_backup_size_zero_inc(): mock_result1 = _create_mock_result("foo", "1", "I", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -273,12 +284,12 @@ def test_alert_backup_size_zero_inc(): Analyzer.simple_rule_based_analysis_inc(1) assert backend.alerts == [{ - "type": 1, - "value": mock_result2.data_size / 1_000_000, - "referenceValue": mock_result1.data_size / 1_000_000, + "size": mock_result2.data_size / 1_000_000, + "referenceSize": mock_result1.data_size / 1_000_000, "backupId": mock_result2.uuid }] + # irregular backup times that should not be alerted def test_alert_backup_size_irregular_inc(): mock_result1 = _create_mock_result("foo", "1", "I", 100_000_000, datetime.fromisoformat("2000-01-01")) @@ -286,7 +297,7 @@ def test_alert_backup_size_irregular_inc(): mock_result3 = _create_mock_result("foo", "3", "I", 100_000_000, datetime.fromisoformat("2000-01-09")) mock_result4 = _create_mock_result("foo", "4", "I", 100_000_000, datetime.fromisoformat("2000-01-10")) - database = MockDatabase([mock_result1, mock_result2, mock_result3,mock_result4]) + database = MockDatabase([mock_result1, mock_result2, mock_result3, mock_result4]) backend = MockBackend() simple_rule_based_analyzer = SimpleRuleBasedAnalyzer(backend, 0.2, 0.2, 0.2, 0.2) Analyzer.init(database, backend, None, simple_rule_based_analyzer) @@ -294,13 +305,14 @@ def test_alert_backup_size_irregular_inc(): assert backend.alerts == [] + # irregular backup sizes def test_alert_backup_size_irregularSize_inc(): mock_result1 = _create_mock_result("foo", "1", "I", 100_000_000, datetime.fromisoformat("2000-01-07")) mock_result2 = _create_mock_result("foo", "2", "I", 100_000_000, datetime.fromisoformat("2000-01-08")) mock_result3 = _create_mock_result("foo", "3", "I", 72_000_000, datetime.fromisoformat("2000-01-09")) mock_result4 = _create_mock_result("foo", "4", "I", 100_000_000, datetime.fromisoformat("2000-01-10")) - avg = (mock_result1.data_size + mock_result2.data_size + mock_result3.data_size + mock_result4.data_size)/4 + avg = (mock_result1.data_size + mock_result2.data_size + mock_result3.data_size + mock_result4.data_size) / 4 database = MockDatabase([mock_result1, mock_result2, mock_result3, mock_result4]) backend = MockBackend() @@ -309,8 +321,7 @@ def test_alert_backup_size_irregularSize_inc(): Analyzer.simple_rule_based_analysis_inc(1) assert backend.alerts == [{ - "type": 1, - "value":72, - "referenceValue": avg / 1_000_000, + "size": 72, + "referenceSize": avg / 1_000_000, "backupId": mock_result3.uuid - }] \ No newline at end of file + }] diff --git a/apps/backend/src/app/alerting/alerting.controller.spec.ts b/apps/backend/src/app/alerting/alerting.controller.spec.ts index ad6edd23..08618ce8 100644 --- a/apps/backend/src/app/alerting/alerting.controller.spec.ts +++ b/apps/backend/src/app/alerting/alerting.controller.spec.ts @@ -4,69 +4,76 @@ import request from 'supertest'; import { getRepositoryToken } from '@nestjs/typeorm'; import { MoreThanOrEqual, Repository } from 'typeorm'; import { AlertingModule } from './alerting.module'; -import { AlertEntity } from './entity/alert.entity'; -import { CreateAlertDto } from './dto/createAlert.dto'; -import { AlertType } from './dto/alertType'; import { BackupDataEntity } from '../backupData/entity/backupData.entity'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { MailService } from '../utils/mail/mail.service'; import { BackupType } from '../backupData/dto/backupType'; +import { SizeAlertEntity } from './entity/alerts/sizeAlert.entity'; +import { AlertTypeEntity } from './entity/alertType.entity'; +import { SeverityType } from './dto/severityType'; +import { CreateSizeAlertDto } from './dto/alerts/createSizeAlert.dto'; -const mockBackupDataEntity: BackupDataEntity = { +const mockedBackupDataEntity: BackupDataEntity = { id: 'backup-id', sizeMB: 100, type: BackupType.FULL, - creationDate: new Date('2023-12-30 00:00:00.000000'), + creationDate: new Date(), }; -const mockAlertEntity: AlertEntity = { - id: 'alert-id', - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, - backup: mockBackupDataEntity, +const mockedAlertTypeEntity: AlertTypeEntity = { + id: 'alert-type-id', + name: 'SIZE_ALERT', + severity: SeverityType.WARNING, + user_active: true, + master_active: true, }; +const sizeAlert: SizeAlertEntity = { + id: 'alert-id', + size: 100, + referenceSize: 200, + backup: mockedBackupDataEntity, + alertType: mockedAlertTypeEntity, +}; const mockAlertRepository = { save: jest.fn().mockImplementation((alert) => Promise.resolve(alert)), - find: jest.fn().mockImplementation(() => Promise.resolve([mockAlertEntity])), + find: jest.fn().mockImplementation(() => Promise.resolve([sizeAlert])), findOneBy: jest.fn().mockResolvedValue(null), }; describe('AlertingController (e2e)', () => { let app: INestApplication; - let repository: Repository; + let repository: Repository; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [AlertingModule, await ConfigModule.forRoot({ isGlobal: true })], + imports: [AlertingModule, ConfigModule.forRoot({ isGlobal: true })], }) .overrideProvider(ConfigService) .useValue({ - getOrThrow: jest.fn(() => { - return 'test'; - }), + getOrThrow: jest.fn(() => 'test'), }) - .overrideProvider(getRepositoryToken(BackupDataEntity)) .useValue({ - findOne: jest - .fn() - .mockImplementation(() => Promise.resolve(mockBackupDataEntity)), + findOne: jest.fn().mockResolvedValue(mockedBackupDataEntity), }) - .overrideProvider(MailService) .useValue({ sendAlertMail: jest.fn(), }) - - .overrideProvider(getRepositoryToken(AlertEntity)) + .overrideProvider(getRepositoryToken(SizeAlertEntity)) .useValue(mockAlertRepository) - + .overrideProvider(getRepositoryToken(AlertTypeEntity)) + .useValue({ + findOneBy: jest.fn().mockImplementation(({ name }) => { + return name === 'SIZE_ALERT' ? mockedAlertTypeEntity : null; + }), + save: jest.fn(), + find: jest.fn().mockResolvedValue([]), + }) .compile(); - repository = module.get(getRepositoryToken(AlertEntity)); - + repository = module.get(getRepositoryToken(SizeAlertEntity)); app = module.createNestApplication(); await app.init(); }); @@ -80,9 +87,8 @@ describe('AlertingController (e2e)', () => { .get('/alerting') .expect(200); - expect(mockAlertRepository.find).toHaveBeenCalledWith(); - // Convert creationDate to Date object for comparison - const receivedAlerts = response.body.map((alert: AlertEntity) => ({ + expect(mockAlertRepository.find).toHaveBeenCalledWith({ where: {} }); + const receivedAlerts = response.body.map((alert: SizeAlertEntity) => ({ ...alert, backup: { ...alert.backup, @@ -90,7 +96,7 @@ describe('AlertingController (e2e)', () => { }, })); - expect(receivedAlerts).toEqual([mockAlertEntity]); + expect(receivedAlerts).toEqual([sizeAlert]); }); it('GET /alerting - should filter alerts by backupId', async () => { @@ -98,12 +104,12 @@ describe('AlertingController (e2e)', () => { .get('/alerting') .query({ backupId: 'backup-id' }) .expect(200); + expect(mockAlertRepository.find).toHaveBeenCalledWith({ where: { backup: { id: 'backup-id' } }, }); - // Convert creationDate to Date object for comparison - const receivedAlerts = response.body.map((alert: AlertEntity) => ({ + const receivedAlerts = response.body.map((alert: SizeAlertEntity) => ({ ...alert, backup: { ...alert.backup, @@ -111,7 +117,7 @@ describe('AlertingController (e2e)', () => { }, })); - expect(receivedAlerts).toEqual([mockAlertEntity]); + expect(receivedAlerts).toEqual([sizeAlert]); }); it('GET /alerting - should filter alerts by the last x days', async () => { @@ -128,8 +134,7 @@ describe('AlertingController (e2e)', () => { where: { backup: { creationDate: MoreThanOrEqual(expect.any(Date)) } }, }); - // Convert creationDate to Date object for comparison - const receivedAlerts = response.body.map((alert: AlertEntity) => ({ + const receivedAlerts = response.body.map((alert: SizeAlertEntity) => ({ ...alert, backup: { ...alert.backup, @@ -137,39 +142,28 @@ describe('AlertingController (e2e)', () => { }, })); - expect(receivedAlerts).toEqual([mockAlertEntity]); + expect(receivedAlerts).toEqual([sizeAlert]); }); - it('POST /alerting - should create a new alert', async () => { - const createAlertDto: CreateAlertDto = { - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, + it('POST /alerting/size - should create a new size alert', async () => { + const createAlertDto: CreateSizeAlertDto = { + size: 100, + referenceSize: 200, backupId: 'backup-id', }; await request(app.getHttpServer()) - .post('/alerting') + .post('/alerting/size') .send(createAlertDto) .expect(201); expect(mockAlertRepository.save).toHaveBeenCalledWith( expect.objectContaining({ - type: createAlertDto.type, - value: createAlertDto.value, - referenceValue: createAlertDto.referenceValue, + size: createAlertDto.size, + referenceSize: createAlertDto.referenceSize, backup: expect.objectContaining({ id: createAlertDto.backupId }), + alertType: expect.objectContaining({ name: 'SIZE_ALERT' }), }) ); }); - it('GET /alerting/:id - should return 404 if alert not found', async () => { - const nonExistentId = 'non-existent-id'; - mockAlertRepository.find.mockImplementationOnce(() => - Promise.resolve(null) - ); - - const response = await request(app.getHttpServer()) - .get(`/alerting/${nonExistentId}`) - .expect(404); - }); }); diff --git a/apps/backend/src/app/alerting/alerting.controller.ts b/apps/backend/src/app/alerting/alerting.controller.ts index 2eeeba00..1e42ae9f 100644 --- a/apps/backend/src/app/alerting/alerting.controller.ts +++ b/apps/backend/src/app/alerting/alerting.controller.ts @@ -1,13 +1,16 @@ import { Body, Controller, Get, Logger, Post, Query } from '@nestjs/common'; import { + ApiBody, ApiConflictResponse, ApiNotFoundResponse, ApiOperation, ApiQuery, } from '@nestjs/swagger'; import { AlertingService } from './alerting.service'; -import { AlertEntity } from './entity/alert.entity'; -import { CreateAlertDto } from './dto/createAlert.dto'; +import { CreateAlertTypeDto } from './dto/createAlertType.dto'; +import { AlertTypeEntity } from './entity/alertType.entity'; +import { CreateSizeAlertDto } from './dto/alerts/createSizeAlert.dto'; +import { Alert } from './entity/alerts/alert'; @Controller('alerting') export class AlertingController { @@ -15,6 +18,35 @@ export class AlertingController { constructor(private readonly alertingService: AlertingService) {} + @Post('type') + @ApiOperation({ summary: 'Create a new alert type.' }) + @ApiConflictResponse({ description: 'Alert type already exists' }) + @ApiBody({ type: CreateAlertTypeDto }) + async createAlertType(@Body() createAlertTypeDto: CreateAlertTypeDto) { + await this.alertingService.createAlertType(createAlertTypeDto); + } + + @Get('type') + @ApiOperation({ summary: 'Get all alert types.' }) + @ApiQuery({ + name: 'user_active', + description: 'Filter alert types by user active', + required: false, + type: Boolean, + }) + @ApiQuery({ + name: 'master_active', + description: 'Filter alert types by master active', + required: false, + type: Boolean, + }) + async getAllAlertTypes( + @Query('user_active') user_active?: boolean, + @Query('master_active') master_active?: boolean + ): Promise { + return this.alertingService.findAllAlertTypes(user_active, master_active); + } + @Get() @ApiOperation({ summary: 'Get all alerts.' }) @ApiQuery({ @@ -31,14 +63,17 @@ export class AlertingController { async getAllAlerts( @Query('backupId') backupId?: string, @Query('days') days?: number - ): Promise { - return this.alertingService.findAllAlerts(backupId, days); + ): Promise { + return this.alertingService.getAllAlerts(backupId, days); } - @Post() - @ApiOperation({ summary: 'Create a new alert.' }) + @Post('size') + @ApiOperation({ summary: 'Create a new size alert.' }) @ApiNotFoundResponse({ description: 'Backup not found' }) - async createAlert(@Body() createAlertDto: CreateAlertDto): Promise { - await this.alertingService.createAlert(createAlertDto); + @ApiBody({ type: CreateSizeAlertDto }) + async createSIzeAlert( + @Body() createSizeAlertDto: CreateSizeAlertDto + ): Promise { + await this.alertingService.createSizeAlert(createSizeAlertDto); } } diff --git a/apps/backend/src/app/alerting/alerting.module.ts b/apps/backend/src/app/alerting/alerting.module.ts index c310826e..de9f2201 100644 --- a/apps/backend/src/app/alerting/alerting.module.ts +++ b/apps/backend/src/app/alerting/alerting.module.ts @@ -3,14 +3,15 @@ import { AlertingService } from './alerting.service'; import { MailModule } from '../utils/mail/mail.module'; import { AlertingController } from './alerting.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AlertEntity } from './entity/alert.entity'; import { BackupDataModule } from '../backupData/backupData.module'; +import { AlertTypeEntity } from './entity/alertType.entity'; +import { SizeAlertEntity } from './entity/alerts/sizeAlert.entity'; @Module({ imports: [ MailModule, BackupDataModule, - TypeOrmModule.forFeature([AlertEntity]), + TypeOrmModule.forFeature([AlertTypeEntity, SizeAlertEntity]), ], providers: [AlertingService], controllers: [AlertingController], diff --git a/apps/backend/src/app/alerting/alerting.service.spec.ts b/apps/backend/src/app/alerting/alerting.service.spec.ts index 6febe95e..8a9d9438 100644 --- a/apps/backend/src/app/alerting/alerting.service.spec.ts +++ b/apps/backend/src/app/alerting/alerting.service.spec.ts @@ -2,12 +2,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AlertingService } from './alerting.service'; import { MailService } from '../utils/mail/mail.service'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { AlertEntity } from './entity/alert.entity'; +import { SizeAlertEntity } from './entity/alerts/sizeAlert.entity'; import { MoreThanOrEqual, Repository } from 'typeorm'; import { BackupDataService } from '../backupData/backupData.service'; -import { NotFoundException } from '@nestjs/common'; -import { CreateAlertDto } from './dto/createAlert.dto'; -import { AlertType } from './dto/alertType'; +import { ConflictException, NotFoundException } from '@nestjs/common'; +import { CreateSizeAlertDto } from './dto/alerts/createSizeAlert.dto'; +import { AlertTypeEntity } from './entity/alertType.entity'; +import { CreateAlertTypeDto } from './dto/createAlertType.dto'; +import { SeverityType } from './dto/severityType'; +import { Alert } from './entity/alerts/alert'; import { BackupDataEntity } from '../backupData/entity/backupData.entity'; import { BackupType } from '../backupData/dto/backupType'; @@ -18,25 +21,28 @@ const mockedBackupDataEntity: BackupDataEntity = { creationDate: new Date(), }; -const alerts: AlertEntity[] = [ - { - id: 'alert-id-1', - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, - backup: mockedBackupDataEntity, - }, +const mockedAlertTypeEntity: AlertTypeEntity = { + id: 'alert-type-id', + name: 'SIZE_ALERT', + severity: SeverityType.WARNING, + user_active: true, + master_active: true, +}; + +const alerts: SizeAlertEntity[] = [ { - id: 'alert-id-2', - type: AlertType.SIZE_INCREASED, - value: 200, - referenceValue: 100, + id: 'alert-id', + size: 100, + referenceSize: 200, backup: mockedBackupDataEntity, + alertType: mockedAlertTypeEntity, }, ]; + describe('AlertingService', () => { let service: AlertingService; - let alertRepository: Repository; + let sizeAlertRepository: Repository; + let alertTypeRepository: Repository; let mailService: MailService; let backupDataService: BackupDataService; @@ -45,13 +51,23 @@ describe('AlertingService', () => { providers: [ AlertingService, { - provide: getRepositoryToken(AlertEntity), + provide: getRepositoryToken(SizeAlertEntity), useValue: { find: jest.fn().mockResolvedValue(alerts), findOneBy: jest.fn().mockResolvedValue(null), save: jest.fn(), }, }, + { + provide: getRepositoryToken(AlertTypeEntity), + useValue: { + findOneBy: jest.fn().mockImplementation(({ name }) => { + return name === 'SIZE_ALERT' ? mockedAlertTypeEntity : null; + }), + save: jest.fn(), + find: jest.fn().mockResolvedValue([]), + }, + }, { provide: MailService, useValue: { @@ -73,7 +89,8 @@ describe('AlertingService', () => { }).compile(); service = module.get(AlertingService); - alertRepository = module.get(getRepositoryToken(AlertEntity)); + sizeAlertRepository = module.get(getRepositoryToken(SizeAlertEntity)); + alertTypeRepository = module.get(getRepositoryToken(AlertTypeEntity)); mailService = module.get(MailService); backupDataService = module.get(BackupDataService); }); @@ -82,23 +99,21 @@ describe('AlertingService', () => { expect(service).toBeDefined(); }); - describe('createAlert', () => { - it('should create and save an alert', async () => { - const createAlertDto: CreateAlertDto = { - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, + describe('createSizeAlert', () => { + it('should create and save a size alert', async () => { + const createSizeAlertDto: CreateSizeAlertDto = { + size: 100, + referenceSize: 200, backupId: 'backup-id', }; - await service.createAlert(createAlertDto); + await service.createSizeAlert(createSizeAlertDto); expect(backupDataService.findOneById).toHaveBeenCalledWith('backup-id'); - expect(alertRepository.save).toHaveBeenCalledWith( + expect(sizeAlertRepository.save).toHaveBeenCalledWith( expect.objectContaining({ - type: 1, - value: 100, - referenceValue: 200, + size: 100, + referenceSize: 200, backup: mockedBackupDataEntity, }) ); @@ -106,32 +121,78 @@ describe('AlertingService', () => { }); it('should throw NotFoundException if backup not found', async () => { - const createAlertDto: CreateAlertDto = { - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, + const createSizeAlertDto: CreateSizeAlertDto = { + size: 100, + referenceSize: 200, backupId: 'not-existing-id', }; - await expect(service.createAlert(createAlertDto)).rejects.toThrow( + await expect(service.createSizeAlert(createSizeAlertDto)).rejects.toThrow( NotFoundException ); }); }); + describe('createAlertType', () => { + it('should create and save an alert type', async () => { + const createAlertTypeDto: CreateAlertTypeDto = { + severity: SeverityType.WARNING, + name: 'CREATION_TIME_ALERT', + master_active: true, + }; + + await service.createAlertType(createAlertTypeDto); + + expect(alertTypeRepository.findOneBy).toHaveBeenCalledWith({ + name: 'CREATION_TIME_ALERT', + }); + expect(alertTypeRepository.save).toHaveBeenCalledWith(createAlertTypeDto); + }); + + it('should throw ConflictException if alert type already exists', async () => { + const createAlertTypeDto: CreateAlertTypeDto = { + severity: SeverityType.WARNING, + name: 'SIZE_ALERT', + master_active: true, + }; + + await expect(service.createAlertType(createAlertTypeDto)).rejects.toThrow( + ConflictException + ); + }); + }); + + describe('findAllAlertTypes', () => { + it('should return all alert types', async () => { + const result = await service.findAllAlertTypes(); + + expect(result).toEqual([]); + expect(alertTypeRepository.find).toHaveBeenCalled(); + }); + + it('should return alert types with user_active and master_active filters', async () => { + const result = await service.findAllAlertTypes(true, true); + + expect(result).toEqual([]); + expect(alertTypeRepository.find).toHaveBeenCalledWith({ + where: { user_active: true, master_active: true }, + }); + }); + }); + describe('findAllAlerts', () => { it('should return all alerts', async () => { - const result = await service.findAllAlerts(); + const result = await service.getAllAlerts(); expect(result).toEqual(alerts); - expect(alertRepository.find).toHaveBeenCalled(); + expect(sizeAlertRepository.find).toHaveBeenCalled(); }); it('should return alerts for a specific backup', async () => { - const result = await service.findAllAlerts('backup-id'); + const result = await service.getAllAlerts('backup-id'); expect(result).toEqual(alerts); - expect(alertRepository.find).toHaveBeenCalledWith({ + expect(sizeAlertRepository.find).toHaveBeenCalledWith({ where: { backup: { id: 'backup-id' } }, }); }); @@ -141,10 +202,10 @@ describe('AlertingService', () => { const date = new Date(); date.setDate(date.getDate() - days); - const result = await service.findAllAlerts(undefined, days); + const result = await service.getAllAlerts(undefined, days); expect(result).toEqual(alerts); - expect(alertRepository.find).toHaveBeenCalledWith({ + expect(sizeAlertRepository.find).toHaveBeenCalledWith({ where: { backup: { creationDate: MoreThanOrEqual(expect.any(Date)) } }, }); }); @@ -152,7 +213,7 @@ describe('AlertingService', () => { describe('triggerAlertMail', () => { it('should call sendAlertMail of MailService', async () => { - const alert = alerts[0]; + const alert: Alert = alerts[0]; await service.triggerAlertMail(alert); diff --git a/apps/backend/src/app/alerting/alerting.service.ts b/apps/backend/src/app/alerting/alerting.service.ts index e56ad6b7..b1f035d2 100644 --- a/apps/backend/src/app/alerting/alerting.service.ts +++ b/apps/backend/src/app/alerting/alerting.service.ts @@ -1,69 +1,120 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { MailService } from '../utils/mail/mail.service'; import { InjectRepository } from '@nestjs/typeorm'; -import { AlertEntity } from './entity/alert.entity'; -import { MoreThanOrEqual, Repository } from 'typeorm'; -import { CreateAlertDto } from './dto/createAlert.dto'; +import { FindOptionsWhere, MoreThanOrEqual, Repository } from 'typeorm'; import { BackupDataService } from '../backupData/backupData.service'; +import { CreateAlertTypeDto } from './dto/createAlertType.dto'; +import { AlertTypeEntity } from './entity/alertType.entity'; +import { Alert } from './entity/alerts/alert'; +import { CreateSizeAlertDto } from './dto/alerts/createSizeAlert.dto'; +import { SizeAlertEntity } from './entity/alerts/sizeAlert.entity'; @Injectable() export class AlertingService { + alertRepositories: Repository[] = []; + constructor( - @InjectRepository(AlertEntity) - private alertRepository: Repository, + @InjectRepository(AlertTypeEntity) + private alertTypeRepository: Repository, + //Alert Repositories + @InjectRepository(SizeAlertEntity) + private sizeAlertRepository: Repository, private mailService: MailService, private backupDataService: BackupDataService - ) {} - - async createAlert(createAlertDto: CreateAlertDto) { - const alert = new AlertEntity(); + ) { + this.alertRepositories.push(this.sizeAlertRepository); + } - alert.type = createAlertDto.type; - alert.value = createAlertDto.value; - alert.referenceValue = createAlertDto.referenceValue; - const backupDataEntity = await this.backupDataService.findOneById( - createAlertDto.backupId - ); - if (!backupDataEntity) { - console.log(`Backup with id ${createAlertDto.backupId} not found`); - throw new NotFoundException( - `Backup with id ${createAlertDto.backupId} not found` + async createAlertType(createAlertTypeDto: CreateAlertTypeDto) { + const entity = await this.alertTypeRepository.findOneBy({ + name: createAlertTypeDto.name, + }); + if (entity) { + throw new ConflictException( + `Alert type with name ${createAlertTypeDto.name} already exists` ); } - alert.backup = backupDataEntity; + return await this.alertTypeRepository.save(createAlertTypeDto); + } - const existingAlertEntity = await this.alertRepository.findOneBy({ - backup: backupDataEntity, - type: alert.type, - }); - if (existingAlertEntity) { - console.log('Alert already exists -> ignoring it'); - return; + async findAllAlertTypes( + user_active?: boolean, + master_active?: boolean + ): Promise { + const where: FindOptionsWhere = {}; + if (user_active) { + where.user_active = user_active; + } + if (master_active) { + where.master_active = master_active; } - const entity = await this.alertRepository.save(alert); - await this.triggerAlertMail(entity); + return await this.alertTypeRepository.find({ where }); } - async findAllAlerts( - backupId?: string, - days?: number - ): Promise { + async triggerAlertMail(alert: Alert) { + await this.mailService.sendAlertMail(alert); + } + + async getAllAlerts(backupId?: string, days?: number): Promise { + const where: FindOptionsWhere = {}; if (backupId) { - return await this.alertRepository.find({ - where: { backup: { id: backupId } }, - }); + where.backup = { id: backupId }; } if (days) { const date = new Date(); date.setDate(date.getDate() - days); - return await this.alertRepository.find({ - where: { backup: { creationDate: MoreThanOrEqual(date) } }, - }); + where.backup = { creationDate: MoreThanOrEqual(date) }; + } + + //Iterate over all alert repositories and get all alerts + const alerts: Alert[] = []; + for (const alertRepository of this.alertRepositories) { + alerts.push(...(await alertRepository.find({ where }))); } - return await this.alertRepository.find(); + return alerts; } - async triggerAlertMail(alert: AlertEntity) { - await this.mailService.sendAlertMail(alert); + async createSizeAlert(createSizeAlertDto: CreateSizeAlertDto) { + // Check if alert already exists + const existingAlertEntity = await this.sizeAlertRepository.findOneBy({ + backup: { id: createSizeAlertDto.backupId }, + }); + + if (existingAlertEntity) { + console.log('Alert already exists -> ignoring it'); + return; + } + + const alert = new SizeAlertEntity(); + alert.size = createSizeAlertDto.size; + alert.referenceSize = createSizeAlertDto.referenceSize; + + const backup = await this.backupDataService.findOneById( + createSizeAlertDto.backupId + ); + if (!backup) { + throw new NotFoundException( + `Backup with id ${createSizeAlertDto.backupId} not found` + ); + } + alert.backup = backup; + + const alertType = await this.alertTypeRepository.findOneBy({ + name: 'SIZE_ALERT', + }); + if (!alertType) { + throw new NotFoundException('Alert type SIZE_ALERT not found'); + } + alert.alertType = alertType; + + await this.sizeAlertRepository.save(alert); + + if (alert.alertType.user_active && alert.alertType.master_active) { + await this.triggerAlertMail(alert); + } } } diff --git a/apps/backend/src/app/alerting/dto/alertType.ts b/apps/backend/src/app/alerting/dto/alertType.ts deleted file mode 100644 index 1967fc7a..00000000 --- a/apps/backend/src/app/alerting/dto/alertType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Possible types of alerts -export enum AlertType { - SIZE_INCREASED, - SIZE_DECREASED, -} diff --git a/apps/backend/src/app/alerting/dto/alertingInformation.dto.ts b/apps/backend/src/app/alerting/dto/alertingInformation.dto.ts deleted file mode 100644 index 9ec7ff7f..00000000 --- a/apps/backend/src/app/alerting/dto/alertingInformation.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class AlertingInformationDto { - @ApiProperty({ - description: 'Reason for the alert', - required: true, - }) - @IsString() - reason!: string; - - @ApiProperty({ - description: 'Description of the alert', - required: true, - }) - @IsString() - description!: string; -} diff --git a/apps/backend/src/app/alerting/dto/alerts/createSizeAlert.dto.ts b/apps/backend/src/app/alerting/dto/alerts/createSizeAlert.dto.ts new file mode 100644 index 00000000..0f8be92a --- /dev/null +++ b/apps/backend/src/app/alerting/dto/alerts/createSizeAlert.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateSizeAlertDto { + @ApiProperty({ + description: 'Id of the belonging Backup', + required: true, + }) + backupId!: string; + + @ApiProperty({ + description: 'Size of the Backup, which is the reason for the alert', + }) + size!: number; + @ApiProperty({ + description: + 'Reference size to the value of the Backup, in which comparison the alert was triggered', + }) + referenceSize!: number; +} diff --git a/apps/backend/src/app/alerting/dto/createAlert.dto.ts b/apps/backend/src/app/alerting/dto/createAlert.dto.ts deleted file mode 100644 index 2a64f26e..00000000 --- a/apps/backend/src/app/alerting/dto/createAlert.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AlertType } from './alertType'; -import { IsEnum, IsNumber, IsString } from 'class-validator'; - -export class CreateAlertDto { - @ApiProperty({ - description: 'Type of alert', - required: true, - enum: AlertType, - }) - @IsEnum(AlertType) - type!: AlertType; - - @ApiProperty({ - description: 'Value of the Backup, which is the reason for the alert', - required: true, - }) - @IsNumber() - value!: number; - - @ApiProperty({ - description: - 'Reference Value to the value of the Backup, in which comparison the alert was triggered', - required: true, - }) - @IsNumber() - referenceValue!: number; - - @ApiProperty({ - description: 'ID of the belonging backup', - required: true, - }) - @IsString() - backupId!: string; -} diff --git a/apps/backend/src/app/alerting/dto/createAlertType.dto.ts b/apps/backend/src/app/alerting/dto/createAlertType.dto.ts new file mode 100644 index 00000000..9e4e8309 --- /dev/null +++ b/apps/backend/src/app/alerting/dto/createAlertType.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { SeverityType } from './severityType'; +import { IsBoolean, IsEnum, IsString } from 'class-validator'; + +export class CreateAlertTypeDto { + @ApiProperty({ + description: 'Name of the alert type', + example: 'SIZE_ALERT', + }) + @IsString() + name!: string; + + @ApiProperty({ + description: 'Severity of the alert type', + enum: SeverityType, + example: SeverityType.WARNING, + }) + @IsEnum(SeverityType) + severity!: SeverityType; + + @ApiProperty({ + description: 'Is the alert type set active by the admin', + example: true, + }) + @IsBoolean() + master_active!: boolean; +} diff --git a/apps/backend/src/app/alerting/dto/severityType.ts b/apps/backend/src/app/alerting/dto/severityType.ts new file mode 100644 index 00000000..73356482 --- /dev/null +++ b/apps/backend/src/app/alerting/dto/severityType.ts @@ -0,0 +1,5 @@ +export enum SeverityType { + INFO = 'INFO', + WARNING = 'WARNING', + CRITICAL = 'CRITICAL', +} diff --git a/apps/backend/src/app/alerting/entity/alert.entity.ts b/apps/backend/src/app/alerting/entity/alert.entity.ts deleted file mode 100644 index 6632d7c0..00000000 --- a/apps/backend/src/app/alerting/entity/alert.entity.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { ApiProperty } from '@nestjs/swagger'; -import { AlertType } from '../dto/alertType'; -import { BackupDataEntity } from '../../backupData/entity/backupData.entity'; - -@Entity('Alert') -export class AlertEntity { - @ApiProperty({ - description: 'Auto-generated UUID of the Alert', - required: true, - }) - @PrimaryGeneratedColumn('uuid') - id!: string; - - @ApiProperty({ - description: 'Reason for the alert', - required: true, - enum: AlertType, - }) - @Column({ - type: 'enum', - enum: AlertType, - }) - type!: AlertType; - - @ApiProperty({ - description: 'Value of the Backup, which is the reason for the alert', - }) - @Column({ type: 'decimal', precision: 20, scale: 6 }) - value!: number; - - @ApiProperty({ - description: - 'Reference Value to the value of the Backup, in which comparison the alert was triggered', - }) - @Column({ type: 'decimal', precision: 20, scale: 6 }) - referenceValue!: number; - - @ManyToOne(() => BackupDataEntity, { - nullable: false, - eager: true, - }) - @JoinColumn({ name: 'backupId', referencedColumnName: 'id' }) - backup!: BackupDataEntity; -} diff --git a/apps/backend/src/app/alerting/entity/alertType.entity.ts b/apps/backend/src/app/alerting/entity/alertType.entity.ts new file mode 100644 index 00000000..f360251c --- /dev/null +++ b/apps/backend/src/app/alerting/entity/alertType.entity.ts @@ -0,0 +1,48 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import { SeverityType } from '../dto/severityType'; + +@Entity('AlertType') +export class AlertTypeEntity { + @ApiProperty({ + description: 'Auto-generated UUID of the Alert Type', + required: true, + }) + @PrimaryGeneratedColumn('uuid') + id!: string; + + @ApiProperty({ + description: 'Name of the Alert Type', + required: true, + uniqueItems: true, + }) + @Column({ nullable: false, unique: true }) + name!: string; + + @ApiProperty({ + description: 'Severity of the Alert Type', + required: true, + enum: SeverityType, + }) + @Column({ + type: 'enum', + enum: SeverityType, + default: SeverityType.WARNING, + nullable: false, + }) + severity!: SeverityType; + + @ApiProperty({ + description: 'Is the alert type set active by the user', + required: true, + }) + @Column({ nullable: false, default: true }) + user_active!: boolean; + + @ApiProperty({ + description: 'Is the alert type set active by the admin', + required: true, + }) + @Column({ nullable: false, default: true }) + master_active!: boolean; +} diff --git a/apps/backend/src/app/alerting/entity/alerts/alert.ts b/apps/backend/src/app/alerting/entity/alerts/alert.ts new file mode 100644 index 00000000..1cfa9362 --- /dev/null +++ b/apps/backend/src/app/alerting/entity/alerts/alert.ts @@ -0,0 +1,27 @@ +import { BackupDataEntity } from '../../../backupData/entity/backupData.entity'; +import { AlertTypeEntity } from '../alertType.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; + +export abstract class Alert { + @ApiProperty({ + description: 'Auto-generated UUID of the Alert', + required: true, + }) + @PrimaryGeneratedColumn('uuid') + id!: string; + + @ManyToOne(() => AlertTypeEntity, { + nullable: false, + eager: true, + }) + @JoinColumn({ name: 'alertTypeId', referencedColumnName: 'id' }) + alertType!: AlertTypeEntity; + + @ManyToOne(() => BackupDataEntity, { + nullable: false, + eager: true, + }) + @JoinColumn({ name: 'backupId', referencedColumnName: 'id' }) + backup!: BackupDataEntity; +} diff --git a/apps/backend/src/app/alerting/entity/alerts/sizeAlert.entity.ts b/apps/backend/src/app/alerting/entity/alerts/sizeAlert.entity.ts new file mode 100644 index 00000000..8b57d229 --- /dev/null +++ b/apps/backend/src/app/alerting/entity/alerts/sizeAlert.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity } from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import { Alert } from './alert'; + +@Entity('SizeAlert') +export class SizeAlertEntity extends Alert { + @ApiProperty({ + description: 'Size of the Backup, which is the reason for the alert', + }) + @Column({ type: 'decimal', precision: 20, scale: 6 }) + size!: number; + + @ApiProperty({ + description: + 'Reference size to the value of the Backup, in which comparison the alert was triggered', + }) + @Column({ type: 'decimal', precision: 20, scale: 6 }) + referenceSize!: number; +} diff --git a/apps/backend/src/app/db-config.service.ts b/apps/backend/src/app/db-config.service.ts index 1571d7a4..4bea6972 100644 --- a/apps/backend/src/app/db-config.service.ts +++ b/apps/backend/src/app/db-config.service.ts @@ -7,11 +7,15 @@ import { Init1730126846408 } from './migrations/1730126846408-Init'; import { BackupDataEntity } from './backupData/entity/backupData.entity'; import { AddBackupDataTable1730491370687 } from './migrations/1730491370687-AddBackupDataTable'; import { RemovedBio1731662089990 } from './migrations/1731662089990-RemovedBio'; -import { AlertEntity } from './alerting/entity/alert.entity'; import { Alert1732390760114 } from './migrations/1732390760114-Alert'; import { ChangedSizeToDecimal1732720032144 } from './migrations/1732720032144-ChangedSizeToDecimal'; import { BackupType1732720927342 } from './migrations/1732720927342-BackupType'; import { COPYBackupType1732873335062 } from './migrations/1732873335062-COPYBackupType'; +import { AlertTypeEntity } from './alerting/entity/alertType.entity'; +import { AlertType1732873882256 } from './migrations/1732873882256-AlertType'; +import { AlertTypeNameUnique1732874749343 } from './migrations/1732874749343-AlertTypeNameUnique'; +import { SizeAlertEntity } from './alerting/entity/alerts/sizeAlert.entity'; +import { NewAlertStructure1732887680122 } from './migrations/1732887680122-NewAlertStructure'; /** * Used by NestJS to reach database. @@ -31,7 +35,12 @@ export class DbConfigService implements TypeOrmOptionsFactory { username: this.configService.getOrThrow('DATABASE_USER'), password: this.configService.getOrThrow('DATABASE_PASSWORD'), database: this.configService.getOrThrow('DATABASE_DATABASE'), - entities: [DemoEntity, BackupDataEntity, AlertEntity], + entities: [ + DemoEntity, + BackupDataEntity, + AlertTypeEntity, + SizeAlertEntity, + ], migrationsRun: true, migrations: [ Init1730126846408, @@ -41,6 +50,9 @@ export class DbConfigService implements TypeOrmOptionsFactory { ChangedSizeToDecimal1732720032144, BackupType1732720927342, COPYBackupType1732873335062, + AlertType1732873882256, + AlertTypeNameUnique1732874749343, + NewAlertStructure1732887680122, ], logging: true, }; diff --git a/apps/backend/src/app/migrations/1732873882256-AlertType.ts b/apps/backend/src/app/migrations/1732873882256-AlertType.ts new file mode 100644 index 00000000..479f3a67 --- /dev/null +++ b/apps/backend/src/app/migrations/1732873882256-AlertType.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AlertType1732873882256 implements MigrationInterface { + name = 'AlertType1732873882256' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."AlertType_severity_enum" AS ENUM('INFO', 'WARNING', 'CRITICAL')`); + await queryRunner.query(`CREATE TABLE "AlertType" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "severity" "public"."AlertType_severity_enum" NOT NULL DEFAULT 'WARNING', "user_active" boolean NOT NULL DEFAULT true, "master_active" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_b6aa8ea5fe488881a86839631dd" PRIMARY KEY ("id"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "AlertType"`); + await queryRunner.query(`DROP TYPE "public"."AlertType_severity_enum"`); + } + +} diff --git a/apps/backend/src/app/migrations/1732874749343-AlertTypeNameUnique.ts b/apps/backend/src/app/migrations/1732874749343-AlertTypeNameUnique.ts new file mode 100644 index 00000000..811e9190 --- /dev/null +++ b/apps/backend/src/app/migrations/1732874749343-AlertTypeNameUnique.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AlertTypeNameUnique1732874749343 implements MigrationInterface { + name = 'AlertTypeNameUnique1732874749343' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "AlertType" ADD CONSTRAINT "UQ_3a8563f12f1aad0b69b24420fe6" UNIQUE ("name")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "AlertType" DROP CONSTRAINT "UQ_3a8563f12f1aad0b69b24420fe6"`); + } + +} diff --git a/apps/backend/src/app/migrations/1732887680122-NewAlertStructure.ts b/apps/backend/src/app/migrations/1732887680122-NewAlertStructure.ts new file mode 100644 index 00000000..a9fb4a07 --- /dev/null +++ b/apps/backend/src/app/migrations/1732887680122-NewAlertStructure.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NewAlertStructure1732887680122 implements MigrationInterface { + name = 'NewAlertStructure1732887680122' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "SizeAlert" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "size" numeric(20,6) NOT NULL, "referenceSize" numeric(20,6) NOT NULL, "alertTypeId" uuid NOT NULL, "backupId" character varying NOT NULL, CONSTRAINT "PK_b2c2090de21927ad3b81ea1ec5f" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "SizeAlert" ADD CONSTRAINT "FK_d359f243b4f8d5c72e8134473db" FOREIGN KEY ("alertTypeId") REFERENCES "AlertType"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "SizeAlert" ADD CONSTRAINT "FK_de933919990c72fd1a912da802c" FOREIGN KEY ("backupId") REFERENCES "BackupData"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "SizeAlert" DROP CONSTRAINT "FK_de933919990c72fd1a912da802c"`); + await queryRunner.query(`ALTER TABLE "SizeAlert" DROP CONSTRAINT "FK_d359f243b4f8d5c72e8134473db"`); + await queryRunner.query(`DROP TABLE "SizeAlert"`); + } + +} diff --git a/apps/backend/src/app/utils/mail/mail.service.spec.ts b/apps/backend/src/app/utils/mail/mail.service.spec.ts index 99950a8f..eda9363f 100644 --- a/apps/backend/src/app/utils/mail/mail.service.spec.ts +++ b/apps/backend/src/app/utils/mail/mail.service.spec.ts @@ -2,9 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { MailService } from './mail.service'; import { MailerService } from '@nestjs-modules/mailer'; import { ConfigService } from '@nestjs/config'; -import { AlertEntity } from '../../alerting/entity/alert.entity'; -import { AlertType } from '../../alerting/dto/alertType'; import { BackupType } from '../../backupData/dto/backupType'; +import { SizeAlertEntity } from '../../alerting/entity/alerts/sizeAlert.entity'; +import { SeverityType } from '../../alerting/dto/severityType'; jest.mock('path', () => ({ resolve: jest.fn().mockReturnValue('mocked/path/to/logo.png'), @@ -45,11 +45,17 @@ describe('MailService', () => { }); it('should send alert mail', async () => { - const alert: AlertEntity = { + const alert: SizeAlertEntity = { id: 'alert-id', - type: AlertType.SIZE_DECREASED, - value: 100, - referenceValue: 200, + alertType: { + id: 'alert-type-id', + name: 'SIZE_ALERT', + severity: SeverityType.CRITICAL, + user_active: true, + master_active: true, + }, + size: 100, + referenceSize: 200, backup: { id: 'backup-id', sizeMB: 100, diff --git a/apps/backend/src/app/utils/mail/mail.service.ts b/apps/backend/src/app/utils/mail/mail.service.ts index 396f3de4..6a2b7b17 100644 --- a/apps/backend/src/app/utils/mail/mail.service.ts +++ b/apps/backend/src/app/utils/mail/mail.service.ts @@ -2,8 +2,8 @@ import { MailerService } from '@nestjs-modules/mailer'; import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as path from 'path'; -import { AlertEntity } from '../../alerting/entity/alert.entity'; -import { AlertType } from '../../alerting/dto/alertType'; +import { Alert } from '../../alerting/entity/alerts/alert'; +import { SizeAlertEntity } from '../../alerting/entity/alerts/sizeAlert.entity'; @Injectable() export class MailService { @@ -19,7 +19,7 @@ export class MailService { } } - async sendAlertMail(alert: AlertEntity) { + async sendAlertMail(alert: Alert) { const receivers = this.configService.getOrThrow('MAILING_LIST').split(',') || []; @@ -28,36 +28,46 @@ export class MailService { let valueColumnName = ''; let referenceValueColumnName = ''; let percentage: string = 'Infinity'; - switch (alert.type) { - case AlertType.SIZE_DECREASED: - if (alert.referenceValue !== 0) { - percentage = Math.floor( - (1 - alert.value / alert.referenceValue) * 100 - ).toString(); + let value: string = '-'; + let referenceValue: string = '-'; + switch (alert.alertType.name) { + //TODO: create constant for that + case 'SIZE_ALERT': + const sizeAlert = alert as SizeAlertEntity; + if (sizeAlert.size < sizeAlert.referenceSize) { + if (sizeAlert.referenceSize !== 0) { + percentage = Math.floor( + (1 - sizeAlert.size / sizeAlert.referenceSize) * 100 + ).toString(); + } + valueColumnName = 'Size of backup'; + referenceValueColumnName = 'Size of previous backup'; + reason = `Size of latest Backup decreased by ${percentage} %`; + description = `Size of latest Backup decreased by ${percentage}% compared to the previous Backup. This could indicate a problem with the Backup.`; + value = sizeAlert.size.toString() + ' MB'; + referenceValue = sizeAlert.referenceSize.toString() + ' MB'; + break; + } else { + if (sizeAlert.referenceSize !== 0) { + percentage = Math.floor( + (sizeAlert.size / sizeAlert.referenceSize - 1) * 100 + ).toString(); + } + valueColumnName = 'Size of backup'; + referenceValueColumnName = 'Size of previous backup'; + reason = `Size of latest Backup increased by ${percentage} %`; + description = `Size of latest Backup increased by ${percentage}% compared to the previous Backup. This could indicate a problem with the Backup.`; + value = sizeAlert.size.toString() + ' MB'; + referenceValue = sizeAlert.referenceSize.toString() + ' MB'; + break; } - valueColumnName = 'Size of backup'; - referenceValueColumnName = 'Size of previous backup'; - reason = `Size of latest Backup decreased by ${percentage} %`; - description = `Size of latest Backup decreased by ${percentage}% compared to the previous Backup. This could indicate a problem with the Backup.`; - break; - case AlertType.SIZE_INCREASED: - if (alert.referenceValue !== 0) { - percentage = Math.floor( - (alert.value / alert.referenceValue - 1) * 100 - ).toString(); - } - valueColumnName = 'Size of backup'; - referenceValueColumnName = 'Size of previous backup'; - reason = `Size of latest Backup increased by ${percentage} %`; - description = `Size of latest Backup increased by ${percentage}% compared to the previous Backup. This could indicate a problem with the Backup.`; - break; } const context = { reason, description, - value: alert.value.toString() + ' MB', - referenceValue: alert.referenceValue.toString() + ' MB', + value, + referenceValue, valueColumnName, referenceValueColumnName, backupId: alert.backup.id, diff --git a/apps/frontend/src/app/alert/component/alert.component.css b/apps/frontend/src/app/alert/component/alert.component.css index b9a5ec28..62f617f4 100644 --- a/apps/frontend/src/app/alert/component/alert.component.css +++ b/apps/frontend/src/app/alert/component/alert.component.css @@ -38,11 +38,20 @@ border-left: 5px solid #f44336; } +.alert-blue { + background-color: #e3f2fd; + border-left: 5px solid #2196f3; +} + .alert cds-icon { margin-right: 10px; color: #f44336; } +.alert-blue cds-icon { + color: #2196f3; +} + .card-header { font-weight: bold; font-size: 1.8em; @@ -90,4 +99,9 @@ .alert-count cds-icon[shape="error-standard"] { stroke: #f44336; stroke-width: 2px; +} + +.alert-count cds-icon[shape="info-standard"] { + stroke: #2196f3; + stroke-width: 2px; } \ No newline at end of file diff --git a/apps/frontend/src/app/alert/component/alert.component.html b/apps/frontend/src/app/alert/component/alert.component.html index f8041905..a5e1ef2f 100644 --- a/apps/frontend/src/app/alert/component/alert.component.html +++ b/apps/frontend/src/app/alert/component/alert.component.html @@ -6,20 +6,26 @@ {{ criticalAlertsCount }} - + - {{ nonCriticalAlertsCount }} + {{ warningAlertsCount }} - + + + {{ warningAlertsCount }} + +
- + - {{ getAlertReason(alert) }}
@@ -41,6 +47,6 @@
\ No newline at end of file diff --git a/apps/frontend/src/app/alert/component/alert.component.ts b/apps/frontend/src/app/alert/component/alert.component.ts index 77339be2..d2725388 100644 --- a/apps/frontend/src/app/alert/component/alert.component.ts +++ b/apps/frontend/src/app/alert/component/alert.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { AlertServiceService } from '../service/alert-service.service'; -import { Alert } from '../../shared/types/alert'; +import { Alert, SizeAlert } from '../../shared/types/alert'; import { DatePipe } from '@angular/common'; -import { AlertType } from '../../shared/enums/alertType'; import { Subject, takeUntil } from 'rxjs'; +import { SeverityType } from '../../shared/enums/severityType'; @Component({ selector: 'app-alert', @@ -16,9 +16,8 @@ export class AlertComponent implements OnInit { alerts: Alert[] = []; criticalAlertsCount: number = 0; - nonCriticalAlertsCount: number = 0; - - criticalAlertTypes: AlertType[] = []; + warningAlertsCount: number = 0; + infoAlertsCount: number = 0; status: 'OK' | 'Warning' | 'Critical' = 'OK'; @@ -38,15 +37,19 @@ export class AlertComponent implements OnInit { .getAllAlerts(this.DAYS) .pipe(takeUntil(this.destroy$)) .subscribe((data: Alert[]) => { - const criticalAlerts = data.filter((alert) => - this.criticalAlertTypes.includes(alert.type) + const criticalAlerts = data.filter( + (alert) => alert.alertType.severity === SeverityType.CRITICAL + ); + const warningAlerts = data.filter( + (alert) => alert.alertType.severity === SeverityType.WARNING ); - const nonCriticalAlerts = data.filter( - (alert) => !this.criticalAlertTypes.includes(alert.type) + const infoAlerts = data.filter( + (alert) => alert.alertType.severity === SeverityType.INFO ); this.criticalAlertsCount = criticalAlerts.length; - this.nonCriticalAlertsCount = nonCriticalAlerts.length; - this.alerts = [...criticalAlerts, ...nonCriticalAlerts]; + this.warningAlertsCount = warningAlerts.length; + this.infoAlertsCount = infoAlerts.length; + this.alerts = [...criticalAlerts, ...warningAlerts, ...infoAlerts]; this.status = this.getStatus(); }); } @@ -62,37 +65,51 @@ export class AlertComponent implements OnInit { } getAlertClass(alert: Alert): string { - if (this.criticalAlertTypes.includes(alert.type)) { + if (alert.alertType.severity === SeverityType.CRITICAL) { return 'alert-red'; - } else { + } else if (alert.alertType.severity === SeverityType.WARNING) { return 'alert-yellow'; + } else { + return 'alert-blue'; } } getStatus() { - if (this.alerts.length === 0) { - return 'OK'; - } if ( - this.alerts.some((alert) => this.criticalAlertTypes.includes(alert.type)) + this.alerts.some( + (alert) => alert.alertType.severity === SeverityType.CRITICAL + ) ) { return 'Critical'; + } else if ( + this.alerts.some( + (alert) => alert.alertType.severity === SeverityType.WARNING + ) + ) { + return 'Warning'; } - return 'Warning'; + return 'OK'; } getAlertReason(alert: Alert) { let reason = ''; let percentage = 0; - switch (alert.type) { - case AlertType.SIZE_DECREASED: - percentage = Math.floor((1 - alert.value / alert.referenceValue) * 100); - reason = `Size of backup decreased`; - break; - case AlertType.SIZE_INCREASED: - percentage = Math.floor((alert.value / alert.referenceValue - 1) * 100); - reason = `Size of backup increased`; - break; + switch (alert.alertType.name) { + case 'SIZE_ALERT': + const sizeAlert = alert as SizeAlert; + if (sizeAlert.size - sizeAlert.referenceSize < 0) { + percentage = Math.floor( + (1 - sizeAlert.size / sizeAlert.referenceSize) * 100 + ); + reason = `Size of backup decreased`; + break; + } else { + percentage = Math.floor( + (sizeAlert.size / sizeAlert.referenceSize - 1) * 100 + ); + reason = `Size of backup increased`; + break; + } } return reason; } @@ -100,15 +117,22 @@ export class AlertComponent implements OnInit { getAlertDetails(alert: Alert) { let description = ''; let percentage = 0; - switch (alert.type) { - case AlertType.SIZE_DECREASED: - percentage = Math.floor((1 - alert.value / alert.referenceValue) * 100); - description = `Size of backup decreased by ${percentage}% compared to the previous backup. This could indicate a problem with the backup.`; - break; - case AlertType.SIZE_INCREASED: - percentage = Math.floor((alert.value / alert.referenceValue - 1) * 100); - description = `Size of backup increased by ${percentage}% compared to the previous backup. This could indicate a problem with the backup.`; - break; + switch (alert.alertType.name) { + case 'SIZE_ALERT': + const sizeAlert = alert as SizeAlert; + if (sizeAlert.size - sizeAlert.referenceSize < 0) { + percentage = Math.floor( + (1 - sizeAlert.size / sizeAlert.referenceSize) * 100 + ); + description = `Size of backup decreased by ${percentage}% compared to the previous backup. This could indicate a problem with the backup.`; + break; + } else { + percentage = Math.floor( + (sizeAlert.size / sizeAlert.referenceSize - 1) * 100 + ); + description = `Size of backup increased by ${percentage}% compared to the previous backup. This could indicate a problem with the backup.`; + break; + } } return description; } @@ -116,4 +140,6 @@ export class AlertComponent implements OnInit { formatDate(date: Date): string { return this.datePipe.transform(date, 'dd.MM.yyyy HH:mm') || ''; } + + protected readonly SeverityType = SeverityType; } diff --git a/apps/frontend/src/app/alert/service/alert-service.service.ts b/apps/frontend/src/app/alert/service/alert-service.service.ts index c447700d..44af7f9d 100644 --- a/apps/frontend/src/app/alert/service/alert-service.service.ts +++ b/apps/frontend/src/app/alert/service/alert-service.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { BASE_URL } from '../../shared/types/configuration'; import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { Alert } from '../../shared/types/alert'; @Injectable({ @@ -14,7 +14,7 @@ export class AlertServiceService { ) {} getAllAlerts(days?: number): Observable { - if(days) { + if (days) { return this.http.get(`${this.baseUrl}/alerting?days=${days}`); } return this.http.get(`${this.baseUrl}/alerting`); diff --git a/apps/frontend/src/app/shared/enums/alertType.ts b/apps/frontend/src/app/shared/enums/alertType.ts deleted file mode 100644 index 1967fc7a..00000000 --- a/apps/frontend/src/app/shared/enums/alertType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Possible types of alerts -export enum AlertType { - SIZE_INCREASED, - SIZE_DECREASED, -} diff --git a/apps/frontend/src/app/shared/enums/severityType.ts b/apps/frontend/src/app/shared/enums/severityType.ts new file mode 100644 index 00000000..73356482 --- /dev/null +++ b/apps/frontend/src/app/shared/enums/severityType.ts @@ -0,0 +1,5 @@ +export enum SeverityType { + INFO = 'INFO', + WARNING = 'WARNING', + CRITICAL = 'CRITICAL', +} diff --git a/apps/frontend/src/app/shared/types/alert.ts b/apps/frontend/src/app/shared/types/alert.ts index ac1fd6f1..8754b92a 100644 --- a/apps/frontend/src/app/shared/types/alert.ts +++ b/apps/frontend/src/app/shared/types/alert.ts @@ -1,10 +1,13 @@ import { Backup } from './backup'; -import { AlertType } from '../enums/alertType'; +import { AlertType } from './alertType'; export interface Alert { id: string; - type: AlertType; - value: number; - referenceValue: number; + alertType: AlertType; backup: Backup; } + +export interface SizeAlert extends Alert { + size: number; + referenceSize: number; +} diff --git a/apps/frontend/src/app/shared/types/alertType.ts b/apps/frontend/src/app/shared/types/alertType.ts new file mode 100644 index 00000000..4b5a3b61 --- /dev/null +++ b/apps/frontend/src/app/shared/types/alertType.ts @@ -0,0 +1,10 @@ +// Possible types of alerts +import { SeverityType } from '../enums/severityType'; + +export class AlertType { + id!: string; + name!: string; + severity!: SeverityType; + user_active!: boolean; + master_active!: boolean; +}