From e19852cd9aa8b2100651a905484cd103d6a8343f Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 3 Apr 2026 16:56:00 +0200 Subject: [PATCH] add structured error code extraction from IME log patterns The backend already reads errorCode from event data for FailureCode and failure reason formatting, but the agent never populated it. This change extracts error codes (exit codes, HRESULT) from named regex capture groups in IME log error patterns and sends them as structured errorCode fields. Also fixes a pre-existing bug where ErrorPatternId/ErrorDetail were not persisted across agent restarts. Co-Authored-By: Claude Opus 4.6 (1M context) --- rules/dist/ime-log-patterns.json | 2 +- rules/ime-log-patterns/IME-ERROR-UNMAPPED-EXIT.json | 2 +- .../Monitoring/Tracking/AppPackageState.cs | 10 ++++++++-- .../Monitoring/Tracking/ImeLogTracker.Handlers.cs | 4 ++-- .../Tracking/ImeLogTracker.LogProcessing.cs | 11 +++++++++-- .../Monitoring/Tracking/ImeLogTracker.cs | 4 ++++ .../Monitoring/Tracking/ImeTrackerStatePersistence.cs | 5 +++++ 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/rules/dist/ime-log-patterns.json b/rules/dist/ime-log-patterns.json index 497372e8..cf3e1f40 100644 --- a/rules/dist/ime-log-patterns.json +++ b/rules/dist/ime-log-patterns.json @@ -235,7 +235,7 @@ { "patternId": "IME-ERROR-UNMAPPED-EXIT", "category": "currentPhase", - "pattern": "\\[Win32App\\] Admin did NOT set mapping for lpExitCode\\W+\\d+ of app\\W+{GUID}", + "pattern": "\\[Win32App\\] Admin did NOT set mapping for lpExitCode\\W+(?\\d+) of app\\W+{GUID}", "action": "updateStateError", "description": "Detects when an app installer returned an exit code that has no mapping configured in Intune.", "enabled": true diff --git a/rules/ime-log-patterns/IME-ERROR-UNMAPPED-EXIT.json b/rules/ime-log-patterns/IME-ERROR-UNMAPPED-EXIT.json index 397b4884..0a25b5ab 100644 --- a/rules/ime-log-patterns/IME-ERROR-UNMAPPED-EXIT.json +++ b/rules/ime-log-patterns/IME-ERROR-UNMAPPED-EXIT.json @@ -2,7 +2,7 @@ "$schema": "../schema/ime-log-pattern.schema.json", "patternId": "IME-ERROR-UNMAPPED-EXIT", "category": "currentPhase", - "pattern": "\\[Win32App\\] Admin did NOT set mapping for lpExitCode\\W+\\d+ of app\\W+{GUID}", + "pattern": "\\[Win32App\\] Admin did NOT set mapping for lpExitCode\\W+(?\\d+) of app\\W+{GUID}", "action": "updateStateError", "description": "Detects when an app installer returned an exit code that has no mapping configured in Intune.", "enabled": true diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs index f42df178..761eb86b 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs @@ -90,6 +90,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking public long BytesTotal { get; private set; } = 0; public string ErrorPatternId { get; private set; } public string ErrorDetail { get; private set; } + public string ErrorCode { get; private set; } // Delivery Optimization telemetry (populated from [DO TEL] log entries) public long DoFileSize { get; private set; } @@ -136,7 +137,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking HashSet dependsOn, AppInstallationState installationState, bool downloadingOrInstallingSeen, int? progressPercent, long bytesDownloaded, long bytesTotal, - string errorPatternId = null, string errorDetail = null, + string errorPatternId = null, string errorDetail = null, string errorCode = null, long doFileSize = 0, long doTotalBytesDownloaded = 0, long doBytesFromPeers = 0, int doPercentPeerCaching = 0, long doBytesFromLanPeers = 0, long doBytesFromGroupPeers = 0, @@ -158,6 +159,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking BytesTotal = bytesTotal, ErrorPatternId = errorPatternId, ErrorDetail = errorDetail, + ErrorCode = errorCode, InstallationStateLastChangedTicks = Stopwatch.GetTimestamp(), DoFileSize = doFileSize, DoTotalBytesDownloaded = doTotalBytesDownloaded, @@ -234,10 +236,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking /// /// Sets the error context for the app (called when an error pattern is matched). /// - public void SetErrorContext(string patternId, string detail) + public void SetErrorContext(string patternId, string detail, string errorCode = null) { ErrorPatternId = patternId; ErrorDetail = detail; + if (!string.IsNullOrEmpty(errorCode)) + ErrorCode = errorCode; } /// @@ -415,6 +419,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking data["errorPatternId"] = ErrorPatternId; if (!string.IsNullOrEmpty(ErrorDetail)) data["errorDetail"] = ErrorDetail; + if (!string.IsNullOrEmpty(ErrorCode)) + data["errorCode"] = ErrorCode; if (HasDoTelemetry) { diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.Handlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.Handlers.cs index 6bf3a4f5..2f710f4a 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.Handlers.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.Handlers.cs @@ -463,14 +463,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _packageStates.SetCurrent(newId); } - private void UpdateStateWithCallback(string id, AppInstallationState newState, int? progressPercent = null, string errorPatternId = null, string errorDetail = null) + private void UpdateStateWithCallback(string id, AppInstallationState newState, int? progressPercent = null, string errorPatternId = null, string errorDetail = null, string errorCode = null) { var pkg = _packageStates.GetPackage(id); if (pkg == null) return; // Set error context before state change so it's available in ToEventData() if (newState == AppInstallationState.Error && !string.IsNullOrEmpty(errorPatternId)) - pkg.SetErrorContext(errorPatternId, errorDetail); + pkg.SetErrorContext(errorPatternId, errorDetail, errorCode); var oldState = pkg.InstallationState; var changed = _packageStates.UpdateState(id, newState, progressPercent); diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs index 6a6543fc..a9416843 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs @@ -249,16 +249,23 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking break; case "updatestateerror": + // Extract structured error code from named capture groups (exitCode, hresult, errorCode) + var extractedErrorCode = match.Groups["exitCode"]?.Value; + if (string.IsNullOrEmpty(extractedErrorCode)) + extractedErrorCode = match.Groups["hresult"]?.Value; + if (string.IsNullOrEmpty(extractedErrorCode)) + extractedErrorCode = match.Groups["errorCode"]?.Value; + if (pattern.Parameters.ContainsKey("checkTo") && pattern.Parameters["checkTo"] == "true") { // Only apply error if the "to" value is "Error" var toValue = match.Groups["to"]?.Value; if (string.Equals(toValue, "Error", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(id)) - UpdateStateWithCallback(id, AppInstallationState.Error, errorPatternId: pattern.PatternId, errorDetail: message); + UpdateStateWithCallback(id, AppInstallationState.Error, errorPatternId: pattern.PatternId, errorDetail: message, errorCode: extractedErrorCode); } else if (!string.IsNullOrEmpty(id)) { - UpdateStateWithCallback(id, AppInstallationState.Error, errorPatternId: pattern.PatternId, errorDetail: message); + UpdateStateWithCallback(id, AppInstallationState.Error, errorPatternId: pattern.PatternId, errorDetail: message, errorCode: extractedErrorCode); } break; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs index 6d649d33..3bcaa5b7 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs @@ -320,6 +320,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking p.DependsOn != null ? new HashSet(p.DependsOn) : new HashSet(), (AppInstallationState)p.InstallationState, p.DownloadingOrInstallingSeen, p.ProgressPercent, p.BytesDownloaded, p.BytesTotal, + errorPatternId: p.ErrorPatternId, errorDetail: p.ErrorDetail, errorCode: p.ErrorCode, doFileSize: p.DoFileSize, doTotalBytesDownloaded: p.DoTotalBytesDownloaded, doBytesFromPeers: p.DoBytesFromPeers, doPercentPeerCaching: p.DoPercentPeerCaching, doBytesFromLanPeers: p.DoBytesFromLanPeers, doBytesFromGroupPeers: p.DoBytesFromGroupPeers, @@ -380,6 +381,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking ProgressPercent = p.ProgressPercent, BytesDownloaded = p.BytesDownloaded, BytesTotal = p.BytesTotal, + ErrorPatternId = p.ErrorPatternId, + ErrorDetail = p.ErrorDetail, + ErrorCode = p.ErrorCode, DoFileSize = p.DoFileSize, DoTotalBytesDownloaded = p.DoTotalBytesDownloaded, DoBytesFromPeers = p.DoBytesFromPeers, diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeTrackerStatePersistence.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeTrackerStatePersistence.cs index c5aca943..70f4a6f1 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeTrackerStatePersistence.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeTrackerStatePersistence.cs @@ -123,6 +123,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking public long BytesDownloaded { get; set; } public long BytesTotal { get; set; } + // Error context + public string ErrorPatternId { get; set; } + public string ErrorDetail { get; set; } + public string ErrorCode { get; set; } + // Delivery Optimization telemetry public long DoFileSize { get; set; } public long DoTotalBytesDownloaded { get; set; }