diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCacheWarmer.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCacheWarmer.cs index 2bfa7032..20181e94 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCacheWarmer.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCacheWarmer.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.ApplicationInsights; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -12,20 +15,31 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability /// host comes up immediately — the actual blob reads run in the background and the /// correlation hot path defensively calls EnsureHydratedAsync in case telemetry /// arrives before warmup completes. + /// + /// Emits a VulnerabilityCacheWarmupCompleted custom event per cold start so + /// runtime health is observable without relying on aggressively sampled trace logs: + /// + /// customEvents + /// | where name == 'VulnerabilityCacheWarmupCompleted' + /// | project timestamp, customDimensions + /// /// public class VulnerabilityCacheWarmer : IHostedService { private readonly MsrcCorrelationService _msrc; private readonly KevDataService _kev; + private readonly TelemetryClient? _telemetryClient; private readonly ILogger _logger; public VulnerabilityCacheWarmer( MsrcCorrelationService msrc, KevDataService kev, + TelemetryClient? telemetryClient, ILogger logger) { _msrc = msrc; _kev = kev; + _telemetryClient = telemetryClient; _logger = logger; } @@ -37,6 +51,9 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability // in-flight load instead of duplicating work. _ = Task.Run(async () => { + var sw = Stopwatch.StartNew(); + string? errorType = null; + string? errorMessage = null; try { await Task.WhenAll( @@ -48,8 +65,30 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability } catch (Exception ex) { + errorType = ex.GetType().Name; + errorMessage = ex.Message; _logger.LogWarning(ex, "Vulnerability cache warmup failed — defensive hydration will still cover correctness"); } + finally + { + sw.Stop(); + // Sampling-bypass observability — host.json's excludedTypes adds "Event" + // so this survives even when default trace sampling is aggressive. Lets + // ops verify per-cold-start that hydration ran and which source(s) hit + // the blob (msrcBlobHit/kevBlobHit = entries > 0 after EnsureHydrated). + _telemetryClient?.TrackEvent("VulnerabilityCacheWarmupCompleted", new Dictionary + { + { "msrcEntries", _msrc.IndexSize.ToString() }, + { "kevEntries", _kev.CatalogSize.ToString() }, + { "msrcBlobHit", (_msrc.IndexSize > 0).ToString() }, + { "kevBlobHit", (_kev.CatalogSize > 0).ToString() }, + { "msrcLastRefresh", _msrc.LastRefresh == DateTime.MinValue ? "" : _msrc.LastRefresh.ToString("o") }, + { "kevLastRefresh", _kev.LastRefresh == DateTime.MinValue ? "" : _kev.LastRefresh.ToString("o") }, + { "durationMs", sw.ElapsedMilliseconds.ToString() }, + { "errorType", errorType ?? "" }, + { "errorMessage", errorMessage ?? "" } + }); + } }, cancellationToken); return Task.CompletedTask;