Skip to content

Commit 7cbbd12

Browse files
Merge pull request #931 from Adeyemi-cmd/Add_data_retention
feat(data-retention): add automated pruning for audit logs and analyt…
2 parents 5517fec + e826b0c commit 7cbbd12

8 files changed

Lines changed: 156 additions & 20 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,12 @@ IDEMPOTENCY_TTL_SECONDS=86400
474474
# Segment write key (for analytics, optional)
475475
SEGMENT_WRITE_KEY=
476476

477+
# Audit log retention period in days (default: 730 = 2 years)
478+
AUDIT_LOG_RETENTION_DAYS=730
479+
480+
# Analytics event retention period in days (default: 365 = 1 year)
481+
ANALYTICS_RETENTION_DAYS=365
482+
477483
# ─────────────────────────────────────────────────────────────────────────────
478484
# 22. CDN CONFIGURATION
479485
# ─────────────────────────────────────────────────────────────────────────────

src/analytics/analytics.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AnalyticsEvent } from './entities/event.entity';
1111
import { EventBatchingService } from './services/event-batching.service';
1212
import { EventValidationService } from './services/event-validation.service';
1313
import { EventTrackingSDK } from './sdk/event-tracking.sdk';
14+
import { AnalyticsRetentionTask } from './tasks/analytics-retention.task';
1415

