diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs index 38f3f133..45bbd8b4 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs @@ -534,14 +534,20 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime if (file.EndsWith(".evtx", StringComparison.OrdinalIgnoreCase)) { tempExport = TryExportEventLogChannel(file); - if (tempExport == null) + if (tempExport != null) { - tracker.RecordSkip(file, "evtx-export", length); - continue; + streamSource = tempExport; + try { length = new FileInfo(tempExport).Length; } + catch { /* keep original length estimate */ } + } + else + { + // Export failed (channel not resolvable / wevtutil error) — fall through + // to the raw copy. Inactive or archived evtx files are not locked, so + // the plain FileStream often still succeeds; if the channel IS active + // and locked, the existing per-file catch logs the failure. + _logger.Warning($"Event log export unavailable for {file} — attempting raw copy."); } - streamSource = tempExport; - try { length = new FileInfo(tempExport).Length; } - catch { /* keep original length estimate */ } } try @@ -596,19 +602,54 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime } } + /// + /// Resolves the event-log CHANNEL that writes to the given .evtx file by matching the + /// registered channels' LogFilePath — never by guessing a naming convention: most + /// channels follow the "%4 encodes '/'" file naming, but not all do (field case + /// 2026-08-17: Microsoft-Autopilot-BootstrapperAgent/BootstrapperAgentServiceLogProvider + /// writes plain "BootstrapperAgentServiceLogProvider.evtx"). Falls back to the %4 + /// convention only when no registered channel claims the file. + /// + internal static string ResolveChannelForEvtxFile(string evtxPath) + { + try + { + var target = Path.GetFullPath(evtxPath); + using (var session = new System.Diagnostics.Eventing.Reader.EventLogSession()) + { + foreach (var logName in session.GetLogNames()) + { + try + { + var config = new System.Diagnostics.Eventing.Reader.EventLogConfiguration(logName, session); + var logFile = config.LogFilePath; + if (string.IsNullOrEmpty(logFile)) continue; + var expanded = Path.GetFullPath(Environment.ExpandEnvironmentVariables(logFile)); + if (string.Equals(expanded, target, StringComparison.OrdinalIgnoreCase)) + return logName; + } + catch { /* individual channel unreadable — keep scanning */ } + } + } + } + catch { /* enumeration unavailable — fall back to convention */ } + + return Path.GetFileNameWithoutExtension(evtxPath).Replace("%4", "/"); + } + /// /// Exports the event-log channel behind a winevt .evtx file to a temp copy via - /// wevtutil epl. Channel name is derived from the file name (%4 decodes to '/', - /// e.g. "Microsoft-Autopilot-BootstrapperAgent%4BootstrapperAgentServiceLogProvider.evtx" - /// → "Microsoft-Autopilot-BootstrapperAgent/BootstrapperAgentServiceLogProvider"). - /// Returns the temp file path (caller deletes) or null when the export failed. + /// wevtutil epl (active channels hold their file exclusively locked, so a raw + /// copy fails). Channel resolution happens against the registered channels — see + /// . Returns the temp file path (caller deletes) + /// or null when the export failed. /// private string TryExportEventLogChannel(string evtxPath) { string tempPath = null; try { - var channel = Path.GetFileNameWithoutExtension(evtxPath).Replace("%4", "/"); + var channel = ResolveChannelForEvtxFile(evtxPath); tempPath = Path.Combine(Path.GetTempPath(), $"am-evtx-{Guid.NewGuid():N}.evtx"); var psi = new System.Diagnostics.ProcessStartInfo {