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) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-30 17:32:37 +02:00
parent 4c6cd8e0ce
commit a993070840
2 changed files with 45 additions and 4 deletions
@@ -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<OrphanedEventSession>();
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)
{
@@ -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,
/// <summary>
/// Records the orphan-cleanup result. <paramref name="cleanedOrphans"/> 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 <c>orphanSessions</c> and a <c>detailsTruncated</c>
/// 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.
/// </summary>
public Task RecordOrphanEventsCleanedAsync(int orphanSessions, int totalEventsDeleted,
IReadOnlyList<OrphanedEventSession>? cleanedOrphans = null)
{
const int maxDetailRows = 50;
var orphanList = cleanedOrphans ?? Array.Empty<OrphanedEventSession>();
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