@@ -46,45 +46,138 @@ public List<LogEventInfo> Parse(string[] lines, TextFileFormat? format)
4646 public List < LogEventInfo > Parse ( string [ ] lines )
4747 {
4848 var results = new List < LogEventInfo > ( ) ;
49+ LogEventInfo ? currentEvent = null ;
4950
5051 foreach ( var line in lines )
5152 {
52- if ( string . IsNullOrWhiteSpace ( line ) )
53- continue ;
53+ // Check if this line starts a new log entry (has a timestamp at the beginning )
54+ var isNewEntry = IsNewLogEntry ( line ) ;
5455
55- var logEvent = ParseLine ( line ) ;
56- if ( logEvent != null )
56+ if ( isNewEntry )
5757 {
58- results . Add ( logEvent ) ;
58+ // Save previous event if exists
59+ if ( currentEvent != null )
60+ {
61+ results . Add ( currentEvent ) ;
62+ }
63+
64+ // Parse new log entry
65+ currentEvent = ParseLine ( line ) ;
66+ }
67+ else if ( currentEvent != null )
68+ {
69+ // This is a continuation line - append to current message
70+ if ( string . IsNullOrWhiteSpace ( line ) )
71+ {
72+ // Preserve empty lines in multi-line messages (e.g., stack traces)
73+ currentEvent . Message += Environment . NewLine ;
74+ }
75+ else
76+ {
77+ currentEvent . Message += Environment . NewLine + line ;
78+ }
5979 }
80+ // If no current event and line doesn't start new entry, skip it (orphaned continuation line)
81+ }
82+
83+ // Add the last event if exists
84+ if ( currentEvent != null )
85+ {
86+ results . Add ( currentEvent ) ;
6087 }
6188
6289 return results ;
6390 }
6491
92+ /// <summary>
93+ /// Determines if a line starts a new log entry by checking for timestamp pattern at the beginning
94+ /// </summary>
95+ private bool IsNewLogEntry ( string line )
96+ {
97+ if ( string . IsNullOrWhiteSpace ( line ) )
98+ return false ;
99+
100+ // Check if line starts with a timestamp pattern
101+ var trimmedLine = line . TrimStart ( ) ;
102+ var timestampMatch = _timestampRegex . Match ( trimmedLine ) ;
103+
104+ // Timestamp must be at the start of the line (after trimming whitespace)
105+ return timestampMatch . Success && timestampMatch . Index == 0 ;
106+ }
107+
65108 /// <summary>
66109 /// Parses lines using the format configuration
67110 /// </summary>
68111 private List < LogEventInfo > ParseWithFormat ( string [ ] lines , TextFileFormat format )
69112 {
70113 var results = new List < LogEventInfo > ( ) ;
71114 var dataLines = lines . Skip ( format . StartLineIndex ) . ToList ( ) ;
115+ LogEventInfo ? currentEvent = null ;
72116
73117 foreach ( var line in dataLines )
74118 {
75- if ( string . IsNullOrWhiteSpace ( line ) )
76- continue ;
119+ // Check if this line starts a new log entry
120+ var isNewEntry = IsNewLogEntryWithFormat ( line , format ) ;
121+
122+ if ( isNewEntry )
123+ {
124+ // Save previous event if exists
125+ if ( currentEvent != null )
126+ {
127+ results . Add ( currentEvent ) ;
128+ }
77129
78- var logEvent = ParseLineWithFormat ( line , format ) ;
79- if ( logEvent != null )
130+ // Parse new log entry
131+ currentEvent = ParseLineWithFormat ( line , format ) ;
132+ }
133+ else if ( currentEvent != null )
80134 {
81- results . Add ( logEvent ) ;
135+ // This is a continuation line - append to current message
136+ if ( string . IsNullOrWhiteSpace ( line ) )
137+ {
138+ // Preserve empty lines in multi-line messages (e.g., stack traces)
139+ currentEvent . Message += Environment . NewLine ;
140+ }
141+ else
142+ {
143+ currentEvent . Message += Environment . NewLine + line ;
144+ }
82145 }
146+ // If no current event and line doesn't start new entry, skip it (orphaned continuation line)
147+ }
148+
149+ // Add the last event if exists
150+ if ( currentEvent != null )
151+ {
152+ results . Add ( currentEvent ) ;
83153 }
84154
85155 return results ;
86156 }
87157
158+ /// <summary>
159+ /// Determines if a line starts a new log entry when using format-based parsing
160+ /// </summary>
161+ private bool IsNewLogEntryWithFormat ( string line , TextFileFormat format )
162+ {
163+ if ( string . IsNullOrWhiteSpace ( line ) )
164+ return false ;
165+
166+ // If timestamp column is mapped, check if first column matches timestamp pattern
167+ if ( format . ColumnMapping . TimestampColumn >= 0 )
168+ {
169+ var parts = SplitLine ( line , format . Separator ) ;
170+ if ( format . ColumnMapping . TimestampColumn < parts . Length )
171+ {
172+ var timestampStr = parts [ format . ColumnMapping . TimestampColumn ] . Trim ( ) ;
173+ return TryParseTimestamp ( timestampStr , out _ ) ;
174+ }
175+ }
176+
177+ // Fallback to general timestamp detection
178+ return IsNewLogEntry ( line ) ;
179+ }
180+
88181 /// <summary>
89182 /// Parses a single line using the format configuration
90183 /// </summary>
@@ -166,6 +259,12 @@ private string[] SplitLine(string line, string separator)
166259 {
167260 try
168261 {
262+ // Check if line uses pipe-separated format
263+ if ( line . Contains ( " | " ) )
264+ {
265+ return ParsePipeSeparatedLine ( line ) ;
266+ }
267+
169268 // Try to extract timestamp
170269 var timestampMatch = _timestampRegex . Match ( line ) ;
171270 var timestamp = timestampMatch . Success && TryParseTimestamp ( timestampMatch . Value , out var dt )
@@ -200,6 +299,50 @@ private string[] SplitLine(string line, string separator)
200299 }
201300 }
202301
302+ /// <summary>
303+ /// Parses a pipe-separated log line: timestamp | level | logger | message
304+ /// </summary>
305+ private LogEventInfo ? ParsePipeSeparatedLine ( string line )
306+ {
307+ try
308+ {
309+ var parts = line . Split ( new [ ] { " | " } , StringSplitOptions . None ) ;
310+
311+ if ( parts . Length < 4 )
312+ {
313+ // Not enough parts for pipe format, fall back to regular parsing
314+ return null ;
315+ }
316+
317+ // Extract timestamp (first part)
318+ var timestampStr = parts [ 0 ] . Trim ( ) ;
319+ var timestamp = TryParseTimestamp ( timestampStr , out var dt ) ? dt : DateTime . Now ;
320+
321+ // Extract level (second part)
322+ var levelStr = parts [ 1 ] . Trim ( ) ;
323+ var level = ParseLogLevel ( levelStr ) ;
324+
325+ // Extract logger (third part)
326+ var loggerName = parts [ 2 ] . Trim ( ) ;
327+ if ( string . IsNullOrWhiteSpace ( loggerName ) )
328+ {
329+ loggerName = "Unknown" ;
330+ }
331+
332+ // Extract message (fourth part and beyond, in case message contains " | ")
333+ var message = string . Join ( " | " , parts . Skip ( 3 ) ) . Trim ( ) ;
334+
335+ return new LogEventInfo ( level , loggerName , message )
336+ {
337+ TimeStamp = timestamp
338+ } ;
339+ }
340+ catch
341+ {
342+ return null ;
343+ }
344+ }
345+
203346 /// <summary>
204347 /// Attempts to parse a timestamp string using various common formats
205348 /// </summary>
@@ -219,6 +362,9 @@ private bool TryParseTimestamp(string timestampStr, out DateTime result)
219362 "yyyy-MM-ddTHH:mm:ss.ffff" , // ISO 8601 with microseconds
220363 "yyyy-MM-ddTHH:mm:ss.fff" , // ISO 8601 with milliseconds
221364 "yyyy-MM-ddTHH:mm:ss" , // ISO 8601
365+ "yyyy-MM-ddTHH:mm:ss.ffffZ" , // ISO 8601 with microseconds and UTC (Z)
366+ "yyyy-MM-ddTHH:mm:ss.fffZ" , // ISO 8601 with milliseconds and UTC (Z)
367+ "yyyy-MM-ddTHH:mm:ssZ" , // ISO 8601 with UTC (Z)
222368 "yyyy-MM-dd HH:mm:ss.ffffK" , // With timezone
223369 "yyyy-MM-dd HH:mm:ss.fffK" , // With timezone
224370 "yyyy-MM-dd HH:mm:ssK" // With timezone
@@ -227,14 +373,24 @@ private bool TryParseTimestamp(string timestampStr, out DateTime result)
227373 // Try parsing with exact formats first (using InvariantCulture to avoid locale issues)
228374 foreach ( var format in formats )
229375 {
230- if ( DateTime . TryParseExact ( timestampStr , format , CultureInfo . InvariantCulture , DateTimeStyles . None , out result ) )
376+ // Use RoundtripKind for formats with 'Z' to preserve UTC
377+ var styles = format . EndsWith ( "Z" )
378+ ? DateTimeStyles . RoundtripKind
379+ : DateTimeStyles . None ;
380+
381+ if ( DateTime . TryParseExact ( timestampStr , format , CultureInfo . InvariantCulture , styles , out result ) )
231382 {
232383 return true ;
233384 }
234385 }
235386
236387 // Fallback: Try parsing with InvariantCulture (handles ISO formats better)
237- if ( DateTime . TryParse ( timestampStr , CultureInfo . InvariantCulture , DateTimeStyles . None , out result ) )
388+ // Use RoundtripKind to preserve UTC for timestamps ending with 'Z'
389+ var fallbackStyles = timestampStr . EndsWith ( "Z" , StringComparison . OrdinalIgnoreCase )
390+ ? DateTimeStyles . RoundtripKind
391+ : DateTimeStyles . None ;
392+
393+ if ( DateTime . TryParse ( timestampStr , CultureInfo . InvariantCulture , fallbackStyles , out result ) )
238394 {
239395 return true ;
240396 }
0 commit comments