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
74 changes: 74 additions & 0 deletions backend/services/__tests__/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { logger, queryLogs, clearLogBuffer, runWithLogContext } from '../logging';

describe('backend logging service', () => {
beforeEach(() => {
clearLogBuffer();
jest.restoreAllMocks();
});

it('records structured log entries and includes service/module metadata', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
logger.info('Service started', { feature: 'billing' });

const entries = queryLogs({ text: 'Service started' });
expect(entries).toHaveLength(1);
expect(entries[0]).toMatchObject({
message: 'Service started',
level: 'info',
service: 'subtrackr-backend',
module: 'backend',
meta: { feature: 'billing' },
});
expect(entries[0].timestamp).toBeDefined();
expect(spy).toHaveBeenCalled();
});

it('propagates correlation ids across async boundaries', async () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
await runWithLogContext('corr-id-123', async () => {
logger.debug('Async operation started', { userId: 'user-1' });
await new Promise((resolve) => setTimeout(resolve, 0));
logger.info('Async operation completed');
});

const entries = queryLogs({ correlationId: 'corr-id-123' });
expect(entries.length).toBe(2);
expect(entries[0].correlationId).toBe('corr-id-123');
expect(entries[1].correlationId).toBe('corr-id-123');
expect(spy).toHaveBeenCalledTimes(2);
});

it('redacts sensitive fields in metadata', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
logger.warn('User data access', {
userId: 'user-2',
email: 'jane@doe.com',
password: 'secret123',
cardNumber: '4111111111111111',
});

const entries = queryLogs({ level: 'warn' });
expect(entries).toHaveLength(1);
expect(entries[0].meta).toEqual({
userId: 'user-2',
email: '[REDACTED]',
password: '[REDACTED]',
cardNumber: '[REDACTED]',
});
expect(spy).toHaveBeenCalled();
});

it('filters the log buffer by module and text', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
logger.info('First message', { detail: 'one' });
const childLogger = logger.child('parser');
childLogger.error('Parse failed', { line: 42 });

const entries = queryLogs({ module: 'parser', text: 'Parse failed' });
expect(entries).toHaveLength(1);
expect(entries[0].module).toContain('parser');
expect(entries[0].message).toBe('Parse failed');
expect(entries[0].meta).toEqual({ line: 42 });
expect(spy).toHaveBeenCalledTimes(2);
});
});
25 changes: 25 additions & 0 deletions backend/services/__tests__/loggingDashboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { clearLogBuffer, logger } from '../logging';
import { getLogDashboard } from '../loggingDashboard';

describe('logging dashboard service', () => {
beforeEach(() => {
clearLogBuffer();
jest.restoreAllMocks();
});

it('returns filtered log entries and total count', () => {
logger.info('First entry', { feature: 'dashboard' });
logger.error('Second entry', { feature: 'dashboard', error: 'boom' });
logger.debug('Debug entry', { feature: 'dashboard' });

const result = getLogDashboard({ level: 'error' }, 20);

expect(result.total).toBe(1);
expect(result.entries).toHaveLength(1);
expect(result.entries[0]).toMatchObject({
level: 'error',
message: 'Second entry',
meta: { feature: 'dashboard', error: 'boom' },
});
});
});
7 changes: 6 additions & 1 deletion backend/services/alerting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Channels are pluggable; add as many as needed.
*/

import { logger } from './logging';
import type { Alert, AlertChannelConfig } from './types';

export interface AlertDispatcher {
Expand All @@ -15,7 +16,11 @@ class ConsoleDispatcher implements AlertDispatcher {
async send(alert: Alert): Promise<void> {
const prefix =
alert.severity === 'critical' ? '🚨' : alert.severity === 'warning' ? '⚠️' : 'ℹ️';
console.log(`${prefix} [${alert.severity.toUpperCase()}] ${alert.title}: ${alert.message}`);
logger.info(`${prefix} [${alert.severity.toUpperCase()}] ${alert.title}: ${alert.message}`, {
correlationId: alert.correlationId,
alertId: alert.id,
severity: alert.severity,
});
}
}

Expand Down
10 changes: 8 additions & 2 deletions backend/services/billing/metering_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ export interface UsageMetric {
timestamp: Date;
}

import { logger } from '../logging';

export class MeteringService {
private thresholdAlerts = [0.8, 1.0, 1.2]; // 80%, 100%, 120%

async recordUsage(metric: UsageMetric): Promise<void> {
// Low-latency metering pipeline integration
console.log(`Recorded ${metric.amount} for ${metric.metricType}`);
logger.info('Recorded usage metric', {
userId: metric.userId,
metricType: metric.metricType,
amount: metric.amount,
});

await this.checkThresholds(metric.userId);
}

async checkThresholds(userId: string): Promise<void> {
// Check usage against thresholds and trigger alerts
console.log(`Checked thresholds for ${userId}`);
logger.debug('Checked thresholds for user usage', { userId });
}

async calculateOverage(userId: string): Promise<number> {
Expand Down
9 changes: 5 additions & 4 deletions backend/services/gdpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './encryption';
import { keyManager } from './keyManager';
import { piiAuditService } from './piiAudit';
import { logger } from './logging';

export interface UserConsent {
analytics: boolean;
Expand Down Expand Up @@ -47,7 +48,7 @@ function generateExportId(): string {
export const exportUserData = async (userId: string): Promise<ExportResult> => {
await ensureEncryptionInitialized();

console.log(`Exporting data for user: ${userId}`);
logger.info('Exporting user data', { userId });

const userData = {
profile: { id: userId, email: 'user@example.com', name: 'John Doe', registeredAt: '2026-01-01' },
Expand Down Expand Up @@ -98,7 +99,7 @@ export const deleteUserData = async (
): Promise<DeletionResult> => {
await ensureEncryptionInitialized();

console.log(`Processing deletion for user: ${userId} (Permanent: ${permanent})`);
logger.info('Processing user deletion', { userId, permanent });

if (!permanent) {
return anonymizeUserData(userId) as Promise<DeletionResult>;
Expand All @@ -119,7 +120,7 @@ export const deleteUserData = async (
export const anonymizeUserData = async (userId: string): Promise<AnonymizationResult> => {
await ensureEncryptionInitialized();

console.log(`Anonymizing data for user: ${userId}`);
logger.info('Anonymizing user data', { userId });

const fields = ['email', 'name', 'phoneNumber', 'address', 'businessName', 'recipientEmail'];

Expand All @@ -144,7 +145,7 @@ export const updateConsent = async (
timestamp: new Date().toISOString(),
};

console.log(`Consent updated for ${userId}:`, newConsent);
logger.info('Consent updated', { userId, newConsent });

return newConsent;
};
Expand Down
1 change: 1 addition & 0 deletions backend/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { AuditService } from './auditService';
export { CampaignService } from './campaignService';
export { DunningService, dunningService } from './dunningService';
export { getLogDashboard, LogQueryFilter, LogDashboardPage } from './loggingDashboard';
export { PricingService } from './pricingService';
export { OracleMonitorService, oracleMonitorService } from './oracleMonitorService';
export { RateLimitingService, rateLimitingService } from './rateLimitingService';
Expand Down
Loading