Skip to content

Commit d33256f

Browse files
author
Timothy Dodd
committed
Enhance logging and JSON parsing capabilities
Updated logging configuration in `Program.cs` to clear existing providers and set a new console format with UTC timestamps. Improved structured logging in `LogSummaryHourlyBackgroundService.cs`. Enhanced `LogParser.cs` to support JSON parsing for timestamps and log levels, and refactored the `ParseContainerLogFormat` method for better message extraction. Added new methods for parsing JSON timestamps and log levels.
1 parent 5c343fd commit d33256f

4 files changed

Lines changed: 156 additions & 17 deletions

File tree

src/LogMkAgent/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ public static async Task Main(string[] args)
5050
// Configure logging
5151
services.AddLogging(logging =>
5252
{
53+
logging.ClearProviders();
5354
logging.AddSimpleConsole(options =>
5455
{
5556
options.SingleLine = true;
56-
options.IncludeScopes = true;
57-
options.TimestampFormat = "HH:mm:ss ";
57+
options.IncludeScopes = false;
58+
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
59+
options.UseUtcTimestamp = true;
5860
});
5961
});
6062

src/LogMkApi/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ public class Program
2525
public static void Main(string[] args)
2626
{
2727
var builder = WebApplication.CreateBuilder(args);
28+
29+
// Configure cleaner logging format
30+
builder.Logging.ClearProviders();
31+
builder.Logging.AddSimpleConsole(options =>
32+
{
33+
options.SingleLine = true;
34+
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
35+
options.UseUtcTimestamp = true;
36+
options.IncludeScopes = false;
37+
});
38+
2839
builder.Services.AddRequestDecompression();
2940
builder.Services.AddResponseCompression(options =>
3041
{

src/LogMkApi/Services/LogSummaryHourlyBackgroundService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3535
var nextRun = CalculateNextRunTime(now);
3636
var delay = nextRun - now;
3737

38-
_logger.LogInformation($"Next log summary update scheduled for {nextRun:yyyy-MM-dd HH:mm:ss}");
38+
_logger.LogInformation("Next log summary update scheduled for {NextRun}", nextRun.ToString("yyyy-MM-dd HH:mm:ss"));
3939

4040
// Wait until the next hour
4141
await Task.Delay(delay, stoppingToken);

src/LogMkCommon/LogParser.cs

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using System.Text.Json;
23
using System.Text.RegularExpressions;
34

45
namespace LogMkCommon;
@@ -13,19 +14,32 @@ public static class LogParser
1314
if (string.IsNullOrWhiteSpace(line))
1415
return null;
1516

17+
// Check if line contains JSON and try to parse timestamp from it
18+
if (line.TrimStart().StartsWith('{') && line.TrimEnd().EndsWith('}'))
19+
{
20+
var jsonTimestamp = ParseJsonTimestamp(line);
21+
if (jsonTimestamp.HasValue)
22+
return jsonTimestamp;
23+
}
24+
1625
// First try to parse container log format timestamp (at the beginning)
1726
var firstSpace = line.IndexOf(' ');
1827
if (firstSpace > 0)
1928
{
2029
var timestampStr = line.Substring(0, firstSpace);
21-
var containerTimestamp = ParseContainerTimestamp(timestampStr);
22-
if (containerTimestamp.HasValue)
23-
return containerTimestamp;
30+
// Skip malformed or partial timestamps (like "696426698Z")
31+
if (timestampStr.Length >= 20 && timestampStr.Contains("T"))
32+
{
33+
var containerTimestamp = ParseContainerTimestamp(timestampStr);
34+
if (containerTimestamp.HasValue)
35+
return containerTimestamp;
36+
}
2437
}
2538

2639
// Try common timestamp patterns anywhere in the line
2740
var patterns = new[]
2841
{
42+
@"\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\]", // [2025-09-03 22:44:07] format
2943
@"(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)", // ISO 8601
3044
@"(\d{4}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2})", // 2024/01/15 12:34:56
3145
@"(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}:\d{2})", // 01/15/2024 12:34:56
@@ -67,6 +81,14 @@ private static LogLevel GetLogLevel(string logLine)
6781
if (string.IsNullOrEmpty(logLine))
6882
return LogLevel.Information;
6983

84+
// Check if line is JSON and try to parse level from it
85+
if (logLine.TrimStart().StartsWith('{') && logLine.TrimEnd().EndsWith('}'))
86+
{
87+
var jsonLevel = ParseJsonLogLevel(logLine);
88+
if (jsonLevel != LogLevel.Information) // If we found a specific level in JSON
89+
return jsonLevel;
90+
}
91+
7092
var upperLine = logLine.ToUpperInvariant();
7193

7294
// Check for common log level patterns with boundaries
@@ -103,7 +125,8 @@ private static bool ContainsLogLevel(string upperLine, params string[] patterns)
103125

104126
public static string ParseContainerLogFormat(string originalLine, string cleanLine)
105127
{
106-
// Container log format: "timestamp stdout/stderr actual-log-message"
128+
// Container log format: "timestamp stdout/stderr F/P actual-log-message"
129+
// F = full line, P = partial line
107130
var firstSpace = originalLine.IndexOf(' ');
108131
if (firstSpace <= 0)
109132
return cleanLine;
@@ -112,20 +135,44 @@ public static string ParseContainerLogFormat(string originalLine, string cleanLi
112135
if (secondSpace <= 0)
113136
return cleanLine;
114137

115-
var thirdSpace = originalLine.IndexOf(' ', secondSpace + 1);
116-
if (thirdSpace <= 0)
117-
return cleanLine;
118-
119138
var outType = originalLine.Substring(firstSpace + 1, secondSpace - firstSpace - 1);
120139
if (outType == "stdout" || outType == "stderr")
121140
{
122-
// Calculate the position adjustment due to ANSI escape sequences removal
123-
var lengthDiff = originalLine.Length - cleanLine.Length;
124-
var adjustedPosition = thirdSpace + 1 - lengthDiff;
125-
126-
if (adjustedPosition >= 0 && adjustedPosition < cleanLine.Length)
141+
// Check for the F/P flag after stdout/stderr
142+
var thirdSpace = originalLine.IndexOf(' ', secondSpace + 1);
143+
if (thirdSpace <= 0)
144+
return cleanLine;
145+
146+
var flag = originalLine.Substring(secondSpace + 1, thirdSpace - secondSpace - 1);
147+
if (flag == "F" || flag == "P")
148+
{
149+
// Find the actual log message after the flag
150+
var fourthSpace = originalLine.IndexOf(' ', thirdSpace + 1);
151+
if (fourthSpace > 0)
152+
{
153+
// We need to find this position in the clean line
154+
// Count how many characters we need to skip in the clean line
155+
var prefixToSkip = originalLine.Substring(0, fourthSpace + 1);
156+
var cleanPrefixToSkip = RemoveANSIEscapeRegex.Replace(prefixToSkip, string.Empty);
157+
158+
if (cleanPrefixToSkip.Length < cleanLine.Length)
159+
{
160+
return cleanLine.Substring(cleanPrefixToSkip.Length);
161+
}
162+
}
163+
}
164+
else
127165
{
128-
return cleanLine.Substring(adjustedPosition);
166+
// Old format without F/P flag
167+
var actualMessageStart = thirdSpace + 1;
168+
// Calculate how many chars to skip in clean line
169+
var prefixToSkip = originalLine.Substring(0, actualMessageStart);
170+
var cleanPrefixToSkip = RemoveANSIEscapeRegex.Replace(prefixToSkip, string.Empty);
171+
172+
if (cleanPrefixToSkip.Length < cleanLine.Length)
173+
{
174+
return cleanLine.Substring(cleanPrefixToSkip.Length);
175+
}
129176
}
130177
}
131178

@@ -170,4 +217,83 @@ public static string RemoveANSIEscapeSequences(string line)
170217
{
171218
return RemoveANSIEscapeRegex.Replace(line, string.Empty);
172219
}
220+
221+
private static DateTimeOffset? ParseJsonTimestamp(string line)
222+
{
223+
try
224+
{
225+
using var doc = JsonDocument.Parse(line);
226+
var root = doc.RootElement;
227+
228+
// Try common timestamp field names
229+
string[] timestampFields = { "ts", "timestamp", "time", "@timestamp", "datetime", "date" };
230+
231+
foreach (var field in timestampFields)
232+
{
233+
if (root.TryGetProperty(field, out var tsElement))
234+
{
235+
var tsValue = tsElement.GetString();
236+
if (!string.IsNullOrEmpty(tsValue))
237+
{
238+
if (DateTimeOffset.TryParse(tsValue, out var timestamp))
239+
return timestamp;
240+
}
241+
}
242+
}
243+
244+
// Try parsing numeric timestamp (Unix epoch)
245+
if (root.TryGetProperty("timestamp", out var unixElement))
246+
{
247+
if (unixElement.ValueKind == JsonValueKind.Number)
248+
{
249+
var unixTime = unixElement.GetInt64();
250+
return DateTimeOffset.FromUnixTimeSeconds(unixTime);
251+
}
252+
}
253+
}
254+
catch
255+
{
256+
// Not valid JSON or parsing error, ignore
257+
}
258+
259+
return null;
260+
}
261+
262+
private static LogLevel ParseJsonLogLevel(string line)
263+
{
264+
try
265+
{
266+
using var doc = JsonDocument.Parse(line);
267+
var root = doc.RootElement;
268+
269+
// Try common log level field names
270+
string[] levelFields = { "level", "severity", "log_level", "logLevel", "@level" };
271+
272+
foreach (var field in levelFields)
273+
{
274+
if (root.TryGetProperty(field, out var levelElement))
275+
{
276+
var levelValue = levelElement.GetString()?.ToUpperInvariant();
277+
if (!string.IsNullOrEmpty(levelValue))
278+
{
279+
return levelValue switch
280+
{
281+
"ERROR" or "ERR" or "FATAL" or "CRITICAL" => LogLevel.Error,
282+
"WARN" or "WARNING" => LogLevel.Warning,
283+
"INFO" or "INFORMATION" => LogLevel.Information,
284+
"DEBUG" or "DBG" => LogLevel.Debug,
285+
"TRACE" or "TRC" or "VERBOSE" => LogLevel.Trace,
286+
_ => LogLevel.Information
287+
};
288+
}
289+
}
290+
}
291+
}
292+
catch
293+
{
294+
// Not valid JSON or parsing error, ignore
295+
}
296+
297+
return LogLevel.Information;
298+
}
173299
}

0 commit comments

Comments
 (0)