11using System . Globalization ;
2+ using System . Text . Json ;
23using System . Text . RegularExpressions ;
34
45namespace 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