From 7d0872b03b815b70c051e5331cbc26ec771b76b9 Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Thu, 27 Aug 2026 10:01:24 +0500 Subject: [PATCH] chore(logging): add trace logging to attendance, template, and photo services Adds TraceEnter / TraceExit at service boundaries for attendance sync, template sync, and photo fetch. --- .../Services/AttendanceSyncService.cs | 7 +++++++ .../Services/EmployeePhotoService.cs | 19 +++++++++++++++++++ .../Services/TemplateSyncService.cs | 8 ++++++++ 3 files changed, 34 insertions(+) diff --git a/src/HikvisionAttendanceManager.App/Services/AttendanceSyncService.cs b/src/HikvisionAttendanceManager.App/Services/AttendanceSyncService.cs index 1e7f5a3..25b7fc6 100644 --- a/src/HikvisionAttendanceManager.App/Services/AttendanceSyncService.cs +++ b/src/HikvisionAttendanceManager.App/Services/AttendanceSyncService.cs @@ -11,6 +11,8 @@ public sealed class AttendanceSyncService( public async Task SyncAsync(Device device, DateTime fromLocal, DateTime toLocal, bool useLastSyncCursor, IProgress? progress, CancellationToken cancellationToken) { + AppLogger.TraceEnter("ATTENDANCE_SYNC", nameof(SyncAsync), + $"device={device.IpAddress} from={fromLocal:yyyy-MM-dd HH:mm} to={toLocal:yyyy-MM-dd HH:mm} useCursor={useLastSyncCursor}"); var entry = new SyncHistoryEntry { Operation = SyncOperationKind.AttendanceSync, @@ -37,7 +39,9 @@ public sealed class AttendanceSyncService( } progress?.Report(new AttendanceSyncProgress(0, 0, 0, 0, "Fetching attendance records from device…")); + AppLogger.TraceEnter("ATTENDANCE_SYNC", "HikvisionIsapiClient.FetchAcsEventsAsync", $"device={device.IpAddress}"); var punches = await hikvision.FetchAcsEventsAsync(device, fromLocal, toLocal, cancellationToken); + AppLogger.TraceExit("ATTENDANCE_SYNC", "HikvisionIsapiClient.FetchAcsEventsAsync", $"fetched={punches.Count}"); fetched = punches.Count; for (var index = 0; index < punches.Count; index++) @@ -81,6 +85,8 @@ public sealed class AttendanceSyncService( entry.Failed = failed; entry.Status = failed > 0 ? "Completed with errors" : "Completed"; await history.UpdateAsync(entry, cancellationToken); + AppLogger.TraceExit("ATTENDANCE_SYNC", nameof(SyncAsync), + $"fetched={fetched} inserted={inserted} skipped={skipped} failed={failed}"); } catch (Exception ex) { @@ -92,6 +98,7 @@ public sealed class AttendanceSyncService( entry.Status = "Failed"; entry.Context += " — " + ex.Message; await history.UpdateAsync(entry, cancellationToken); + AppLogger.TraceExit("ATTENDANCE_SYNC", nameof(SyncAsync), $"result=FAILED reason={ex.Message}"); throw; } diff --git a/src/HikvisionAttendanceManager.App/Services/EmployeePhotoService.cs b/src/HikvisionAttendanceManager.App/Services/EmployeePhotoService.cs index 80a361a..c567c23 100644 --- a/src/HikvisionAttendanceManager.App/Services/EmployeePhotoService.cs +++ b/src/HikvisionAttendanceManager.App/Services/EmployeePhotoService.cs @@ -12,24 +12,43 @@ public sealed class EmployeePhotoService public async Task DownloadAndNormalizeAsync(HrmsEmployee employee, CancellationToken cancellationToken) { + AppLogger.TraceEnter("USER_CREATE", nameof(DownloadAndNormalizeAsync), $"employee={employee.SerialNumber} id={employee.Id}"); try { var response = await _client.GetAsync(GetPhotoUri(employee), cancellationToken); if (!response.IsSuccessStatusCode) + { + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=false http={(int)response.StatusCode}"); return ApiResultWithBytes.Failed($"Photo not found or unavailable (HTTP {(int)response.StatusCode})."); + } var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken); if (bytes.Length < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8 || bytes[2] != 0xFF) + { + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=false reason=not-jpeg"); return ApiResultWithBytes.Failed("Face processing failed: the employee photo is not a JPEG."); + } if (bytes.Length > 2 * 1024 * 1024) + { + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=false reason=too-large"); return ApiResultWithBytes.Failed("Face processing failed: photo exceeds the 2 MB safety limit."); + } + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=true bytes={bytes.Length}"); return ApiResultWithBytes.Succeeded(bytes); } catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) { + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=false reason=timeout"); return ApiResultWithBytes.Failed("Photo download timed out."); } catch (Exception) { + AppLogger.TraceExit("USER_CREATE", nameof(DownloadAndNormalizeAsync), + $"employee={employee.SerialNumber} success=false reason=error"); return ApiResultWithBytes.Failed("Photo download failed."); } } diff --git a/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs b/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs index 851640b..9f06c39 100644 --- a/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs +++ b/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs @@ -10,6 +10,7 @@ public sealed class TemplateSyncService( { public async Task DeviceToDbAsync(Device device, IProgress? progress, CancellationToken cancellationToken) { + AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DeviceToDbAsync), $"device={device.IpAddress} direction=DeviceToDb"); var entry = new SyncHistoryEntry { Operation = SyncOperationKind.DeviceToDb, @@ -19,6 +20,7 @@ public sealed class TemplateSyncService( await history.AddAsync(entry, cancellationToken); var users = await hikvision.GetUsersAsync(device, cancellationToken); + AppLogger.Info($"[TEMPLATE_SYNC] DeviceToDb loaded {users.Count} user(s) from device"); entry.Total = users.Count; var success = 0; var failed = 0; @@ -65,12 +67,16 @@ public sealed class TemplateSyncService( entry.Status = failed > 0 ? "Completed with errors" : "Completed"; await history.UpdateAsync(entry, cancellationToken); progress?.Report(new UserSyncProgress(users.Count, users.Count, "", "Completed", success, 0, 0, 0, skipped, failed)); + AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DeviceToDbAsync), + $"success={success} failed={failed} skipped={skipped}"); return entry; } public async Task DbToDeviceAsync(Device source, Device target, IReadOnlyList serialNumbers, IProgress? progress, CancellationToken cancellationToken) { + AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DbToDeviceAsync), + $"source={source.IpAddress} target={target.IpAddress} count={serialNumbers.Count}"); if (string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) && string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Source and target devices must be different."); @@ -151,6 +157,8 @@ public sealed class TemplateSyncService( entry.Status = failed > 0 ? "Completed with errors" : "Completed"; await history.UpdateAsync(entry, cancellationToken); progress?.Report(new UserSyncProgress(serialNumbers.Count, serialNumbers.Count, "", "Completed", created, 0, faces, 0, skipped, failed)); + AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DbToDeviceAsync), + $"created={created} faces={faces} failed={failed} skipped={skipped}"); return entry; } }