要进一步增强MES日志分析工具,使其支持复杂的依赖规则,包括时间窗口、前置接口的最小调用次数,以及多个关键字段的联合匹配,我们需要修改数据结构、分析逻辑和配置文件格式。以下是实现思路和更新后的C#代码,基于之前的`EnhancedMESLogAnalyzer.cs`,并提供中文解释。
实现思路
1. 支持复杂规则:
- 时间窗口:允许指定前置接口调用必须在目标接口调用前的特定时间范围内(如前1小时内)。
- 最小调用次数:要求前置接口针对相同关键字段值至少成功调用指定次数(如至少2次)。
- 多关键字段联合匹配:支持多个关键字段(如`TraySn`和`LotSn`)的联合匹配,确保前置接口和目标接口的这些字段值一致。
2. 修改数据结构:
- 更新`DependencyRule`类,添加:
- `TimeWindowSeconds`:时间窗口(秒),限制前置接口调用时间范围。
- `MinCallCount`:前置接口的最小成功调用次数。
- `KeyFields`:支持多个关键字段(列表),替代单一`KeyField`。
- 保持`PrerequisiteStatus`和`AnalysisResult`结构,适应多字段匹配。
3. 更新配置文件:
- 修改`config.json`格式,支持新的字段:
- `TimeWindowSeconds`(可选,默认为无限)。
- `MinCallCount`(可选,默认为1)。
- `KeyFields`(字符串列表,替代`KeyField`)。
- 示例配置文件:
```json
[
{
"TargetApi": "/api/eapmanagement/ProductOut/CheckProductOut",
"PrerequisiteApis": ["/api/eapmanagement/TraySN/TraySNBind"],
"KeyFields": ["TraySn", "LotSn"],
"ErrorCode": "EAPManagement:04448",
"TimeWindowSeconds": 3600,
"MinCallCount": 1
}
]
```
4. 分析逻辑调整:
- 在`Analyze`方法中:
- 检查前置接口调用是否在指定时间窗口内。
- 验证成功调用次数是否达到`MinCallCount`。
- 确保所有`KeyFields`的值在前置接口和目标接口中匹配。
- 记录不符合规则的前置接口状态(未调用、调用失败或不足次数)。
5. 报告增强:
- 在报告中显示时间窗口和最小调用次数要求。
- 注明多字段匹配的实际值(如`TraySn`和`LotSn`)。
- 详细说明失败原因(如“调用次数不足”或“时间窗口外”)。
更新后的C#代码
以下是支持复杂规则的代码,基于之前的实现,添加了时间窗口、最小调用次数和多关键字段支持。
```x-csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ComplexMESLogAnalyzer
{
public class LogEntry
{
public DateTime Timestamp { get; set; }
public string LogLevel { get; set; }
public string Uri { get; set; }
public JObject Request { get; set; }
public JObject Response { get; set; }
public int Time { get; set; }
public string Status { get; set; }
public string RawMessage { get; set; }
}
public class DependencyRule
{
public string TargetApi { get; set; }
public List<string> PrerequisiteApis { get; set; }
public List<string> KeyFields { get; set; } // 支持多个关键字段
public string ErrorCode { get; set; }
public int? TimeWindowSeconds { get; set; } // 时间窗口(秒),可选
public int MinCallCount { get; set; } = 1; // 最小调用次数,默认为1
}
public class PrerequisiteStatus
{
public string PrerequisiteApi { get; set; }
public string Status { get; set; } // "NotCalled", "Failed", "Success", "InsufficientCalls", "OutOfTimeWindow"
public List<LogEntry> FailedCalls { get; set; }
public int SuccessfulCallCount { get; set; }
}
public class AnalysisResult
{
public LogEntry ErrorEntry { get; set; }
public List<PrerequisiteStatus> PrerequisiteStatuses { get; set; }
public Dictionary<string, string> KeyValues { get; set; } // 存储多个关键字段值
public string Details { get; set; }
}
public class MESLogAnalyzer
{
private readonly List<LogEntry> logEntries = new List<LogEntry>();
private List<DependencyRule> rules = new List<DependencyRule>();
public void LoadConfig(string configPath)
{
try
{
string configJson = File.ReadAllText(configPath);
rules = JsonConvert.DeserializeObject<List<DependencyRule>>(configJson);
}
catch (Exception ex)
{
throw new Exception($"Failed to load config file: {ex.Message}");
}
}
public void LoadLogFile(string filePath)
{
string[] lines = File.ReadAllLines(filePath);
Regex logPattern = new Regex(@"\[(.*?)\]\s*(\w+)\s*-\s*PostSync,uri:(.*?),request:(.*?),response:(.*?),time:(\d+),(\w+\s+\w+\s+\w+)");
foreach (string line in lines)
{
Match match = logPattern.Match(line);
if (match.Success)
{
try
{
var entry = new LogEntry
{
Timestamp = DateTime.Parse(match.Groups[1].Value),
LogLevel = match.Groups[2].Value,
Uri = match.Groups[3].Value,
Request = JsonConvert.DeserializeObject<JObject>(match.Groups[4].Value),
Response = JsonConvert.DeserializeObject<JObject>(match.Groups[5].Value),
Time = int.Parse(match.Groups[6].Value),
Status = match.Groups[7].Value,
RawMessage = line
};
logEntries.Add(entry);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to parse line: {line}\nError: {ex.Message}");
}
}
}
}
public List<AnalysisResult> Analyze()
{
var results = new List<AnalysisResult>();
foreach (var entry in logEntries.Where(e => e.Response["isError"]?.Value<bool>() == true))
{
foreach (var rule in rules)
{
if (entry.Uri.EndsWith(rule.TargetApi) && entry.Response["code"]?.Value<string>() == rule.ErrorCode)
{
// 提取所有关键字段值
var keyValues = new Dictionary<string, string>();
bool allKeyFieldsValid = true;
foreach (var keyField in rule.KeyFields)
{
string keyValue = entry.Request[keyField]?.Value<string>();
if (string.IsNullOrEmpty(keyValue))
{
allKeyFieldsValid = false;
break;
}
keyValues[keyField] = keyValue;
}
if (!allKeyFieldsValid)
continue;
var prereqStatuses = new List<PrerequisiteStatus>();
bool hasIssue = false;
foreach (var prereqApi in rule.PrerequisiteApis)
{
// 查找前置接口调用,匹配所有关键字段
var prereqCalls = logEntries
.Where(e => e.Timestamp < entry.Timestamp &&
e.Uri.EndsWith(prereqApi) &&
rule.KeyFields.All(kf => e.Request[kf]?.Value<string>() == keyValues[kf]))
.ToList();
var status = new PrerequisiteStatus
{
PrerequisiteApi = prereqApi,
FailedCalls = new List<LogEntry>(),
SuccessfulCallCount = 0
};
if (!prereqCalls.Any())
{
status.Status = "NotCalled";
hasIssue = true;
}
else
{
// 应用时间窗口过滤
var validCalls = prereqCalls;
if (rule.TimeWindowSeconds.HasValue)
{
var timeWindow = TimeSpan.FromSeconds(rule.TimeWindowSeconds.Value);
validCalls = prereqCalls
.Where(c => entry.Timestamp - c.Timestamp <= timeWindow)
.ToList();
}
var successfulCalls = validCalls
.Where(c => c.Response["isError"]?.Value<bool>() == false)
.ToList();
var failedCalls = validCalls
.Where(c => c.Response["isError"]?.Value<bool>() == true)
.ToList();
status.SuccessfulCallCount = successfulCalls.Count;
status.FailedCalls = failedCalls;
if (successfulCalls.Count >= rule.MinCallCount)
{
status.Status = "Success";
}
else if (validCalls.Any())
{
status.Status = failedCalls.Any() ? "Failed" : "InsufficientCalls";
hasIssue = true;
}
else
{
status.Status = "OutOfTimeWindow";
hasIssue = true;
}
}
prereqStatuses.Add(status);
}
if (hasIssue)
{
var details = new List<string>
{
$"Error in {rule.TargetApi} for {string.Join(", ", keyValues.Select(kv => $"{kv.Key}: '{kv.Value}'"))} likely caused by issues with prerequisite APIs:"
};
foreach (var status in prereqStatuses)
{
details.Add($"- {status.PrerequisiteApi}: {status.Status} (Successful calls: {status.SuccessfulCallCount}, Required: {rule.MinCallCount})");
if (status.Status == "Failed" && status.FailedCalls.Any())
{
details.Add(" Failed calls:");
foreach (var failedCall in status.FailedCalls)
{
details.Add($" Timestamp: {failedCall.Timestamp}, Error: {failedCall.Response["message"]?.Value<string>()}");
}
}
if (rule.TimeWindowSeconds.HasValue)
{
details.Add($" Time Window: Within {rule.TimeWindowSeconds.Value} seconds");
}
}
results.Add(new AnalysisResult
{
ErrorEntry = entry,
PrerequisiteStatuses = prereqStatuses,
KeyValues = keyValues,
Details = string.Join("\n", details)
});
}
}
}
}
return results;
}
public void GenerateReport(List<AnalysisResult> results, string outputPath)
{
using (StreamWriter writer = new StreamWriter(outputPath))
{
writer.WriteLine("MES Log Analysis Report");
writer.WriteLine("=====================");
writer.WriteLine($"Generated on: {DateTime.Now}");
writer.WriteLine($"Total Errors Analyzed: {results.Count}");
writer.WriteLine();
foreach (var result in results)
{
writer.WriteLine($"Error Timestamp: {result.ErrorEntry.Timestamp}");
writer.WriteLine($"Error API: {result.ErrorEntry.Uri}");
writer.WriteLine($"Error Message: {result.ErrorEntry.Response["message"]?.Value<string>()}");
writer.WriteLine($"Key Field Values: {string.Join(", ", result.KeyValues.Select(kv => $"{kv.Key}: {kv.Value}"))}");
writer.WriteLine($"Details:\n{result.Details}");
writer.WriteLine($"Raw Log: {result.ErrorEntry.RawMessage}");
writer.WriteLine();
}
if (results.Count == 0)
writer.WriteLine("No errors matched the defined dependency rules.");
}
}
public static void Main(string[] args)
{
if (args.Length < 3)
{
Console.WriteLine("Usage: ComplexMESLogAnalyzer <logFilePath> <configFilePath> <outputReportPath>");
return;
}
string logFilePath = args[0];
string configFilePath = args[1];
string outputPath = args[2];
try
{
var analyzer = new MESLogAnalyzer();
analyzer.LoadConfig(configFilePath);
analyzer.LoadLogFile(logFilePath);
var results = analyzer.Analyze();
analyzer.GenerateReport(results, outputPath);
Console.WriteLine($"Analysis complete. Report generated at {outputPath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}
```
配置文件示例
创建一个`config.json`文件,包含时间窗口、最小调用次数和多关键字段:
```json
[
{
"TargetApi": "/api/eapmanagement/ProductOut/CheckProductOut",
"PrerequisiteApis": [
"/api/eapmanagement/TraySN/TraySNBind",
"/api/eapmanagement/FixtureWithDut/FixtureBindDut"
],
"KeyFields": ["TraySn", "LotSn"],
"ErrorCode": "EAPManagement:04448",
"TimeWindowSeconds": 3600,
"MinCallCount": 1
},
{
"TargetApi": "/api/eapmanagement/ProductIn/CheckProductIn",
"PrerequisiteApis": [
"/api/eapmanagement/FixtureWithDut/FixtureUnBindEquip"
],
"KeyFields": ["TraySn"],
"ErrorCode": "EAPManagement:04448",
"TimeWindowSeconds": 7200,
"MinCallCount": 2
},
{
"TargetApi": "/api/eapmanagement/Parameter/UploadParameterDatas",
"PrerequisiteApis": [
"/api/eapmanagement/TraySN/TraySNBind"
],
"KeyFields": ["LotSn"],
"ErrorCode": "EAPManagement:04448",
"MinCallCount": 1
}
]
```
使用方法
1. 环境准备:
- 安装.NET SDK。
- 添加`Newtonsoft.Json` NuGet包。
2. 准备配置文件:
- 创建`config.json`,定义依赖规则,包括`KeyFields`、`TimeWindowSeconds`和`MinCallCount`。
3. 编译和运行:
- 保存代码为`ComplexMESLogAnalyzer.cs`。
- 编译并运行:
```bash
dotnet run MES.log config.json output_report.txt
```
- `MES.log`:日志文件路径
- `config.json`:配置文件路径
- `output_report.txt`:输出报告路径
4. 输出:
- 报告列出错误详情,包括多关键字段值、时间窗口、最小调用次数要求,以及每个前置接口的状态(未调用、调用失败、调用次数不足、时间窗口外或成功)。
示例输出
基于提供的MES日志和配置文件,报告可能如下:
```
MES Log Analysis Report
=====================
Generated on: 2025-05-17 22:41:00
Total Errors Analyzed: 2
Error Timestamp: 2025-05-16 21:55:05
Error API: http://172.16.6.101:44388/api/eapmanagement/ProductOut/CheckProductOut
Error Message: 该托盘G-1-V400L-W2-H-2024-2-008没有需要出站的器件!
Key Field Values: TraySn: G-1-V400L-W2-H-2024-2-008, LotSn: P25190002
Details:
Error in /api/eapmanagement/ProductOut/CheckProductOut for TraySn: 'G-1-V400L-W2-H-2024-2-008', LotSn: 'P25190002' likely caused by issues with prerequisite APIs:
- /api/eapmanagement/TraySN/TraySNBind: NotCalled (Successful calls: 0, Required: 1)
Time Window: Within 3600 seconds
- /api/eapmanagement/FixtureWithDut/FixtureBindDut: NotCalled (Successful calls: 0, Required: 1)
Time Window: Within 3600 seconds
Raw Log: [2025-05-16 21:55:05,908] INFO - PostSync,uri:http://172.16.6.101:44388/api/eapmanagement/ProductOut/CheckProductOut,...
Error Timestamp: 2025-05-16 10:37:09
Error API: http://172.16.6.101:44388/api/eapmanagement/Parameter/UploadParameterDatas
Error Message: 设备8H-ECHJZZ-01未在产品工艺工序中绑定!
Key Field Values: LotSn: P25190002
Details:
Error in /api/eapmanagement/Parameter/UploadParameterDatas for LotSn: 'P25190002' likely caused by issues with prerequisite APIs:
- /api/eapmanagement/TraySN/TraySNBind: Failed (Successful calls: 0, Required: 1)
Failed calls:
Timestamp: 2025-05-16 08:42:29, Error: 托盘G-1-1-H-2024-11-013未绑定器件!
Raw Log: [2025-05-16 10:37:09,009] INFO - PostSync,uri:http://172.16.6.101:44388/api/eapmanagement/Parameter/UploadParameterDatas,...
```
代码说明
1. 数据结构更新:
- DependencyRule:
- `KeyFields`:替换`KeyField`,支持多字段列表。
- `TimeWindowSeconds`:可选,定义时间窗口(秒)。
- `MinCallCount`:最小成功调用次数,默认为1。
- PrerequisiteStatus:
- 添加`SuccessfulCallCount`,记录实际成功调用次数。
- 新增状态`InsufficientCalls`(调用次数不足)和`OverTimeWindow`(时间窗口外)。
- AnalysisResult:
- `KeyValues`:存储多个关键字段的名称和值。
2. 分析逻辑:
- 多关键字段匹配:
- 提取目标接口请求中的所有`KeyFields`值,存储在`KeyValues`字典。
- 前置接口调用需匹配所有`KeyFields`值。
- 时间窗口:
- 如果`TimeWindowSeconds`存在,过滤调用记录,确保时间差在指定范围内。
- 如果无有效调用,标记为`OutOfTimeWindow`。
- 最小调用次数:
- 计算时间窗口内的成功调用次数,比较`MinCallCount`。
- 如果成功次数不足,标记为`InsufficientCalls`(若有调用)或`Failed`(若全失败)。
- 状态判定:
- `NotCalled`:无调用记录。
- `OutOfTimeWindow`:有调用但不在时间窗口内。
- `Failed`:有调用但全失败。
- `InsufficientCalls`:有调用但成功次数不足。
- `Success`:成功次数满足要求。
3. 报告生成:
- 显示所有`KeyFields`的值(如`TraySn: xxx, LotSn: yyy`)。
- 每个前置接口显示状态、成功调用次数和要求次数。
- 注明时间窗口(如果有)。
- 失败调用列出时间戳和错误消息。
中文解释
功能改进
- 时间窗口:
- 用户可指定前置接口调用必须在目标接口前的多少秒内(如3600秒=1小时)。
- 例如,`TraySN/TraySNBind`必须在`ProductOut/CheckProductOut`前1小时内调用,否则标记为`OutOfTimeWindow`。
- 最小调用次数:
- 要求前置接口成功调用至少N次(如2次),否则标记为`InsufficientCalls`。
- 适用于需要多次初始化的场景,如多次绑定操作。
- 多关键字段联合匹配:
- 支持匹配多个字段(如`TraySn`和`LotSn`),确保前置接口和目标接口的这些字段值一致。
- 提高匹配精度,适应复杂依赖关系。
代码变化
- DependencyRule:
- `KeyFields`支持多字段,`TimeWindowSeconds`和`MinCallCount`增加规则灵活性。
- Analyze:
- 验证所有`KeyFields`值,匹配时要求完全一致。
- 应用时间窗口过滤,检查`entry.Timestamp - call.Timestamp <= TimeWindow`。
- 计算成功调用次数,比较`MinCallCount`。
- GenerateReport:
- 显示多字段值、时间窗口和调用次数要求。
- 新增状态说明(如`InsufficientCalls`)和失败调用详情。
使用场景
- 时间窗口:确保前置接口调用不过期,如绑定操作需在1小时内完成。
- 最小调用次数:适用于需要多次调用的场景,如多次设备初始化。
- 多关键字段:处理复杂流程,如同时依赖托盘号(`TraySn`)和批次号(`LotSn`)的接口。
扩展功能
1. 更复杂条件:
- 支持`KeyFields`的条件匹配(如正则表达式或部分匹配)。
- 添加调用顺序要求(如接口A必须在接口B前调用)。
2. 动态规则:
- 支持运行时修改`config.json`,无需重启。
- 添加命令行选项指定临时规则。
3. 性能优化:
- 对`KeyFields`值建立索引,加速多字段匹配。
- 使用并行处理分析多个错误。
4. 增强报告:
- 支持JSON或HTML报告格式。
- 添加统计信息(如各接口的错误率)。
注意事项
- 配置文件:
- 确保`KeyFields`中的字段名与日志请求JSON一致。
- `TimeWindowSeconds`和`MinCallCount`需合理设置,避免过于严格。
- 日志格式:
- 假设日志格式与样例一致,若不同,需调整正则表达式。
- 错误覆盖:
- 用户需根据MES系统补充规则,覆盖所有场景。
- 性能:
- 多关键字段匹配可能增加计算量,建议对大日志文件优化查询。
这个工具通过支持时间窗口、最小调用次数和多关键字段联合匹配,大幅提升了规则的灵活性和诊断精度,适用于复杂的MES日志分析场景。