- Add comprehensive structured logging utility (lib/utils/sync-logger.ts) - Log levels: DEBUG, INFO, WARN, ERROR - Automatic error categorization (NETWORK_ERROR, AUTH_ERROR, DATABASE_ERROR, etc.) - Phase tracking (INITIALIZING, FETCHING, MAPPING, VALIDATING, UPSERTING, DELETING, COMPLETING) - Contextual metadata (syncId, entityType, phase, duration, record counts) - Timing helpers for operations - Update sync-service.ts with structured logging - Replace console.log/error with structured logger - Consistent error categorization across all error paths - Fix swallowed errors in sync history updates - Add child loggers for entity-specific operations - Update entity-sync.ts with structured logging - Complete phase tracking throughout sync lifecycle - Detailed context for warnings and errors - Update chunked sync methods with structured logging - Update picklist sync methods (issue types, sub-issue types) - Better debugging info with timing and sample data - Add sync failure analysis script (scripts/analyze-sync-failures.ts) - Query sync history by date range, entity type, or status - Generate failure summaries by entity - Automatic error categorization - Calculate success rates and statistics - Display detailed sync records with timing - Add comprehensive documentation (docs/SYNC_LOGGING_IMPROVEMENTS.md) - Usage guide and examples - Migration guide for developers - Before/after comparisons This addresses inconsistent logging and improves debugging capabilities for sync operations.
360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
/**
|
|
* Sync Failure Analysis Script
|
|
* Query and analyze sync_history for failures and patterns
|
|
* Run with: npx tsx scripts/analyze-sync-failures.ts [options]
|
|
*/
|
|
|
|
import { config } from 'dotenv';
|
|
import { resolve } from 'path';
|
|
import postgresClient from '../lib/services/postgres-client';
|
|
|
|
// Load environment variables
|
|
config({ path: resolve(__dirname, '../.env.local') });
|
|
|
|
interface SyncHistoryRecord {
|
|
id: number;
|
|
entity_type: string;
|
|
sync_type: string;
|
|
status: string;
|
|
started_at: Date;
|
|
completed_at: Date | null;
|
|
records_added: number;
|
|
records_updated: number;
|
|
records_deleted: number;
|
|
error_message: string | null;
|
|
triggered_by: string;
|
|
}
|
|
|
|
interface FailureSummary {
|
|
entity_type: string;
|
|
failure_count: number;
|
|
last_failure: Date;
|
|
common_errors: string[];
|
|
}
|
|
|
|
/**
|
|
* Parse command line arguments
|
|
*/
|
|
function parseArgs(): {
|
|
days: number;
|
|
entity?: string;
|
|
status?: string;
|
|
showAll: boolean;
|
|
} {
|
|
const args = process.argv.slice(2);
|
|
const options = {
|
|
days: 7,
|
|
entity: undefined as string | undefined,
|
|
status: undefined as string | undefined,
|
|
showAll: false,
|
|
};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--days' || arg === '-d') {
|
|
options.days = parseInt(args[++i]);
|
|
} else if (arg === '--entity' || arg === '-e') {
|
|
options.entity = args[++i];
|
|
} else if (arg === '--status' || arg === '-s') {
|
|
options.status = args[++i];
|
|
} else if (arg === '--all' || arg === '-a') {
|
|
options.showAll = true;
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
console.log(`
|
|
Sync Failure Analysis Script
|
|
|
|
Usage: npx tsx scripts/analyze-sync-failures.ts [options]
|
|
|
|
Options:
|
|
-d, --days <number> Number of days to look back (default: 7)
|
|
-e, --entity <type> Filter by entity type
|
|
-s, --status <status> Filter by status (failed, completed, started)
|
|
-a, --all Show all records, not just failures
|
|
-h, --help Show this help message
|
|
|
|
Examples:
|
|
# Show failures from last 7 days
|
|
npx tsx scripts/analyze-sync-failures.ts
|
|
|
|
# Show all syncs from yesterday
|
|
npx tsx scripts/analyze-sync-failures.ts --days 1 --all
|
|
|
|
# Show ticket sync failures from last 30 days
|
|
npx tsx scripts/analyze-sync-failures.ts --days 30 --entity tickets
|
|
|
|
# Show all completed syncs
|
|
npx tsx scripts/analyze-sync-failures.ts --status completed --all
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Query sync history
|
|
*/
|
|
async function querySyncHistory(
|
|
days: number,
|
|
entity?: string,
|
|
status?: string
|
|
): Promise<SyncHistoryRecord[]> {
|
|
const params: any[] = [];
|
|
let query = `
|
|
SELECT
|
|
id,
|
|
entity_type,
|
|
sync_type,
|
|
status,
|
|
started_at,
|
|
completed_at,
|
|
records_added,
|
|
records_updated,
|
|
records_deleted,
|
|
error_message,
|
|
triggered_by
|
|
FROM sync_history
|
|
WHERE started_at >= NOW() - INTERVAL '${days} days'
|
|
`;
|
|
|
|
if (entity) {
|
|
params.push(entity);
|
|
query += ` AND entity_type = $${params.length}`;
|
|
}
|
|
|
|
if (status) {
|
|
params.push(status);
|
|
query += ` AND status = $${params.length}`;
|
|
}
|
|
|
|
query += ' ORDER BY started_at DESC';
|
|
|
|
const result = await postgresClient.query<SyncHistoryRecord>(query, params);
|
|
return result.rows;
|
|
}
|
|
|
|
/**
|
|
* Get failure summary by entity
|
|
*/
|
|
async function getFailureSummary(days: number): Promise<FailureSummary[]> {
|
|
const query = `
|
|
SELECT
|
|
entity_type,
|
|
COUNT(*) as failure_count,
|
|
MAX(started_at) as last_failure,
|
|
ARRAY_AGG(DISTINCT SUBSTRING(error_message, 1, 100)) as common_errors
|
|
FROM sync_history
|
|
WHERE status = 'failed'
|
|
AND started_at >= NOW() - INTERVAL '${days} days'
|
|
GROUP BY entity_type
|
|
ORDER BY failure_count DESC, last_failure DESC
|
|
`;
|
|
|
|
const result = await postgresClient.query<any>(query);
|
|
return result.rows.map(row => ({
|
|
entity_type: row.entity_type,
|
|
failure_count: parseInt(row.failure_count),
|
|
last_failure: row.last_failure,
|
|
common_errors: row.common_errors.filter((e: string | null) => e !== null),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Categorize error message
|
|
*/
|
|
function categorizeError(errorMessage: string | null): string {
|
|
if (!errorMessage) return 'UNKNOWN';
|
|
|
|
if (errorMessage.includes('NETWORK_ERROR')) return 'NETWORK';
|
|
if (errorMessage.includes('AUTH_ERROR')) return 'AUTH';
|
|
if (errorMessage.includes('RATE_LIMIT_ERROR')) return 'RATE_LIMIT';
|
|
if (errorMessage.includes('DATABASE_CONSTRAINT_ERROR')) return 'DB_CONSTRAINT';
|
|
if (errorMessage.includes('DATABASE_ERROR')) return 'DATABASE';
|
|
if (errorMessage.includes('API_ERROR')) return 'API';
|
|
if (errorMessage.includes('MAPPING_ERROR')) return 'MAPPING';
|
|
if (errorMessage.includes('VALIDATION_ERROR')) return 'VALIDATION';
|
|
|
|
return 'OTHER';
|
|
}
|
|
|
|
/**
|
|
* Format duration
|
|
*/
|
|
function formatDuration(startedAt: Date, completedAt: Date | null): string {
|
|
if (!completedAt) return 'N/A';
|
|
const duration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
|
|
const seconds = Math.floor(duration / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
if (minutes > 0) {
|
|
return `${minutes}m ${seconds % 60}s`;
|
|
}
|
|
return `${seconds}s`;
|
|
}
|
|
|
|
/**
|
|
* Main analysis function
|
|
*/
|
|
async function analyzeSyncFailures() {
|
|
console.log('🔍 Sync Failure Analysis\n');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
const options = parseArgs();
|
|
|
|
try {
|
|
// Test database connection
|
|
const isConnected = await postgresClient.testConnection();
|
|
if (!isConnected) {
|
|
console.error('❌ Database connection failed');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Get sync history
|
|
const records = await querySyncHistory(
|
|
options.days,
|
|
options.entity,
|
|
options.status || (options.showAll ? undefined : 'failed')
|
|
);
|
|
|
|
console.log(`📊 Analysis Period: Last ${options.days} day(s)`);
|
|
if (options.entity) {
|
|
console.log(`🎯 Entity Filter: ${options.entity}`);
|
|
}
|
|
if (options.status) {
|
|
console.log(`📌 Status Filter: ${options.status}`);
|
|
}
|
|
console.log(`📝 Total Records: ${records.length}\n`);
|
|
|
|
if (records.length === 0) {
|
|
console.log('✅ No sync records found for the specified criteria.\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Display failure summary
|
|
if (!options.entity && !options.showAll) {
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log('📈 FAILURE SUMMARY BY ENTITY');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
const summary = await getFailureSummary(options.days);
|
|
|
|
if (summary.length === 0) {
|
|
console.log('✅ No failures found in the specified period!\n');
|
|
} else {
|
|
summary.forEach(item => {
|
|
console.log(`Entity: ${item.entity_type}`);
|
|
console.log(` Failures: ${item.failure_count}`);
|
|
console.log(` Last Failure: ${new Date(item.last_failure).toLocaleString()}`);
|
|
console.log(` Common Errors:`);
|
|
item.common_errors.slice(0, 3).forEach(err => {
|
|
console.log(` - ${err.substring(0, 80)}...`);
|
|
});
|
|
console.log();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Display detailed records
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log('📋 DETAILED SYNC RECORDS');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
// Group by status
|
|
const byStatus = records.reduce((acc, record) => {
|
|
if (!acc[record.status]) acc[record.status] = [];
|
|
acc[record.status].push(record);
|
|
return acc;
|
|
}, {} as Record<string, SyncHistoryRecord[]>);
|
|
|
|
Object.entries(byStatus).forEach(([status, statusRecords]) => {
|
|
const statusIcon = status === 'completed' ? '✅' : status === 'failed' ? '❌' : '⏳';
|
|
console.log(`${statusIcon} ${status.toUpperCase()} (${statusRecords.length} records)`);
|
|
console.log('─────────────────────────────────────────────────────────\n');
|
|
|
|
statusRecords.forEach(record => {
|
|
const duration = formatDuration(record.started_at, record.completed_at);
|
|
const errorCategory = categorizeError(record.error_message);
|
|
|
|
console.log(` ID: ${record.id}`);
|
|
console.log(` Entity: ${record.entity_type}`);
|
|
console.log(` Type: ${record.sync_type}`);
|
|
console.log(` Started: ${new Date(record.started_at).toLocaleString()}`);
|
|
console.log(` Duration: ${duration}`);
|
|
console.log(` Records: +${record.records_added} ~${record.records_updated} -${record.records_deleted}`);
|
|
console.log(` Triggered By: ${record.triggered_by}`);
|
|
|
|
if (record.error_message) {
|
|
console.log(` Error Category: ${errorCategory}`);
|
|
console.log(` Error: ${record.error_message}`);
|
|
}
|
|
|
|
console.log();
|
|
});
|
|
});
|
|
|
|
// Statistics
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log('📊 STATISTICS');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
const totalRecordsAdded = records.reduce((sum, r) => sum + r.records_added, 0);
|
|
const totalRecordsUpdated = records.reduce((sum, r) => sum + r.records_updated, 0);
|
|
const totalRecordsDeleted = records.reduce((sum, r) => sum + r.records_deleted, 0);
|
|
const failedCount = records.filter(r => r.status === 'failed').length;
|
|
const completedCount = records.filter(r => r.status === 'completed').length;
|
|
const successRate = records.length > 0 ? ((completedCount / records.length) * 100).toFixed(1) : '0';
|
|
|
|
console.log(`Total Syncs: ${records.length}`);
|
|
console.log(` Completed: ${completedCount}`);
|
|
console.log(` Failed: ${failedCount}`);
|
|
console.log(` Success Rate: ${successRate}%`);
|
|
console.log();
|
|
console.log(`Total Records Processed:`);
|
|
console.log(` Added: ${totalRecordsAdded}`);
|
|
console.log(` Updated: ${totalRecordsUpdated}`);
|
|
console.log(` Deleted: ${totalRecordsDeleted}`);
|
|
console.log();
|
|
|
|
// Error categories
|
|
if (failedCount > 0) {
|
|
const errorCategories = records
|
|
.filter(r => r.status === 'failed')
|
|
.reduce((acc, r) => {
|
|
const category = categorizeError(r.error_message);
|
|
acc[category] = (acc[category] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>);
|
|
|
|
console.log('Error Categories:');
|
|
Object.entries(errorCategories)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.forEach(([category, count]) => {
|
|
console.log(` ${category}: ${count}`);
|
|
});
|
|
console.log();
|
|
}
|
|
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Analysis failed:', error);
|
|
if (error instanceof Error) {
|
|
console.error('Stack trace:', error.stack);
|
|
}
|
|
process.exit(1);
|
|
} finally {
|
|
await postgresClient.close();
|
|
}
|
|
}
|
|
|
|
// Run analysis
|
|
analyzeSyncFailures()
|
|
.then(() => {
|
|
console.log('✅ Analysis complete\n');
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error('❌ Fatal error:', error);
|
|
process.exit(1);
|
|
});
|