diff --git a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs index 16c930da..03440078 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.Aggregation.cs @@ -709,6 +709,9 @@ namespace AutopilotMonitor.Functions.Services int totalEventsDeleted = 0; int sessionsCleanedUp = 0; + // Per-orphan breakdown for the OpsEvent so the dashboard shows which tenant/session + // was cleaned (the worker LogInformation below does not reach App Insights). + var cleanedOrphans = new List(); foreach (var orphan in orphans) { @@ -719,6 +722,14 @@ namespace AutopilotMonitor.Functions.Services totalEventsDeleted += deletedEvents; sessionsCleanedUp++; + // Report the actual rows deleted (may differ from the index's recorded count). + cleanedOrphans.Add(new OrphanedEventSession + { + TenantId = orphan.TenantId, + SessionId = orphan.SessionId, + LastIngestAt = orphan.LastIngestAt, + EventCount = deletedEvents + }); _logger.LogInformation( "Cleaned orphan: TenantId={TenantId}, SessionId={SessionId}, Events={Events}", @@ -736,7 +747,7 @@ namespace AutopilotMonitor.Functions.Services "Orphaned events cleanup completed in {Ms}ms: {Sessions} sessions, {Events} events deleted", sw.ElapsedMilliseconds, sessionsCleanedUp, totalEventsDeleted); - await _opsEventService.RecordOrphanEventsCleanedAsync(sessionsCleanedUp, totalEventsDeleted); + await _opsEventService.RecordOrphanEventsCleanedAsync(sessionsCleanedUp, totalEventsDeleted, cleanedOrphans); } catch (Exception ex) { diff --git a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs index 4fe3a029..a6a62b79 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AutopilotMonitor.Shared.DataAccess; @@ -76,10 +78,38 @@ namespace AutopilotMonitor.Functions.Services $"Cleaned up {deletedCount} ops events older than {retentionDays} days", null, "System.Maintenance", new { deletedCount, retentionDays }); - public Task RecordOrphanEventsCleanedAsync(int orphanSessions, int totalEventsDeleted) - => WriteAsync(OpsEventCategory.Maintenance, "OrphanEventsCleaned", OpsEventSeverity.Warning, + /// + /// Records the orphan-cleanup result. carries the + /// per-session breakdown (which tenant + session lost lingering events and how many), + /// so the ops dashboard shows *what* was cleaned, not just a count. The list is capped + /// to keep the OpsEvents Table row under the per-property (32 KB) / entity (~1 MB) limits; + /// the full count always survives in orphanSessions and a detailsTruncated + /// flag flags any clipping. Note: an "orphan" is a session row that no longer exists while + /// its events lingered past the 24h grace — the tenant itself usually still exists. + /// + public Task RecordOrphanEventsCleanedAsync(int orphanSessions, int totalEventsDeleted, + IReadOnlyList? cleanedOrphans = null) + { + const int maxDetailRows = 50; + + var orphanList = cleanedOrphans ?? Array.Empty(); + var detail = orphanList + .OrderByDescending(o => o.EventCount) + .Take(maxDetailRows) + .Select(o => new { tenantId = o.TenantId, sessionId = o.SessionId, eventCount = o.EventCount }) + .ToList(); + + return WriteAsync(OpsEventCategory.Maintenance, "OrphanEventsCleaned", OpsEventSeverity.Warning, $"Cleaned {totalEventsDeleted} orphaned events across {orphanSessions} sessions", - null, "System.Maintenance", new { orphanSessions, totalEventsDeleted }); + null, "System.Maintenance", + new + { + orphanSessions, + totalEventsDeleted, + orphans = detail, + detailsTruncated = orphanList.Count > maxDetailRows + }); + } // ── Cascade-Delete Maintenance (Plan §5 PR6 / §16 R14) ───────────────── // Four event types dispatched by SessionDeletionMaintenanceFunction. Each is also