1516
@Module({
1617
imports: [
@@ -24,6 +25,7 @@ import { EventTrackingSDK } from './sdk/event-tracking.sdk';
2425
EventBatchingService,
2526
EventValidationService,
2627
EventTrackingSDK,
28+
AnalyticsRetentionTask,
2729
{ provide: APP_INTERCEPTOR, useClass: FingerprintInterceptor },
2830
],
2931
controllers: [AnalyticsController],
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { Cron } from '@nestjs/schedule';
3+
import { InjectRepository } from '@nestjs/typeorm';
4+
import { Repository, LessThan } from 'typeorm';
5+
import { Counter } from 'prom-client';
6+
import { AnalyticsEvent } from '../entities/event.entity';
7+
import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service';
8+
import { ConfigService } from '@nestjs/config';
9+
10+
@Injectable()
11+
export class AnalyticsRetentionTask {
12+
private readonly logger = new Logger(AnalyticsRetentionTask.name);
13+
private readonly retentionDays: number;
14+
private readonly batchSize = 1000;
15+
private deletedCounter: Counter<'table'>;
16+
17+
constructor(
18+
@InjectRepository(AnalyticsEvent)
19+
private readonly eventRepository: Repository<AnalyticsEvent>,
20+
private readonly configService: ConfigService,
21+
private readonly metrics: MetricsCollectionService,
22+
) {
23+
this.retentionDays = this.configService.get<number>('ANALYTICS_RETENTION_DAYS', 365);
24+
const registry = this.metrics.getRegistry();
25+
this.deletedCounter =
26+
(registry.getSingleMetric('deleted_count') as Counter<'table'>) ??
27+
new Counter({
28+
name: 'deleted_count',
29+
help: 'Number of rows deleted by data retention policies',
30+
labelNames: ['table'] as const,
31+
registers: [registry],
32+
});
33+
}
34+
35+
@Cron('30 2 * * *')
36+
async handleDailyRetention(): Promise<void> {
37+
this.logger.log('Starting daily analytics event retention policy...');
38+
let totalDeleted = 0;
39+
try {
40+
const cutoff = new Date();
41+
cutoff.setDate(cutoff.getDate() - this.retentionDays);
42+
43+
let deleted = 0;
44+
do {
45+
const eventsToDelete = await this.eventRepository.find({
46+
select: ['id'],
47+
where: { timestamp: LessThan(cutoff) },
48+
take: this.batchSize,
49+
});
50+
51+
if (eventsToDelete.length === 0) {
52+
deleted = 0;
53+
break;
54+
}
55+
56+
const idValues = eventsToDelete.map((e) => e.id);
57+
const result = await this.eventRepository
58+
.createQueryBuilder()
59+
.delete()
60+
.from(AnalyticsEvent)
61+
.whereInIds(idValues)
62+
.execute();
63+
deleted = result.affected || 0;
64+
totalDeleted += deleted;
65+
} while (deleted >= this.batchSize);
66+
67+
this.deletedCounter.inc({ table: 'analytics_events' }, totalDeleted);
68+
this.logger.log(
69+
`Daily analytics retention policy completed. Deleted ${totalDeleted} old events.`,
70+
);
71+
} catch (error) {
72+
this.logger.error('Failed to apply analytics retention policy:', error);
73+
}
74+
}
75+
}

src/audit-log/audit-log.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AuditQueryService } from './services/audit-query.service';
77
import { AuditReportingService } from './services/audit-reporting.service';
88
import { AuditExportService } from './services/audit-export.service';
99
import { AuditRetentionTask } from './tasks/audit-retention.task';
10+
import { MetricsCollectionService } from '../monitoring/metrics/metrics-collection.service';
1011

1112
/**
1213
* Audit Log Module
@@ -26,6 +27,7 @@ import { AuditRetentionTask } from './tasks/audit-retention.task';
2627
AuditExportService,
2728
AuditRetentionTask,
2829
AuditLogService,
30+
MetricsCollectionService,
2931
],
3032
exports: [AuditLogService],
3133
})

src/audit-log/tasks/audit-retention.task.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,79 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { Cron, CronExpression } from '@nestjs/schedule';
3+
import { InjectRepository } from '@nestjs/typeorm';
4+
import { Repository, LessThan } from 'typeorm';
5+
import { Counter } from 'prom-client';
6+
import { AuditLog } from '../audit-log.entity';
37
import { AuditLogService } from '../audit-log.service';
8+
import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service';
9+
import { ConfigService } from '@nestjs/config';
410

5-
/**
6-
* Provides audit Retention Task behavior.
7-
*/
811
@Injectable()
912
export class AuditRetentionTask {
1013
private readonly logger = new Logger(AuditRetentionTask.name);
11-
constructor(private readonly auditLogService: AuditLogService) {}
12-
/**
13-
* Run retention policy daily at 2 AM
14-
*/
14+
private readonly retentionDays: number;
15+
private readonly batchSize = 1000;
16+
private deletedCounter: Counter<'table'>;
17+
18+
constructor(
19+
@InjectRepository(AuditLog)
20+
private readonly auditLogRepo: Repository<AuditLog>,
21+
private readonly auditLogService: AuditLogService,
22+
private readonly configService: ConfigService,
23+
private readonly metrics: MetricsCollectionService,
24+
) {
25+
this.retentionDays = this.configService.get<number>('AUDIT_LOG_RETENTION_DAYS', 730);
26+
const registry = this.metrics.getRegistry();
27+
this.deletedCounter =
28+
(registry.getSingleMetric('deleted_count') as Counter<'table'>) ??
29+
new Counter({
30+
name: 'deleted_count',
31+
help: 'Number of rows deleted by data retention policies',
32+
labelNames: ['table'] as const,
33+
registers: [registry],
34+
});
35+
}
36+
1537
@Cron(CronExpression.EVERY_DAY_AT_2AM)
1638
async handleDailyRetention(): Promise<void> {
1739
this.logger.log('Starting daily audit log retention policy...');
40+
let totalDeleted = 0;
1841
try {
19-
const deletedCount = await this.auditLogService.applyRetentionPolicy();
20-
this.logger.log(`Daily retention policy completed. Deleted ${deletedCount} old audit logs.`);
42+
const cutoff = new Date();
43+
cutoff.setDate(cutoff.getDate() - this.retentionDays);
44+
45+
let deleted = 0;
46+
do {
47+
const logsToDelete = await this.auditLogRepo.find({
48+
select: ['id'],
49+
where: { timestamp: LessThan(cutoff) },
50+
take: this.batchSize,
51+
});
52+
53+
if (logsToDelete.length === 0) {
54+
deleted = 0;
55+
break;
56+
}
57+
58+
const idValues = logsToDelete.map((l) => l.id);
59+
const result = await this.auditLogRepo
60+
.createQueryBuilder()
61+
.delete()
62+
.from(AuditLog)
63+
.whereInIds(idValues)
64+
.execute();
65+
deleted = result.affected || 0;
66+
totalDeleted += deleted;
67+
} while (deleted >= this.batchSize);
68+
69+
this.deletedCounter.inc({ table: 'audit_logs' }, totalDeleted);
70+
this.logger.log(`Daily retention policy completed. Deleted ${totalDeleted} old audit logs.`);
2171
} catch (error) {
2272
this.logger.error('Failed to apply retention policy:', error);
2373
}
2474
}
25-
/**
26-
* Generate weekly report every Monday at 3 AM
27-
*/
28-
@Cron('0 3 * * 1') // Every Monday at 3 AM
75+
76+
@Cron('0 3 * * 1')
2977
async handleWeeklyReport(): Promise<void> {
3078
this.logger.log('Generating weekly audit report...');
3179
try {
@@ -38,8 +86,6 @@ export class AuditRetentionTask {
3886
criticalEvents: report.eventsBySeverity['CRITICAL'] || 0,
3987
errorEvents: report.eventsBySeverity['ERROR'] || 0,
4088
});
41-
// In a real implementation, you might send this report via email
42-
// or store it for compliance purposes
4389
} catch (error) {
4490
this.logger.error('Failed to generate weekly report:', error);
4591
}

src/config/env.validation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ export const envValidationSchema = Joi.object({
158158
// Segment Analytics
159159
SEGMENT_WRITE_KEY: Joi.string().optional(),
160160

161+
// Data Retention
162+
AUDIT_LOG_RETENTION_DAYS: Joi.number().integer().min(1).default(730),
163+
ANALYTICS_RETENTION_DAYS: Joi.number().integer().min(1).default(365),
164+
161165
// Circuit Breaker Configuration
162166
CIRCUIT_BREAKER_TIMEOUT_MS: Joi.number().integer().min(100).default(3000),
163167
CIRCUIT_BREAKER_ERROR_THRESHOLD: Joi.number().integer().min(1).max(100).default(50),

src/config/retention.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export const retentionConfig = registerAs('retention', () => ({
1717
*/
1818
notificationRetentionDays: parseInt(process.env.RETENTION_NOTIFICATION_DAYS || '30', 10),
1919

20+
/**
21+
* Retention period for analytics events in days.
22+
*/
23+
analyticsRetentionDays: parseInt(process.env.ANALYTICS_RETENTION_DAYS || '365', 10),
24+
2025
/**
2126
* Whether to archive data before purging.
2227
*/

tsconfig.build.tsbuildinfo

Lines changed: 1 addition & 5 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)