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) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-04-03 16:56:00 +02:00
parent 77ff826c20
commit e19852cd9a
7 changed files with 30 additions and 8 deletions
+1 -1
View File
@@ -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+(?<exitCode>\\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
@@ -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+(?<exitCode>\\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
@@ -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<string> 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
/// <summary>
/// Sets the error context for the app (called when an error pattern is matched).
/// </summary>
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;
}
/// <summary>
@@ -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)
{
@@ -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);
@@ -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;
@@ -320,6 +320,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
p.DependsOn != null ? new HashSet<string>(p.DependsOn) : new HashSet<string>(),
(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,
@@ -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; }