Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,12 @@ IDEMPOTENCY_TTL_SECONDS=86400
# Segment write key (for analytics, optional)
SEGMENT_WRITE_KEY=

# Audit log retention period in days (default: 730 = 2 years)
AUDIT_LOG_RETENTION_DAYS=730

# Analytics event retention period in days (default: 365 = 1 year)
ANALYTICS_RETENTION_DAYS=365

# ─────────────────────────────────────────────────────────────────────────────
# 22. CDN CONFIGURATION
# ─────────────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AnalyticsEvent } from './entities/event.entity';
import { EventBatchingService } from './services/event-batching.service';
import { EventValidationService } from './services/event-validation.service';
import { EventTrackingSDK } from './sdk/event-tracking.sdk';
import { AnalyticsRetentionTask } from './tasks/analytics-retention.task';

@Module({
imports: [
Expand All @@ -24,6 +25,7 @@ import { EventTrackingSDK } from './sdk/event-tracking.sdk';
EventBatchingService,
EventValidationService,
EventTrackingSDK,
AnalyticsRetentionTask,
{ provide: APP_INTERCEPTOR, useClass: FingerprintInterceptor },
],
controllers: [AnalyticsController],
Expand Down
65 changes: 65 additions & 0 deletions src/analytics/tasks/analytics-retention.task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

Check warning on line 2 in src/analytics/tasks/analytics-retention.task.ts

View workflow job for this annotation

GitHub Actions / validate

'CronExpression' is defined but never used. Allowed unused vars must match /^_/u
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DeleteResult } from 'typeorm';
import { Counter } from 'prom-client';
import { AnalyticsEvent } from '../entities/event.entity';
import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AnalyticsRetentionTask {
private readonly logger = new Logger(AnalyticsRetentionTask.name);
private readonly retentionDays: number;
private readonly batchSize = 1000;
private deletedCounter: Counter<'table'>;

constructor(
@InjectRepository(AnalyticsEvent)
private readonly eventRepository: Repository<AnalyticsEvent>,
private readonly configService: ConfigService,
private readonly metrics: MetricsCollectionService,
) {
this.retentionDays = this.configService.get<number>('ANALYTICS_RETENTION_DAYS', 365);
const registry = this.metrics.getRegistry();
const prom = require('prom-client');

Check failure on line 25 in src/analytics/tasks/analytics-retention.task.ts

View workflow job for this annotation

GitHub Actions / validate

A `require()` style import is forbidden
this.deletedCounter =
(registry.getSingleMetric('deleted_count') as Counter<'table'>) ??
new prom.Counter({
name: 'deleted_count',
help: 'Number of rows deleted by data retention policies',
labelNames: ['table'] as const,
registers: [registry],
});
}

@Cron('30 2 * * *')
async handleDailyRetention(): Promise<void> {
this.logger.log('Starting daily analytics event retention policy...');
let totalDeleted = 0;
try {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - this.retentionDays);

let deleted = 0;
do {
const result: DeleteResult = await this.eventRepository
.createQueryBuilder()
.delete()
.from(AnalyticsEvent)
.where('timestamp < :cutoff', { cutoff })
.limit(this.batchSize)
.execute();
deleted = result.affected || 0;
totalDeleted += deleted;
} while (deleted >= this.batchSize);

this.deletedCounter.inc({ table: 'analytics_events' }, totalDeleted);
this.logger.log(
`Daily analytics retention policy completed. Deleted ${totalDeleted} old events.`,
);
} catch (error) {
this.logger.error('Failed to apply analytics retention policy:', error);
}
}
}
2 changes: 2 additions & 0 deletions src/audit-log/audit-log.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuditQueryService } from './services/audit-query.service';
import { AuditReportingService } from './services/audit-reporting.service';
import { AuditExportService } from './services/audit-export.service';
import { AuditRetentionTask } from './tasks/audit-retention.task';
import { MetricsCollectionService } from '../monitoring/metrics/metrics-collection.service';

