mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
75197a4184
* updated commands * feat(backend): logparser pattern test endpoint with agent-exact semantics POST /api/rules/gather/test-pattern evaluates a logparser gather-rule regex against pasted sample lines exactly the way the agent would on the device. Motivated by a field incident: a customer verified a pattern with a PHP regex tester and the .NET engine matched differently. - CmTraceLogParser moves Agent -> AutopilotMonitor.Shared.Logging so agent (IME tracker, logparser collector, stall probe, IME matching mode) and backend run the SAME parsing implementation. Pure mechanical move, agent behavior unchanged (V2.Core suite 2147/2147 green). - TestLogPatternFunction mirrors LogParserCollector verbatim: Regex(Compiled, 1s timeout, NO IgnoreCase -> case-sensitive), first match per line, group "0" excluded / unsuccessful groups omitted, format cmtrace (default, match against parsed message) vs text (raw line), per-line outcomes incl. parse_failed/regex_timeout, and the agent's "set parameter format=text" hint verbatim. Pure compute, no tenant data. Caps: 200 lines, 8KB/line. - Policy: TenantAdminOrGlobalReader (same tier as the analyze dry-run). - Tests pin the parity showcases: case-sensitivity, envelope invisibility in cmtrace mode, optional-group omission, format hint, request validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): test_log_pattern tool + logparser lint and guide hardening - test_log_pattern: wraps POST /api/rules/gather/test-pattern — tests a logparser regex against pasted sample lines with the agent-exact .NET semantics (case-sensitive, cmtrace/text mode). Description steers clients away from JS/PHP/Python regex testing. - validate_rule: logparser parameter lint (missing/non-compiling pattern, unknown format value = silently cmtrace on the agent) + info pointing to test_log_pattern; nextStep for valid logparser drafts routes there too. - rule_authoring_guide: logparser section now states the matching semantics (case-SENSITIVE, .NET, first match per line, cmtrace default) and two new pitfalls (engine mismatch, plain-text log under cmtrace default). - Catalog snapshot + handler/validator tests extended (MCP suite 532 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(okf): test-pattern endpoint + shared CmTraceLogParser in rule authoring surface Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
298 lines
12 KiB
C#
298 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime;
|
|
using AutopilotMonitor.Shared.Logging;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace AutopilotMonitor.Agent.V2
|
|
{
|
|
/// <summary>
|
|
/// <c>--run-ime-matching</c> standalone IME log pattern-matching mode. Plan §4.x M4.6.δ.
|
|
/// <para>
|
|
/// Parses IME log files from a given path (single file or directory) with the same pattern
|
|
/// set the live agent uses, and writes all matches to <c>ime_pattern_matching.log</c> next
|
|
/// to the input. Parity-port of Legacy <c>Program.ImeMatchingMode.cs</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
public static partial class Program
|
|
{
|
|
// Same file-name patterns the live ImeLogTracker watches.
|
|
private static readonly string[] ImeLogFilePatterns = new[]
|
|
{
|
|
"IntuneManagementExtension.log",
|
|
"_IntuneManagementExtension.log",
|
|
"IntuneManagementExtension-????????-??????.log",
|
|
"AppWorkload.log",
|
|
"AppWorkload-????????-??????.log",
|
|
"AgentExecutor.log",
|
|
"AgentExecutor-????????-??????.log",
|
|
"HealthScripts.log",
|
|
"HealthScripts-????????-??????.log",
|
|
};
|
|
|
|
private const string ImePatternsGuidPattern = @"(?<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})";
|
|
private const string ImePatternsGitHubUrl = "https://raw.githubusercontent.com/okieselbach/Autopilot-Monitor/refs/heads/main/rules/dist/ime-log-patterns.json";
|
|
|
|
internal static int RunImeMatchingMode(string[] args)
|
|
{
|
|
Console.WriteLine("Autopilot Monitor Agent V2 — IME Pattern Matching");
|
|
Console.WriteLine("==================================================");
|
|
Console.WriteLine();
|
|
|
|
var pathIndex = Array.IndexOf(args, "--run-ime-matching");
|
|
if (pathIndex < 0 || pathIndex + 1 >= args.Length)
|
|
{
|
|
Console.Error.WriteLine("ERROR: --run-ime-matching requires a path argument (file or directory).");
|
|
Console.Error.WriteLine("Usage: AutopilotMonitor.Agent.exe --run-ime-matching <path> [--patterns <file>]");
|
|
return 1;
|
|
}
|
|
|
|
var inputPath = args[pathIndex + 1];
|
|
var patternsFile = GetArgValue(args, "--patterns");
|
|
|
|
var logFiles = new List<string>();
|
|
string outputDir;
|
|
|
|
if (File.Exists(inputPath))
|
|
{
|
|
logFiles.Add(Path.GetFullPath(inputPath));
|
|
outputDir = Path.GetDirectoryName(Path.GetFullPath(inputPath));
|
|
Console.WriteLine($"Mode: Single file");
|
|
Console.WriteLine($"Input: {Path.GetFullPath(inputPath)}");
|
|
}
|
|
else if (Directory.Exists(inputPath))
|
|
{
|
|
var fullDir = Path.GetFullPath(inputPath);
|
|
foreach (var pattern in ImeLogFilePatterns)
|
|
{
|
|
try { logFiles.AddRange(Directory.GetFiles(fullDir, pattern)); }
|
|
catch (DirectoryNotFoundException) { }
|
|
}
|
|
logFiles.Sort(StringComparer.OrdinalIgnoreCase);
|
|
outputDir = fullDir;
|
|
|
|
Console.WriteLine($"Mode: Directory scan");
|
|
Console.WriteLine($"Input: {fullDir}");
|
|
Console.WriteLine($"Log files: {logFiles.Count}");
|
|
|
|
if (logFiles.Count == 0)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("No IME log files found in the specified directory.");
|
|
Console.WriteLine("Expected files: IntuneManagementExtension*.log, AppWorkload*.log, AgentExecutor*.log, HealthScripts*.log");
|
|
return 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.Error.WriteLine($"ERROR: Path not found: {inputPath}");
|
|
return 1;
|
|
}
|
|
|
|
var patterns = !string.IsNullOrEmpty(patternsFile)
|
|
? LoadImePatternsFromFile(patternsFile)
|
|
: LoadImePatternsFromGitHub();
|
|
|
|
if (patterns == null || patterns.Count == 0)
|
|
{
|
|
Console.Error.WriteLine("ERROR: No patterns loaded. Cannot proceed.");
|
|
return 1;
|
|
}
|
|
|
|
Console.WriteLine($"Loaded: {patterns.Count} patterns ({patterns.Count(p => p.Enabled)} enabled)");
|
|
|
|
var compiledPatterns = CompileImeMatchingPatterns(patterns);
|
|
Console.WriteLine($"Compiled: {compiledPatterns.Count} patterns");
|
|
|
|
var outputPath = Path.Combine(outputDir, "ime_pattern_matching.log");
|
|
Console.WriteLine($"Output: {outputPath}");
|
|
Console.WriteLine();
|
|
|
|
if (File.Exists(outputPath)) File.Delete(outputPath);
|
|
|
|
int totalLines = 0;
|
|
int totalMatches = 0;
|
|
var matchCountByPattern = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var filePath in logFiles)
|
|
{
|
|
var fileName = Path.GetFileName(filePath);
|
|
Console.Write($" Scanning {fileName}...");
|
|
|
|
int fileLines = 0;
|
|
int fileMatches = 0;
|
|
|
|
try
|
|
{
|
|
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
|
using (var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
|
{
|
|
string line;
|
|
while ((line = reader.ReadLine()) != null)
|
|
{
|
|
fileLines++;
|
|
string messageToMatch = CmTraceLogParser.TryParseLine(line, out var entry)
|
|
? entry.Message
|
|
: line;
|
|
if (string.IsNullOrEmpty(messageToMatch)) continue;
|
|
|
|
foreach (var pattern in compiledPatterns)
|
|
{
|
|
try
|
|
{
|
|
var match = pattern.Regex.Match(messageToMatch);
|
|
if (match.Success)
|
|
{
|
|
fileMatches++;
|
|
var logEntry = $"[{fileName}] [{pattern.PatternId}] {line}";
|
|
File.AppendAllText(outputPath, logEntry + Environment.NewLine);
|
|
|
|
if (!matchCountByPattern.ContainsKey(pattern.PatternId))
|
|
matchCountByPattern[pattern.PatternId] = 0;
|
|
matchCountByPattern[pattern.PatternId]++;
|
|
}
|
|
}
|
|
catch (RegexMatchTimeoutException) { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" ERROR: {ex.Message}");
|
|
continue;
|
|
}
|
|
|
|
totalLines += fileLines;
|
|
totalMatches += fileMatches;
|
|
Console.WriteLine($" {fileLines} lines, {fileMatches} matches");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("=== Summary ===");
|
|
Console.WriteLine($"Files scanned: {logFiles.Count}");
|
|
Console.WriteLine($"Lines parsed: {totalLines}");
|
|
Console.WriteLine($"Total matches: {totalMatches}");
|
|
Console.WriteLine($"Output: {outputPath}");
|
|
|
|
if (matchCountByPattern.Count > 0)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("Matches by pattern:");
|
|
foreach (var kvp in matchCountByPattern.OrderByDescending(x => x.Value))
|
|
Console.WriteLine($" {kvp.Key,-40} {kvp.Value,5}");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static List<ImeLogPattern> LoadImePatternsFromGitHub()
|
|
{
|
|
try
|
|
{
|
|
Console.Write(" Downloading patterns from GitHub...");
|
|
using (var handler = new HttpClientHandler())
|
|
using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) })
|
|
{
|
|
var json = client.GetStringAsync(ImePatternsGitHubUrl).GetAwaiter().GetResult();
|
|
var wrapper = JsonConvert.DeserializeObject<PatternsFileWrapper>(json);
|
|
var patterns = wrapper?.Rules ?? new List<ImeLogPattern>();
|
|
Console.WriteLine(" OK");
|
|
return patterns;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" FAILED: {ex.Message}");
|
|
|
|
// Fallback: cached remote-config.json (written by previous live agent runs).
|
|
try
|
|
{
|
|
var cachePath = Environment.ExpandEnvironmentVariables(CachedRemoteConfigPath);
|
|
if (File.Exists(cachePath))
|
|
{
|
|
Console.Write(" Falling back to cached remote-config.json...");
|
|
var json = File.ReadAllText(cachePath);
|
|
var config = JsonConvert.DeserializeObject<CachedConfigWrapper>(json);
|
|
if (config?.ImeLogPatterns != null && config.ImeLogPatterns.Count > 0)
|
|
{
|
|
Console.WriteLine(" OK");
|
|
return config.ImeLogPatterns;
|
|
}
|
|
}
|
|
}
|
|
catch { /* falls through to null */ }
|
|
|
|
Console.WriteLine(" No fallback available.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static List<ImeLogPattern> LoadImePatternsFromFile(string filePath)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(filePath))
|
|
{
|
|
Console.Error.WriteLine($"ERROR: Patterns file not found: {filePath}");
|
|
return null;
|
|
}
|
|
var json = File.ReadAllText(filePath);
|
|
var wrapper = JsonConvert.DeserializeObject<PatternsFileWrapper>(json);
|
|
return wrapper?.Rules ?? new List<ImeLogPattern>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"ERROR: Failed to load patterns file: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static List<CompiledImeMatchPattern> CompileImeMatchingPatterns(List<ImeLogPattern> patterns)
|
|
{
|
|
var compiled = new List<CompiledImeMatchPattern>();
|
|
foreach (var pattern in patterns.Where(p => p.Enabled))
|
|
{
|
|
try
|
|
{
|
|
var regexStr = pattern.Pattern.Replace("{GUID}", ImePatternsGuidPattern);
|
|
var regex = new Regex(
|
|
regexStr,
|
|
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline,
|
|
TimeSpan.FromSeconds(1));
|
|
compiled.Add(new CompiledImeMatchPattern { PatternId = pattern.PatternId, Regex = regex });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" WARNING: Failed to compile pattern {pattern.PatternId}: {ex.Message}");
|
|
}
|
|
}
|
|
return compiled;
|
|
}
|
|
|
|
private sealed class CompiledImeMatchPattern
|
|
{
|
|
public string PatternId { get; set; }
|
|
public Regex Regex { get; set; }
|
|
}
|
|
|
|
private sealed class PatternsFileWrapper
|
|
{
|
|
[JsonProperty("rules")]
|
|
public List<ImeLogPattern> Rules { get; set; } = new List<ImeLogPattern>();
|
|
}
|
|
|
|
private sealed class CachedConfigWrapper
|
|
{
|
|
[JsonProperty("imeLogPatterns")]
|
|
public List<ImeLogPattern> ImeLogPatterns { get; set; }
|
|
}
|
|
}
|
|
}
|