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
|
|
@ -20,6 +20,7 @@ import {
|
|||
buildTimeEntriesFilter,
|
||||
getTableName
|
||||
} from '../utils/sync-helpers';
|
||||
import { createSyncLogger, SyncPhase, categorizeError } from '../utils/sync-logger';
|
||||
|
||||
/**
|
||||
* Entity Sync Result
|
||||
|
|
@ -37,6 +38,7 @@ export class EntitySyncService {
|
|||
private autotaskClient: AutotaskClient;
|
||||
private cachedValidResourceIds?: Set<number>;
|
||||
private cachedValidContactIds?: Set<number>;
|
||||
private logger = createSyncLogger({ component: 'EntitySyncService' });
|
||||
|
||||
constructor(autotaskClient: AutotaskClient) {
|
||||
this.autotaskClient = autotaskClient;
|
||||
|
|
@ -63,13 +65,13 @@ export class EntitySyncService {
|
|||
return await this.syncSubIssueTypes(isIncremental);
|
||||
}
|
||||
|
||||
const syncStartTime = Date.now();
|
||||
const trackingId = syncId || `${entity}_${Date.now()}`;
|
||||
const entityLogger = this.logger.child({ syncId: trackingId, entityType: entity });
|
||||
const syncStartTime = entityLogger.start(`${entity} sync (${isIncremental ? 'incremental' : 'full'})`);
|
||||
|
||||
// Start progress tracking
|
||||
syncProgressTracker.startSync(trackingId, entity);
|
||||
|
||||
console.log(`[${entity}] Starting sync (${isIncremental ? 'incremental' : 'full'})`);
|
||||
entityLogger.phase(SyncPhase.INITIALIZING);
|
||||
|
||||
try {
|
||||
const autotaskEntityName = getAutotaskEntityName(entity);
|
||||
|
|
@ -81,13 +83,14 @@ export class EntitySyncService {
|
|||
const lastSyncTime = await getLastSyncTime(entity);
|
||||
if (lastSyncTime) {
|
||||
params.filter = buildIncrementalFilter(entity, lastSyncTime);
|
||||
console.log(`[${entity}] Incremental sync from ${lastSyncTime.toISOString()}`);
|
||||
entityLogger.info(`Incremental sync from ${lastSyncTime.toISOString()}`);
|
||||
} else {
|
||||
console.log(`[${entity}] No previous sync found, performing full sync`);
|
||||
entityLogger.info('No previous sync found, performing full sync');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${entity}] Failed to get last sync time:`, error);
|
||||
throw new Error(`Failed to determine sync time: ${error instanceof Error ? error.message : String(error)}`);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
entityLogger.error('Failed to get last sync time', {}, err);
|
||||
throw new Error(`Failed to determine sync time: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
// For full sync, build filters
|
||||
|
|
@ -96,26 +99,26 @@ export class EntitySyncService {
|
|||
// Special handling for entities that require filters
|
||||
if (entity === EntityType.CONTRACTS) {
|
||||
filters.push(...buildContractsFilter());
|
||||
console.log(`[${entity}] Full sync with status filter for active contracts`);
|
||||
entityLogger.info('Full sync with status filter for active contracts');
|
||||
} else if (entity === EntityType.PROJECTS) {
|
||||
filters.push(...buildProjectsFilter());
|
||||
console.log(`[${entity}] Full sync with status filter for non-completed projects`);
|
||||
entityLogger.info('Full sync with status filter for non-completed projects');
|
||||
} else if (entity === EntityType.TIME_ENTRIES) {
|
||||
filters.push(...buildTimeEntriesFilter(yearsBack));
|
||||
console.log(`[${entity}] Full sync with dateWorked filter for last ${yearsBack} years`);
|
||||
entityLogger.info(`Full sync with dateWorked filter for last ${yearsBack} years`);
|
||||
} else {
|
||||
// Add active filter if applicable
|
||||
const activeFilter = buildActiveFilter(entity);
|
||||
if (activeFilter) {
|
||||
filters.push(...activeFilter);
|
||||
console.log(`[${entity}] Full sync with active filter`);
|
||||
entityLogger.info('Full sync with active filter');
|
||||
}
|
||||
|
||||
// Add date range filter for time-based entities (tickets, tasks, etc.)
|
||||
const dateRangeFilter = buildDateRangeFilter(entity, yearsBack);
|
||||
if (dateRangeFilter) {
|
||||
filters.push(...dateRangeFilter);
|
||||
console.log(`[${entity}] Full sync limited to last ${yearsBack} years`);
|
||||
entityLogger.info(`Full sync limited to last ${yearsBack} years`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,71 +128,81 @@ export class EntitySyncService {
|
|||
}
|
||||
|
||||
// Fetch data from Autotask with pagination
|
||||
console.log(`[${entity}] Fetching records from Autotask API...`);
|
||||
entityLogger.phase(SyncPhase.FETCHING, 'Fetching records from Autotask API');
|
||||
syncProgressTracker.updateProgress(trackingId, { phase: 'fetching' });
|
||||
|
||||
let autotaskRecords: any[];
|
||||
const fetchStartTime = Date.now();
|
||||
try {
|
||||
autotaskRecords = await this.autotaskClient.queryEntityPaginated(
|
||||
autotaskEntityName,
|
||||
params,
|
||||
500 // Page size
|
||||
);
|
||||
entityLogger.debug('API fetch completed', { duration: Date.now() - fetchStartTime });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] API fetch failed:`, errorMessage);
|
||||
throw new Error(`Autotask API error: ${errorMessage}`);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const errorCategory = categorizeError(err);
|
||||
entityLogger.error('API fetch failed', { phase: SyncPhase.FETCHING, errorCategory }, err);
|
||||
throw new Error(`Autotask API error: ${err.message}`);
|
||||
}
|
||||
|
||||
console.log(`[${entity}] Fetched ${autotaskRecords.length} records from Autotask`);
|
||||
entityLogger.info('Fetched records from Autotask', { recordCount: autotaskRecords.length });
|
||||
syncProgressTracker.updateProgress(trackingId, {
|
||||
totalRecords: autotaskRecords.length,
|
||||
phase: 'mapping'
|
||||
});
|
||||
|
||||
if (autotaskRecords.length === 0) {
|
||||
console.log(`[${entity}] No records to sync`);
|
||||
entityLogger.info('No records to sync');
|
||||
syncProgressTracker.completeSync(trackingId, 0);
|
||||
return { recordsAdded: 0, recordsUpdated: 0, recordsDeleted: 0 };
|
||||
}
|
||||
|
||||
// Map Autotask data to PostgreSQL schema
|
||||
console.log(`[${entity}] Mapping ${autotaskRecords.length} records to database schema...`);
|
||||
entityLogger.phase(SyncPhase.MAPPING, `Mapping ${autotaskRecords.length} records to database schema`);
|
||||
|
||||
// DEBUG: Log first record to see actual field names from Autotask
|
||||
if (autotaskRecords.length > 0 && (entity === EntityType.TICKETS || entity === EntityType.TIME_ENTRIES)) {
|
||||
console.log(`[${entity}] DEBUG - Sample raw Autotask record keys:`, Object.keys(autotaskRecords[0]));
|
||||
entityLogger.debug('Sample raw Autotask record keys', { keys: Object.keys(autotaskRecords[0]) });
|
||||
if (entity === EntityType.TIME_ENTRIES) {
|
||||
console.log(`[${entity}] DEBUG - Sample time entry:`, JSON.stringify(autotaskRecords[0], null, 2));
|
||||
entityLogger.debug('Sample time entry', { sample: JSON.stringify(autotaskRecords[0], null, 2) });
|
||||
}
|
||||
}
|
||||
|
||||
let mappedRecords: Record<string, any>[];
|
||||
const mapStartTime = Date.now();
|
||||
try {
|
||||
mappedRecords = mapAutotaskBatch(entity, autotaskRecords);
|
||||
entityLogger.debug('Mapping completed', { duration: Date.now() - mapStartTime });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Mapping failed:`, errorMessage);
|
||||
throw new Error(`Data mapping error: ${errorMessage}`);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const errorCategory = categorizeError(err);
|
||||
entityLogger.error('Mapping failed', { phase: SyncPhase.MAPPING, errorCategory }, err);
|
||||
throw new Error(`Data mapping error: ${err.message}`);
|
||||
}
|
||||
|
||||
if (mappedRecords.length === 0) {
|
||||
console.warn(`[${entity}] Warning: All records failed mapping validation`);
|
||||
entityLogger.warn('All records failed mapping validation', { originalCount: autotaskRecords.length });
|
||||
return { recordsAdded: 0, recordsUpdated: 0, recordsDeleted: 0 };
|
||||
}
|
||||
|
||||
// Check for records with missing company_id (for entities that require it)
|
||||
// Note: TIME_ENTRIES removed from this check because company_id is nullable for time entries
|
||||
entityLogger.phase(SyncPhase.VALIDATING, 'Validating records');
|
||||
if (entity === EntityType.TICKETS || entity === EntityType.PROJECTS ||
|
||||
entity === EntityType.CONFIGURATION_ITEMS || entity === EntityType.CONTACTS ||
|
||||
entity === EntityType.CONTRACTS || entity === EntityType.BILLING_ITEMS) {
|
||||
const recordsWithoutCompany = mappedRecords.filter(r => !r.company_id);
|
||||
if (recordsWithoutCompany.length > 0) {
|
||||
console.warn(`[${entity}] Found ${recordsWithoutCompany.length} records without company_id (out of ${mappedRecords.length} total)`);
|
||||
console.warn(`[${entity}] Sample IDs without company_id:`, recordsWithoutCompany.slice(0, 5).map(r => r.id));
|
||||
entityLogger.warn('Found records without company_id', {
|
||||
missingCount: recordsWithoutCompany.length,
|
||||
totalCount: mappedRecords.length,
|
||||
sampleIds: recordsWithoutCompany.slice(0, 5).map(r => r.id),
|
||||
});
|
||||
// Filter out records without company_id to prevent constraint violation
|
||||
mappedRecords = mappedRecords.filter(r => r.company_id);
|
||||
console.log(`[${entity}] Filtered to ${mappedRecords.length} records with valid company_id`);
|
||||
entityLogger.info('Filtered to records with valid company_id', { validCount: mappedRecords.length });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +217,10 @@ export class EntitySyncService {
|
|||
mappedRecords = mappedRecords.map(ticket => {
|
||||
// Set invalid resource IDs to null instead of filtering out the entire ticket
|
||||
if (ticket.assigned_resource_id && !validResourceIds.has(ticket.assigned_resource_id)) {
|
||||
console.warn(`[${entity}] Ticket ${ticket.id}: Invalid assigned_resource_id ${ticket.assigned_resource_id}, setting to null`);
|
||||
entityLogger.debug(`Invalid assigned_resource_id, setting to null`, {
|
||||
ticketId: ticket.id,
|
||||
invalidResourceId: ticket.assigned_resource_id,
|
||||
});
|
||||
ticket.assigned_resource_id = null;
|
||||
}
|
||||
if (ticket.first_response_assigned_resource_id && !validResourceIds.has(ticket.first_response_assigned_resource_id)) {
|
||||
|
|
@ -218,7 +234,7 @@ export class EntitySyncService {
|
|||
|
||||
const nullifiedCount = initialCount - mappedRecords.filter(t => t.assigned_resource_id).length;
|
||||
if (nullifiedCount > 0) {
|
||||
console.warn(`[${entity}] Nullified ${nullifiedCount} invalid resource references`);
|
||||
entityLogger.warn('Nullified invalid resource references', { nullifiedCount });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +249,10 @@ export class EntitySyncService {
|
|||
mappedRecords = mappedRecords.map(item => {
|
||||
// Set invalid contact IDs to null instead of filtering out the entire item
|
||||
if (item.contact_id && !validContactIds.has(item.contact_id)) {
|
||||
console.warn(`[${entity}] Configuration Item ${item.id}: Invalid contact_id ${item.contact_id}, setting to null`);
|
||||
entityLogger.debug(`Invalid contact_id, setting to null`, {
|
||||
itemId: item.id,
|
||||
invalidContactId: item.contact_id,
|
||||
});
|
||||
item.contact_id = null;
|
||||
}
|
||||
return item;
|
||||
|
|
@ -241,26 +260,29 @@ export class EntitySyncService {
|
|||
|
||||
const nullifiedCount = initialCount - mappedRecords.filter(i => i.contact_id).length;
|
||||
if (nullifiedCount > 0) {
|
||||
console.warn(`[${entity}] Nullified ${nullifiedCount} invalid contact references`);
|
||||
entityLogger.warn('Nullified invalid contact references', { nullifiedCount });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[${entity}] Successfully mapped ${mappedRecords.length} records`);
|
||||
entityLogger.info('Successfully mapped records', { mappedCount: mappedRecords.length });
|
||||
|
||||
// Bulk upsert to PostgreSQL
|
||||
console.log(`[${entity}] Upserting records to PostgreSQL...`);
|
||||
entityLogger.phase(SyncPhase.UPSERTING, 'Upserting records to PostgreSQL');
|
||||
syncProgressTracker.updateProgress(trackingId, { phase: 'upserting' });
|
||||
|
||||
let upsertedCount: number;
|
||||
const upsertStartTime = Date.now();
|
||||
try {
|
||||
upsertedCount = await bulkUpsertRecords(entity, mappedRecords, 100);
|
||||
entityLogger.debug('Upsert completed', { duration: Date.now() - upsertStartTime });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Database upsert failed:`, errorMessage);
|
||||
throw new Error(`Database error: ${errorMessage}`);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const errorCategory = categorizeError(err);
|
||||
entityLogger.error('Database upsert failed', { phase: SyncPhase.UPSERTING, errorCategory }, err);
|
||||
throw new Error(`Database error: ${err.message}`);
|
||||
}
|
||||
|
||||
console.log(`[${entity}] Upserted ${upsertedCount} records to PostgreSQL`);
|
||||
entityLogger.info('Upserted records to PostgreSQL', { upsertedCount });
|
||||
|
||||
// For full sync, soft delete records not in the fetched set
|
||||
// IMPORTANT: Only delete for entities without date filters to avoid deleting records outside sync window
|
||||
|
|
@ -272,32 +294,34 @@ export class EntitySyncService {
|
|||
entity === EntityType.CONTRACTS;
|
||||
|
||||
if (!isIncremental && !hasDateFilter) {
|
||||
console.log(`[${entity}] Checking for records to soft delete...`);
|
||||
entityLogger.phase(SyncPhase.DELETING, 'Checking for records to soft delete');
|
||||
syncProgressTracker.updateProgress(trackingId, { phase: 'deleting' });
|
||||
|
||||
try {
|
||||
const activeIds = mappedRecords.map(r => r.id);
|
||||
deletedCount = await softDeleteMissingRecords(entity, activeIds);
|
||||
console.log(`[${entity}] Soft deleted ${deletedCount} missing records`);
|
||||
entityLogger.info('Soft deleted missing records', { deletedCount });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Soft delete failed:`, errorMessage);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
entityLogger.warn('Soft delete failed, continuing sync', {}, err);
|
||||
// Don't throw - soft delete failure shouldn't fail the entire sync
|
||||
console.warn(`[${entity}] Continuing despite soft delete failure`);
|
||||
}
|
||||
} else if (!isIncremental && hasDateFilter) {
|
||||
console.log(`[${entity}] Skipping soft-delete for date-filtered sync (would delete records outside sync window)`);
|
||||
entityLogger.info('Skipping soft-delete for date-filtered sync (would delete records outside sync window)');
|
||||
}
|
||||
|
||||
// Calculate added vs updated (simplified - actual count would require tracking)
|
||||
const recordsAdded = Math.floor(upsertedCount * 0.1); // Estimate 10% new
|
||||
const recordsUpdated = upsertedCount - recordsAdded;
|
||||
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.log(`[${entity}] Sync completed in ${duration}ms`);
|
||||
|
||||
// Mark sync as completed
|
||||
entityLogger.phase(SyncPhase.COMPLETING);
|
||||
syncProgressTracker.completeSync(trackingId, mappedRecords.length);
|
||||
entityLogger.complete(`${entity} sync`, syncStartTime, {
|
||||
recordsAdded,
|
||||
recordsUpdated,
|
||||
recordsDeleted: deletedCount,
|
||||
});
|
||||
|
||||
return {
|
||||
recordsAdded,
|
||||
|
|
@ -305,12 +329,11 @@ export class EntitySyncService {
|
|||
recordsDeleted: deletedCount,
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - syncStartTime;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Sync failed after ${duration}ms:`, errorMessage);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
// Mark sync as failed
|
||||
syncProgressTracker.failSync(trackingId, errorMessage);
|
||||
syncProgressTracker.failSync(trackingId, err.message);
|
||||
entityLogger.fail(`${entity} sync`, syncStartTime, err);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -341,14 +364,14 @@ export class EntitySyncService {
|
|||
yearsBack: number = 2,
|
||||
onChunkProgress?: (chunk: { index: number; total: number; description: string; recordsProcessed: number }) => void
|
||||
): Promise<EntitySyncStats> {
|
||||
const syncStartTime = Date.now();
|
||||
const entity = EntityType.TICKETS;
|
||||
console.log(`[${entity}] Starting chunked sync for last ${yearsBack} years`);
|
||||
const chunkLogger = this.logger.child({ entityType: entity, syncType: 'chunked' });
|
||||
const syncStartTime = chunkLogger.start(`Chunked sync for last ${yearsBack} years`);
|
||||
|
||||
try {
|
||||
// Calculate date chunks (monthly)
|
||||
const chunks = this.calculateMonthlyChunks(yearsBack);
|
||||
console.log(`[${entity}] Split into ${chunks.length} monthly chunks`);
|
||||
chunkLogger.info('Split into monthly chunks', { chunkCount: chunks.length, yearsBack });
|
||||
|
||||
let totalRecordsAdded = 0;
|
||||
let totalRecordsUpdated = 0;
|
||||
|
|
@ -360,7 +383,7 @@ export class EntitySyncService {
|
|||
const chunk = chunks[i];
|
||||
const chunkDescription = `${chunk.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}`;
|
||||
|
||||
console.log(`[${entity}] Processing chunk ${i + 1}/${chunks.length}: ${chunkDescription}`);
|
||||
chunkLogger.info(`Processing chunk ${i + 1}/${chunks.length}`, { chunkDescription });
|
||||
|
||||
// Notify progress
|
||||
if (onChunkProgress) {
|
||||
|
|
@ -380,7 +403,11 @@ export class EntitySyncService {
|
|||
{ field: 'createDate', op: 'lt' as const, value: chunk.endDate.toISOString() },
|
||||
];
|
||||
|
||||
console.log(`[${entity}] Fetching records from ${chunk.startDate.toISOString()} to ${chunk.endDate.toISOString()}`);
|
||||
chunkLogger.debug('Fetching records for chunk', {
|
||||
chunkIndex: i + 1,
|
||||
startDate: chunk.startDate.toISOString(),
|
||||
endDate: chunk.endDate.toISOString(),
|
||||
});
|
||||
|
||||
const autotaskRecords = await this.autotaskClient.queryEntityPaginated(
|
||||
autotaskEntityName,
|
||||
|
|
@ -388,23 +415,26 @@ export class EntitySyncService {
|
|||
500
|
||||
);
|
||||
|
||||
console.log(`[${entity}] Chunk ${i + 1}: Fetched ${autotaskRecords.length} records`);
|
||||
chunkLogger.info(`Chunk ${i + 1}: Fetched records`, { recordCount: autotaskRecords.length });
|
||||
|
||||
if (autotaskRecords.length > 0) {
|
||||
// Map and upsert records
|
||||
let mappedRecords = mapAutotaskBatch(entity, autotaskRecords);
|
||||
|
||||
// Filter out records without company_id
|
||||
const beforeFilter = mappedRecords.length;
|
||||
mappedRecords = mappedRecords.filter(r => r.company_id);
|
||||
if (mappedRecords.length < autotaskRecords.length) {
|
||||
console.warn(`[${entity}] Chunk ${i + 1}: Filtered out ${autotaskRecords.length - mappedRecords.length} records without company_id`);
|
||||
if (mappedRecords.length < beforeFilter) {
|
||||
chunkLogger.warn(`Chunk ${i + 1}: Filtered out records without company_id`, {
|
||||
filteredCount: beforeFilter - mappedRecords.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate resource foreign keys (fetch once per sync, not per chunk)
|
||||
if (i === 0) {
|
||||
// Cache valid resource IDs for all chunks
|
||||
this.cachedValidResourceIds = await this.getValidResourceIds();
|
||||
console.log(`[${entity}] Cached ${this.cachedValidResourceIds.size} valid resource IDs`);
|
||||
chunkLogger.info('Cached valid resource IDs', { cacheSize: this.cachedValidResourceIds.size });
|
||||
}
|
||||
|
||||
// Nullify invalid resource references
|
||||
|
|
@ -431,24 +461,37 @@ export class EntitySyncService {
|
|||
totalRecordsAdded += recordsAdded;
|
||||
totalRecordsUpdated += recordsUpdated;
|
||||
|
||||
console.log(`[${entity}] Chunk ${i + 1}: Upserted ${upsertedCount} records (+${recordsAdded} ~${recordsUpdated})`);
|
||||
chunkLogger.info(`Chunk ${i + 1}: Upserted records`, {
|
||||
upsertedCount,
|
||||
recordsAdded,
|
||||
recordsUpdated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Chunk ${i + 1} (${chunkDescription}) failed:`, errorMessage);
|
||||
failedChunks.push(`${chunkDescription}: ${errorMessage}`);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const errorCategory = categorizeError(err);
|
||||
chunkLogger.error(`Chunk ${i + 1} (${chunkDescription}) failed`, {
|
||||
chunkIndex: i + 1,
|
||||
chunkDescription,
|
||||
errorCategory,
|
||||
}, err);
|
||||
failedChunks.push(`${chunkDescription}: ${err.message}`);
|
||||
// Continue with next chunk instead of failing entire sync
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.log(`[${entity}] Chunked sync completed in ${duration}ms`);
|
||||
console.log(`[${entity}] Total: +${totalRecordsAdded} ~${totalRecordsUpdated} -${totalRecordsDeleted}`);
|
||||
chunkLogger.complete('Chunked sync', syncStartTime, {
|
||||
totalRecordsAdded,
|
||||
totalRecordsUpdated,
|
||||
totalRecordsDeleted,
|
||||
totalChunks: chunks.length,
|
||||
failedChunks: failedChunks.length,
|
||||
});
|
||||
|
||||
if (failedChunks.length > 0) {
|
||||
console.warn(`[${entity}] ${failedChunks.length} chunks failed:`, failedChunks);
|
||||
chunkLogger.warn('Some chunks failed', { failedChunks });
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -457,9 +500,8 @@ export class EntitySyncService {
|
|||
recordsDeleted: totalRecordsDeleted,
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - syncStartTime;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[${entity}] Chunked sync failed after ${duration}ms:`, errorMessage);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
chunkLogger.fail('Chunked sync', syncStartTime, err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -475,7 +517,8 @@ export class EntitySyncService {
|
|||
const result = await postgresClient.query<{ id: number }>(query);
|
||||
return new Set(result.rows.map(row => row.id));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch valid resource IDs:', error);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('Failed to fetch valid resource IDs', {}, err);
|
||||
// Return empty set on error - will cause all resource IDs to be nullified
|
||||
return new Set();
|
||||
}
|
||||
|
|
@ -491,7 +534,8 @@ export class EntitySyncService {
|
|||
const result = await postgresClient.query<{ id: number }>(query);
|
||||
return new Set(result.rows.map(row => row.id));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch valid contact IDs:', error);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('Failed to fetch valid contact IDs', {}, err);
|
||||
// Return empty set on error - will cause all contact IDs to be nullified
|
||||
return new Set();
|
||||
}
|
||||
|
|
@ -594,8 +638,8 @@ export class EntitySyncService {
|
|||
* Sync Issue Types (Picklist from Ticket field)
|
||||
*/
|
||||
async syncIssueTypes(isIncremental: boolean = false): Promise<EntitySyncStats> {
|
||||
const syncStartTime = Date.now();
|
||||
console.log(`[issue_types] Starting picklist sync`);
|
||||
const picklistLogger = this.logger.child({ entityType: EntityType.ISSUE_TYPES, syncType: 'picklist' });
|
||||
const syncStartTime = picklistLogger.start('Picklist sync');
|
||||
|
||||
try {
|
||||
// Get issue type picklist values from Tickets entity
|
||||
|
|
@ -610,7 +654,7 @@ export class EntitySyncService {
|
|||
synced_at: new Date(),
|
||||
}));
|
||||
|
||||
console.log(`[issue_types] Found ${records.length} picklist values`);
|
||||
picklistLogger.info('Found picklist values', { recordCount: records.length });
|
||||
|
||||
// Upsert to database
|
||||
const tableName = getTableName(EntityType.ISSUE_TYPES);
|
||||
|
|
@ -622,13 +666,15 @@ export class EntitySyncService {
|
|||
recordsDeleted: 0,
|
||||
};
|
||||
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.log(`[issue_types] Sync completed in ${duration}ms: +${stats.recordsAdded} ~${stats.recordsUpdated}`);
|
||||
picklistLogger.complete('Picklist sync', syncStartTime, {
|
||||
recordsAdded: stats.recordsAdded,
|
||||
recordsUpdated: stats.recordsUpdated,
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.error(`[issue_types] Sync failed after ${duration}ms:`, error);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
picklistLogger.fail('Picklist sync', syncStartTime, err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -637,8 +683,8 @@ export class EntitySyncService {
|
|||
* Sync Sub-Issue Types (Picklist from Ticket field)
|
||||
*/
|
||||
async syncSubIssueTypes(isIncremental: boolean = false): Promise<EntitySyncStats> {
|
||||
const syncStartTime = Date.now();
|
||||
console.log(`[sub_issue_types] Starting picklist sync`);
|
||||
const picklistLogger = this.logger.child({ entityType: EntityType.SUB_ISSUE_TYPES, syncType: 'picklist' });
|
||||
const syncStartTime = picklistLogger.start('Picklist sync');
|
||||
|
||||
try {
|
||||
// Get sub-issue type picklist values from Tickets entity
|
||||
|
|
@ -670,7 +716,7 @@ export class EntitySyncService {
|
|||
synced_at: new Date(),
|
||||
}));
|
||||
|
||||
console.log(`[sub_issue_types] Found ${records.length} picklist values`);
|
||||
picklistLogger.info('Found picklist values', { recordCount: records.length });
|
||||
|
||||
// Upsert to database
|
||||
const tableName = getTableName(EntityType.SUB_ISSUE_TYPES);
|
||||
|
|
@ -682,13 +728,15 @@ export class EntitySyncService {
|
|||
recordsDeleted: 0,
|
||||
};
|
||||
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.log(`[sub_issue_types] Sync completed in ${duration}ms: +${stats.recordsAdded} ~${stats.recordsUpdated}`);
|
||||
picklistLogger.complete('Picklist sync', syncStartTime, {
|
||||
recordsAdded: stats.recordsAdded,
|
||||
recordsUpdated: stats.recordsUpdated,
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - syncStartTime;
|
||||
console.error(`[sub_issue_types] Sync failed after ${duration}ms:`, error);
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
picklistLogger.fail('Picklist sync', syncStartTime, err);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue