feat: implement structured logging for sync operations
- 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.
This commit is contained in:
parent
df333348f6
commit
e25eb3fa5e
5 changed files with 1056 additions and 139 deletions
360
scripts/analyze-sync-failures.ts
Normal file
360
scripts/analyze-sync-failures.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue