forked from Azure/azure-sdk-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLogAnalysisHelper.cs
More file actions
159 lines (137 loc) · 6.08 KB
/
LogAnalysisHelper.cs
File metadata and controls
159 lines (137 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
using System.Security.Policy;
using Azure.Sdk.Tools.Cli.Models;
namespace Azure.Sdk.Tools.Cli.Helpers;
public interface ILogAnalysisHelper
{
Task<List<LogEntry>> AnalyzeLogContent(string filePath, List<string>? keywords, int? beforeLines, int? afterLines);
}
public class Keyword
{
public string Value { get; }
public Func<string, bool> MatchFunc { get; }
public Keyword(string keyword)
{
Value = keyword;
MatchFunc = input => input.Contains(keyword, StringComparison.OrdinalIgnoreCase);
}
public Keyword(string keyword, Func<string, bool> matchFunc)
{
this.Value = keyword;
this.MatchFunc = matchFunc;
}
public bool Matches(string input) => MatchFunc(input);
public override string ToString()
{
return Value;
}
public static implicit operator Keyword(string keyword) => new(keyword);
public static implicit operator string(Keyword keyword) => keyword.Value;
}
public class LogAnalysisHelper(ILogger<LogAnalysisHelper> logger) : ILogAnalysisHelper
{
private readonly ILogger<LogAnalysisHelper> logger = logger;
public const int DEFAULT_BEFORE_LINES = 20;
public const int DEFAULT_AFTER_LINES = 20;
// Built-in error keywords for robust error detection
private static readonly HashSet<Keyword> defaultErrorKeywords =
[
// ANSI color codes (red, etc.)
"[31m",
// custom keyword comparers
new("error", (i) => {
var falsePositives = new[] { "no error", "0 error", "any errors", "`error`", "error.type" };
var hasFalsePositives = falsePositives.Any(fp => i.Contains(fp, StringComparison.OrdinalIgnoreCase));
return hasFalsePositives ? false : i.Contains("error", StringComparison.OrdinalIgnoreCase);
}),
new("fail", (i) => {
var falsePositives = new[] { "no fail", "0 fail", "any fail" };
var hasFalsePositives = falsePositives.Any(fp => i.Contains(fp, StringComparison.OrdinalIgnoreCase));
return hasFalsePositives ? false : i.Contains("fail", StringComparison.OrdinalIgnoreCase);
}),
// Common error indicators
"exception", "aborted", "fatal", "critical", "panic", "crash", "crashed", "segfault", "stacktrace",
// Network/connection errors
"timeout", "timed out", "unreachable", "refused",
// Permission/access errors
"access denied", "permission denied", "unauthorized", "forbidden", "token expired",
// File/IO errors
"file not found", "directory not found", "no such file", "permission denied", "disk full", "out of space",
// Memory/resource errors
"out of memory", "memory leak", "resource exhausted", "quota exceeded", "too many", "limit exceeded", "overflow", "underflow",
// Process/service errors
"service unavailable", "service down", "process died", "killed", "terminated", "non-zero exit",
// HTTP/API errors
"bad request"
];
public async Task<List<LogEntry>> AnalyzeLogContent(string filePath, List<string>? keywordOverrides, int? beforeLines, int? afterLines)
{
using var stream = new StreamReader(filePath);
return await AnalyzeLogContent(stream, keywordOverrides, beforeLines, afterLines, filePath: filePath);
}
public async Task<List<LogEntry>> AnalyzeLogContent(StreamReader reader, List<string>? keywordOverrides, int? beforeLines, int? afterLines, string url = "", string filePath = "")
{
var keywords = defaultErrorKeywords;
if (keywordOverrides?.Count > 0)
{
keywords = [];
foreach (var keyword in keywordOverrides)
{
keywords.Add(keyword);
}
}
beforeLines ??= DEFAULT_BEFORE_LINES;
afterLines ??= DEFAULT_AFTER_LINES;
var before = new Queue<string>((int)beforeLines);
var after = new Queue<string>((int)afterLines);
var maxAfterLines = afterLines ?? 100;
var errors = new List<LogEntry>();
var lineNumber = 0;
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
lineNumber++;
// check > not >= because an error match will take up an extra slot
if (before.Count > beforeLines)
{
before.Dequeue();
}
before.Enqueue(line);
var matchedKeywords = keywords.Where(k => k.Matches(line)).ToList();
if (matchedKeywords.Count > 0)
{
logger.LogDebug("Found error matches at line {lineNumber}: {keywords}. Line: {line}", lineNumber, string.Join(", ", matchedKeywords), line);
while (after.Count < afterLines && (line = await reader.ReadLineAsync()) != null)
{
lineNumber++;
matchedKeywords = keywords.Where(k => k.Matches(line)).ToList();
// Keep seeking if we find new errors while collecting the trailing error context
if (matchedKeywords.Count > 0 && afterLines < maxAfterLines)
{
logger.LogDebug("Found contiguous error matches at line {lineNumber}: {keywords}", lineNumber, string.Join(", ", matchedKeywords));
afterLines++;
}
after.Enqueue(line);
}
var fullContext = before.Concat(after).ToList();
before.Clear();
after.Clear();
var entry = new LogEntry
{
Line = lineNumber,
Message = string.Join(Environment.NewLine, fullContext)
};
if (!string.IsNullOrEmpty(url))
{
entry.Url = url;
}
if (!string.IsNullOrEmpty(filePath))
{
entry.File = filePath;
}
errors.Add(entry);
}
}
logger.LogDebug("Found {errorCount} non-contiguous errors in {filePath}", errors.Count, filePath);
return errors;
}
}