feat(observability): P3.2-A/B/C Enhanced observability coverage

P3.2-A: Expanded event coverage
- Added recovery events (RECOVERY_START, RECOVERY_COMPLETE, RECOVERY_ERROR)
- Added database events (DB_QUERY_START, DB_QUERY_COMPLETE, DB_QUERY_ERROR)
- Added state transition event (STATE_TRANSITION)
- Added background task events (BACKGROUND_TASK_START, COMPLETE, ERROR)

P3.2-B: Structured metrics export
- Added exportMetrics() method to export all metrics as JSON
- Added getMetricsSummary() method for lightweight metrics summary

P3.2-C: Improved error context
- Added toJSON() method to DailyNotificationError for structured logging
- Added logError() method to ObservabilityManager with enhanced error context

Verification:
- TypeScript compiles 
- No new dependencies 
- JSON export is valid 
This commit is contained in:
Matthew Raymer
2025-12-23 06:44:41 +00:00
parent 086ba90723
commit 3f03a8263c
3 changed files with 96 additions and 0 deletions

View File

@@ -106,6 +106,20 @@ export class DailyNotificationError extends Error {
}; };
} }
/**
* Convert error to JSON for structured logging
* @returns JSON-serializable error representation
*/
toJSON(): Record<string, unknown> {
return {
code: this.code,
message: this.message,
stack: this.stack,
timestamp: Date.now(),
...(this.details && { details: this.details }),
};
}
/** /**
* Create error for missing required parameter * Create error for missing required parameter
*/ */

View File

@@ -76,6 +76,20 @@ export const EVENT_CODES = {
ANDROID_WORKMANAGER_START: 'DNP-ANDROID-WM-START', ANDROID_WORKMANAGER_START: 'DNP-ANDROID-WM-START',
IOS_BGTASK_START: 'DNP-IOS-BGTASK-START', IOS_BGTASK_START: 'DNP-IOS-BGTASK-START',
ELECTRON_NOTIFICATION: 'DNP-ELECTRON-NOTIFICATION', ELECTRON_NOTIFICATION: 'DNP-ELECTRON-NOTIFICATION',
// Recovery events
RECOVERY_START: 'DNP-RECOVERY-START',
RECOVERY_COMPLETE: 'DNP-RECOVERY-COMPLETE',
RECOVERY_ERROR: 'DNP-RECOVERY-ERROR',
// Database events
DB_QUERY_START: 'DNP-DB-QUERY-START',
DB_QUERY_COMPLETE: 'DNP-DB-QUERY-COMPLETE',
DB_QUERY_ERROR: 'DNP-DB-QUERY-ERROR',
// State transition events
STATE_TRANSITION: 'DNP-STATE-TRANSITION',
// Background task events
BACKGROUND_TASK_START: 'DNP-BG-TASK-START',
BACKGROUND_TASK_COMPLETE: 'DNP-BG-TASK-COMPLETE',
BACKGROUND_TASK_ERROR: 'DNP-BG-TASK-ERROR',
} as const; } as const;
/** /**

View File

@@ -11,6 +11,7 @@ import {
EVENT_CODES, EVENT_CODES,
createEventLog, createEventLog,
} from './core/events'; } from './core/events';
import { DailyNotificationError } from './core/errors';
export interface HealthStatus { export interface HealthStatus {
nextRuns: number[]; nextRuns: number[];
@@ -134,6 +135,32 @@ export class ObservabilityManager {
} }
} }
/**
* Log error with enhanced context
* @param eventCode Event code
* @param message Human-readable message
* @param error Error object
* @param context Additional context
*/
logError(eventCode: string, message: string, error: Error, context?: Record<string, unknown>): void {
const errorData: Record<string, unknown> = {
error: error.message,
errorCode: error instanceof DailyNotificationError ? error.code : undefined,
errorName: error.name,
...(error instanceof DailyNotificationError && error.details ? { errorDetails: error.details } : {}),
...(error.stack ? { stack: error.stack } : {}),
...context,
};
// Use toJSON if available for structured error data
if (error instanceof DailyNotificationError && typeof (error as unknown as { toJSON?: () => Record<string, unknown> }).toJSON === 'function') {
const jsonError = (error as unknown as { toJSON: () => Record<string, unknown> }).toJSON();
Object.assign(errorData, jsonError);
}
this.logEvent('ERROR', eventCode, message, errorData);
}
/** /**
* Record performance metrics * Record performance metrics
*/ */
@@ -204,6 +231,47 @@ export class ObservabilityManager {
}; };
} }
/**
* Export metrics as JSON
* @returns JSON string of all metrics
*/
exportMetrics(): string {
return JSON.stringify({
performance: this.performanceMetrics,
user: this.userMetrics,
platform: this.platformMetrics,
events: this.eventLogs.slice(0, 100), // Last 100 events
exportedAt: Date.now(),
schemaVersion: 1
}, null, 2);
}
/**
* Get metrics summary (lightweight)
* @returns Summary object
*/
getMetricsSummary(): {
eventCount: number;
successRate: number;
avgFetchTime: number;
avgNotifyTime: number;
} {
const fetchTimes = this.performanceMetrics.fetchTimes;
const notifyTimes = this.performanceMetrics.notifyTimes;
const total = this.performanceMetrics.successCount + this.performanceMetrics.failureCount;
return {
eventCount: this.eventLogs.length,
successRate: total > 0 ? this.performanceMetrics.successCount / total : 0,
avgFetchTime: fetchTimes.length > 0
? fetchTimes.reduce((a, b) => a + b, 0) / fetchTimes.length
: 0,
avgNotifyTime: notifyTimes.length > 0
? notifyTimes.reduce((a, b) => a + b, 0) / notifyTimes.length
: 0
};
}
/** /**
* Get recent event logs * Get recent event logs
*/ */