From a993070840b68df4f6da671585e4303a73dd8879 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 30 Jun 2026 17:32:37 +0200 Subject: [PATCH] feat(ops): include per-orphan tenant/session breakdown in OrphanEventsCleaned The OrphanEventsCleaned ops-event only persisted counts; the per-orphan detail (which tenant/session lost lingering events) was logged via worker LogInformation, which does not reach App Insights, so it was effectively lost. Enrich the event details with a capped (top-50 by EventCount) list of {tenantId, sessionId, eventCount} plus a detailsTruncated flag, reporting the actual rows deleted. Keeps the Table row under the 32KB property limit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MaintenanceService.Aggregation.cs | 13 ++++++- .../Services/OpsEventService.cs | 36 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) 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