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;