From 7c27518dbb48c8df55ca85adb54d93e84132005d Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 28 Apr 2026 17:02:41 +0200 Subject: [PATCH] feat(vuln): emit VulnerabilityCacheWarmupCompleted customEvent per cold start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trace-level warmup logs are dropped by default App Insights sampling, so ops cannot reliably tell whether the MSRC / KEV cache hydration ran cleanly on a given cold start. Add a sampling-bypass customEvent (host.json already excludes "Event" from sampling) emitted once per warmup run. Captured properties let ops verify runtime health at a glance: - msrcEntries / kevEntries — how many entries each cache holds after hydrate - msrcBlobHit / kevBlobHit — derived from entries > 0; false on first deploy before any sync has written the blob, true on every subsequent cold start - msrcLastRefresh / kevLastRefresh — ISO timestamp from the persisted snapshot - durationMs — total warmup wall time (parallel Task.WhenAll, typically <100ms) - errorType / errorMessage — populated only if Task.WhenAll threw Query: customEvents | where name == 'VulnerabilityCacheWarmupCompleted' | project timestamp, customDimensions | order by timestamp desc Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Vulnerability/VulnerabilityCacheWarmer.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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;