/**
* Audit Log Module
Expand All @@ -26,6 +27,7 @@ import { AuditRetentionTask } from './tasks/audit-retention.task';
AuditExportService,
AuditRetentionTask,
AuditLogService,
MetricsCollectionService,
],
exports: [AuditLogService],
})
Expand Down
66 changes: 51 additions & 15 deletions src/audit-log/tasks/audit-retention.task.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,69 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DeleteResult } from 'typeorm';
import { Counter } from 'prom-client';
import { AuditLog } from '../audit-log.entity';
import { AuditLogService } from '../audit-log.service';
import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service';
import { ConfigService } from '@nestjs/config';

/**
* Provides audit Retention Task behavior.
*/
@Injectable()
export class AuditRetentionTask {
private readonly logger = new Logger(AuditRetentionTask.name);
constructor(private readonly auditLogService: AuditLogService) {}
/**
* Run retention policy daily at 2 AM
*/
private readonly retentionDays: number;
private readonly batchSize = 1000;
private deletedCounter: Counter<'table'>;

constructor(
@InjectRepository(AuditLog)
private readonly auditLogRepo: Repository<AuditLog>,
private readonly auditLogService: AuditLogService,
private readonly configService: ConfigService,
private readonly metrics: MetricsCollectionService,
) {
this.retentionDays = this.configService.get<number>('AUDIT_LOG_RETENTION_DAYS', 730);
const registry = this.metrics.getRegistry();
const prom = require('prom-client');

Check failure on line 27 in src/audit-log/tasks/audit-retention.task.ts

View workflow job for this annotation

GitHub Actions / validate

A `require()` style import is forbidden
this.deletedCounter =
(registry.getSingleMetric('deleted_count') as Counter<'table'>) ??
new prom.Counter({
name: 'deleted_count',
help: 'Number of rows deleted by data retention policies',
labelNames: ['table'] as const,
registers: [registry],
});
}

@Cron(CronExpression.EVERY_DAY_AT_2AM)
async handleDailyRetention(): Promise<void> {
this.logger.log('Starting daily audit log retention policy...');
let totalDeleted = 0;
try {
const deletedCount = await this.auditLogService.applyRetentionPolicy();
this.logger.log(`Daily retention policy completed. Deleted ${deletedCount} old audit logs.`);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - this.retentionDays);

let deleted = 0;
do {
const result: DeleteResult = await this.auditLogRepo
.createQueryBuilder()
.delete()
.from(AuditLog)
.where('timestamp < :cutoff', { cutoff })
.limit(this.batchSize)
.execute();
deleted = result.affected || 0;
totalDeleted += deleted;
} while (deleted >= this.batchSize);

this.deletedCounter.inc({ table: 'audit_logs' }, totalDeleted);
this.logger.log(`Daily retention policy completed. Deleted ${totalDeleted} old audit logs.`);
} catch (error) {
this.logger.error('Failed to apply retention policy:', error);
}
}
/**
* Generate weekly report every Monday at 3 AM
*/
@Cron('0 3 * * 1') // Every Monday at 3 AM

@Cron('0 3 * * 1')
async handleWeeklyReport(): Promise<void> {
this.logger.log('Generating weekly audit report...');
try {
Expand All @@ -38,8 +76,6 @@
criticalEvents: report.eventsBySeverity['CRITICAL'] || 0,
errorEvents: report.eventsBySeverity['ERROR'] || 0,
});
// In a real implementation, you might send this report via email
// or store it for compliance purposes
} catch (error) {
this.logger.error('Failed to generate weekly report:', error);
}
Expand Down
4 changes: 4 additions & 0 deletions src/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export const envValidationSchema = Joi.object({
// Segment Analytics
SEGMENT_WRITE_KEY: Joi.string().optional(),

// Data Retention
AUDIT_LOG_RETENTION_DAYS: Joi.number().integer().min(1).default(730),
ANALYTICS_RETENTION_DAYS: Joi.number().integer().min(1).default(365),

// Circuit Breaker Configuration
CIRCUIT_BREAKER_TIMEOUT_MS: Joi.number().integer().min(100).default(3000),
CIRCUIT_BREAKER_ERROR_THRESHOLD: Joi.number().integer().min(1).max(100).default(50),
Expand Down
5 changes: 5 additions & 0 deletions src/config/retention.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export const retentionConfig = registerAs('retention', () => ({
*/
notificationRetentionDays: parseInt(process.env.RETENTION_NOTIFICATION_DAYS || '30', 10),

/**
* Retention period for analytics events in days.
*/
analyticsRetentionDays: parseInt(process.env.ANALYTICS_RETENTION_DAYS || '365', 10),

/**
* Whether to archive data before purging.
*/
Expand Down
Loading