2021-11-08 12:18:49 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
2022-06-02 10:31:43 -04:00
using System.Diagnostics ;
2021-11-08 12:18:49 -05:00
using System.Linq ;
2022-05-17 12:57:51 -04:00
using System.Reflection ;
2021-11-08 12:18:49 -05:00
using System.Text ;
using System.Text.RegularExpressions ;
2022-06-04 09:08:44 -04:00
using System.Threading.Tasks ;
2022-03-24 16:35:00 -04:00
using Microsoft.Extensions.Logging ;
2021-11-08 12:18:49 -05:00
namespace EpicGames.Core
{
/// <summary>
/// Confidence of a matched log event being the correct derivation
/// </summary>
public enum LogEventPriority
{
2022-06-22 13:23:01 -04:00
/// <summary>
/// Unspecified priority
/// </summary>
2021-11-08 12:18:49 -05:00
None ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Lowest confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
Lowest ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Low confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
Low ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Below normal confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
BelowNormal ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Normal confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
Normal ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Above normal confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
AboveNormal ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// High confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
High ,
2022-06-22 13:23:01 -04:00
/// <summary>
/// Highest confidence match
/// </summary>
2021-11-08 12:18:49 -05:00
Highest ,
}
2022-06-22 13:23:01 -04:00
/// <summary>
/// Information about a matched event
/// </summary>
2021-11-08 12:18:49 -05:00
public class LogEventMatch
{
2022-06-22 13:23:01 -04:00
/// <summary>
/// Confidence of the match
/// </summary>
2021-11-08 12:18:49 -05:00
public LogEventPriority Priority { get ; }
2022-06-22 13:23:01 -04:00
/// <summary>
/// Matched events
/// </summary>
2021-11-08 12:18:49 -05:00
public List < LogEvent > Events { get ; }
2022-06-22 13:23:01 -04:00
/// <summary>
/// Constructor
/// </summary>
2022-03-24 16:35:00 -04:00
public LogEventMatch ( LogEventPriority priority , LogEvent logEvent )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
Priority = priority ;
Events = new List < LogEvent > { logEvent } ;
2021-11-08 12:18:49 -05:00
}
2022-06-22 13:23:01 -04:00
/// <summary>
/// Constructor
/// </summary>
2022-03-24 16:35:00 -04:00
public LogEventMatch ( LogEventPriority priority , IEnumerable < LogEvent > events )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
Priority = priority ;
Events = events . ToList ( ) ;
2021-11-08 12:18:49 -05:00
}
}
/// <summary>
/// Interface for a class which matches error strings
/// </summary>
public interface ILogEventMatcher
{
/// <summary>
/// Attempt to match events from the given input buffer
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="cursor">The input buffer</param>
2021-11-08 12:18:49 -05:00
/// <returns>Information about the error that was matched, or null if an error was not matched</returns>
2022-03-24 16:35:00 -04:00
LogEventMatch ? Match ( ILogCursor cursor ) ;
2021-11-08 12:18:49 -05:00
}
/// <summary>
/// Turns raw text output into structured logging events
/// </summary>
public class LogEventParser : IDisposable
{
/// <summary>
/// List of event matchers for this parser
/// </summary>
public List < ILogEventMatcher > Matchers { get ; } = new List < ILogEventMatcher > ( ) ;
/// <summary>
/// List of patterns to ignore
/// </summary>
public List < Regex > IgnorePatterns { get ; } = new List < Regex > ( ) ;
/// <summary>
/// Buffer of input lines
/// </summary>
2022-03-24 16:35:00 -04:00
readonly LogBuffer _buffer ;
2021-11-08 12:18:49 -05:00
/// <summary>
/// Buffer for holding partial line data
/// </summary>
2022-10-03 17:01:13 -04:00
readonly ByteArrayBuilder _partialLine = new ByteArrayBuilder ( ) ;
2021-11-08 12:18:49 -05:00
/// <summary>
/// Whether matching is currently enabled
/// </summary>
2022-03-24 16:35:00 -04:00
int _matchingEnabled ;
2021-11-08 12:18:49 -05:00
/// <summary>
/// The inner logger
/// </summary>
2022-05-17 12:57:51 -04:00
ILogger _logger ;
2023-01-23 13:00:45 -05:00
/// <summary>
/// Log events sinks in addition to <see cref="_logger" />
/// </summary>
2023-01-24 14:10:29 -05:00
readonly List < ILogEventSink > _logEventSinks = new List < ILogEventSink > ( ) ;
2023-01-23 13:00:45 -05:00
2022-06-02 10:31:43 -04:00
/// <summary>
/// Timer for the parser being active
/// </summary>
readonly Stopwatch _timer = Stopwatch . StartNew ( ) ;
/// <summary>
/// Amount of time that the log parser has been processing events
/// </summary>
readonly Stopwatch _activeTimer = new Stopwatch ( ) ;
/// <summary>
/// Number of lines parsed in the last interval
/// </summary>
int _linesParsed = 0 ;
2022-05-17 12:57:51 -04:00
/// <summary>
/// Public accessor for the logger
/// </summary>
public ILogger Logger
{
get = > _logger ;
set = > _logger = value ;
}
2021-11-08 12:18:49 -05:00
/// <summary>
/// Constructor
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="logger">The logger to receive parsed output messages</param>
2023-01-23 13:00:45 -05:00
/// <param name="logEventSinks">Additional sinks to receive log events</param>
public LogEventParser ( ILogger logger , List < ILogEventSink > ? logEventSinks = null )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
_logger = logger ;
_buffer = new LogBuffer ( 50 ) ;
2023-01-23 13:00:45 -05:00
if ( logEventSinks ! = null )
{
_logEventSinks . AddRange ( logEventSinks ) ;
}
2021-11-08 12:18:49 -05:00
}
/// <inheritdoc/>
2022-10-03 17:01:13 -04:00
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
/// <summary>
/// Standard Dispose pattern method
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose ( bool disposing )
{
if ( disposing )
{
Flush ( ) ;
}
}
2021-11-08 12:18:49 -05:00
2022-05-17 12:57:51 -04:00
/// <summary>
/// Enumerate all the types that implement <see cref="ILogEventMatcher"/> in the given assembly, and create instances of them
/// </summary>
/// <param name="assembly">The assembly to enumerate matchers from</param>
public void AddMatchersFromAssembly ( Assembly assembly )
{
foreach ( Type type in assembly . GetTypes ( ) )
{
if ( type . IsClass & & typeof ( ILogEventMatcher ) . IsAssignableFrom ( type ) )
{
2022-05-31 15:32:26 -04:00
_logger . LogDebug ( "Adding event matcher: {Type}" , type . Name ) ;
2023-03-20 17:36:31 -04:00
try
{
ILogEventMatcher matcher = ( ILogEventMatcher ) Activator . CreateInstance ( type ) ! ;
Matchers . Add ( matcher ) ;
}
catch ( Exception ex )
{
_logger . LogDebug ( "Failed to add event matcher for {Type}: {Exception}" , type . Name , ex . ToString ( ) ) ;
}
2022-05-17 12:57:51 -04:00
}
}
}
2022-06-04 09:08:44 -04:00
/// <summary>
/// Read ignore patterns from the given root directory
/// </summary>
/// <param name="rootDir"></param>
/// <returns></returns>
public async Task ReadIgnorePatternsAsync ( DirectoryReference rootDir )
{
Stopwatch timer = Stopwatch . StartNew ( ) ;
List < DirectoryReference > baseDirs = new List < DirectoryReference > ( ) ;
baseDirs . Add ( rootDir ) ;
AddRestrictedDirs ( baseDirs , "Restricted" ) ;
AddRestrictedDirs ( baseDirs , "Platforms" ) ;
List < ( FileReference , Task ) > tasks = new List < ( FileReference , Task ) > ( ) ;
foreach ( DirectoryReference baseDir in baseDirs )
{
FileReference ignorePatternFile = FileReference . Combine ( baseDir , "Build" , "Horde" , "IgnorePatterns.txt" ) ;
if ( FileReference . Exists ( ignorePatternFile ) )
{
_logger . LogDebug ( "Reading ignore patterns from {File}..." , ignorePatternFile ) ;
tasks . Add ( ( ignorePatternFile , ReadIgnorePatternsAsync ( ignorePatternFile ) ) ) ;
}
}
foreach ( ( FileReference file , Task task ) in tasks )
{
try
{
await task ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Exception reading patterns from {File}: {Message}" , file , ex . Message ) ;
}
}
_logger . LogDebug ( "Took {TimeMs}ms to read ignore patterns" , timer . ElapsedMilliseconds ) ;
}
/// <summary>
/// Read ignore patterns from a single file
/// </summary>
/// <param name="ignorePatternFile"></param>
/// <returns></returns>
public async Task ReadIgnorePatternsAsync ( FileReference ignorePatternFile )
{
string [ ] lines = await FileReference . ReadAllLinesAsync ( ignorePatternFile ) ;
List < Regex > patterns = new List < Regex > ( ) ;
foreach ( string line in lines )
{
string trimLine = line . Trim ( ) ;
if ( trimLine . Length > 0 & & trimLine [ 0 ] ! = '#' )
{
patterns . Add ( new Regex ( trimLine ) ) ;
}
}
lock ( IgnorePatterns )
{
IgnorePatterns . AddRange ( patterns ) ;
}
}
static void AddRestrictedDirs ( List < DirectoryReference > directories , string subFolder )
{
int numDirs = directories . Count ;
for ( int idx = 0 ; idx < numDirs ; idx + + )
{
DirectoryReference subDir = DirectoryReference . Combine ( directories [ idx ] , subFolder ) ;
if ( DirectoryReference . Exists ( subDir ) )
{
directories . AddRange ( DirectoryReference . EnumerateDirectories ( subDir ) ) ;
}
}
}
2021-11-08 12:18:49 -05:00
/// <summary>
/// Writes a line to the event filter
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="line">The line to output</param>
public void WriteLine ( string line )
2021-11-08 12:18:49 -05:00
{
2022-05-17 12:57:51 -04:00
if ( line . Length > 0 & & line [ 0 ] = = '{' )
{
2023-10-27 10:05:40 -04:00
int length = line . Length ;
while ( length > 0 & & Char . IsWhiteSpace ( line [ length - 1 ] ) )
{
length - - ;
}
byte [ ] data = Encoding . UTF8 . GetBytes ( line , 0 , length ) ;
2022-05-23 09:58:48 -04:00
try
2022-05-17 12:57:51 -04:00
{
2022-05-23 09:58:48 -04:00
JsonLogEvent jsonEvent ;
if ( JsonLogEvent . TryParse ( data , out jsonEvent ) )
{
ProcessData ( true ) ;
_logger . Log ( jsonEvent . Level , jsonEvent . EventId , jsonEvent , null , JsonLogEvent . Format ) ;
return ;
}
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Exception while parsing log event" ) ;
2022-05-17 12:57:51 -04:00
}
}
_buffer . AddLine ( StringUtils . ParseEscapeCodes ( line ) ) ;
2021-11-08 12:18:49 -05:00
ProcessData ( false ) ;
}
/// <summary>
/// Writes data to the log parser
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="data">Data to write</param>
public void WriteData ( ReadOnlyMemory < byte > data )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
int baseIdx = 0 ;
int scanIdx = 0 ;
ReadOnlySpan < byte > span = data . Span ;
2021-11-08 12:18:49 -05:00
// Handle a partially existing line
2022-03-24 16:35:00 -04:00
if ( _partialLine . Length > 0 )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
for ( ; scanIdx < span . Length ; scanIdx + + )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
if ( span [ scanIdx ] = = '\n' )
2021-11-08 12:18:49 -05:00
{
2022-10-03 17:01:13 -04:00
_partialLine . WriteFixedLengthBytes ( span . Slice ( baseIdx , scanIdx - baseIdx ) ) ;
2021-11-08 12:18:49 -05:00
FlushPartialLine ( ) ;
2022-03-24 16:35:00 -04:00
baseIdx = + + scanIdx ;
2021-11-08 12:18:49 -05:00
break ;
}
}
}
// Handle any complete lines
2022-03-24 16:35:00 -04:00
for ( ; scanIdx < span . Length ; scanIdx + + )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
if ( span [ scanIdx ] = = '\n' )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
AddLine ( data . Slice ( baseIdx , scanIdx - baseIdx ) ) ;
baseIdx = scanIdx + 1 ;
2021-11-08 12:18:49 -05:00
}
}
// Add the rest of the text to the partial line buffer
2022-10-03 17:01:13 -04:00
_partialLine . WriteFixedLengthBytes ( span . Slice ( baseIdx ) ) ;
2021-11-08 12:18:49 -05:00
// Process the new data
ProcessData ( false ) ;
}
/// <summary>
/// Flushes the current contents of the parser
/// </summary>
public void Flush ( )
{
// If there's a partially written line, write that out first
2022-03-24 16:35:00 -04:00
if ( _partialLine . Length > 0 )
2021-11-08 12:18:49 -05:00
{
FlushPartialLine ( ) ;
}
// Process any remaining data
ProcessData ( true ) ;
}
/// <summary>
/// Adds a raw utf-8 string to the buffer
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="data">The string data</param>
private void AddLine ( ReadOnlyMemory < byte > data )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
if ( data . Length > 0 & & data . Span [ data . Length - 1 ] = = '\r' )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
data = data . Slice ( 0 , data . Length - 1 ) ;
2021-11-08 12:18:49 -05:00
}
2022-03-24 16:35:00 -04:00
if ( data . Length > 0 & & data . Span [ 0 ] = = '{' )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
JsonLogEvent jsonEvent ;
if ( JsonLogEvent . TryParse ( data , out jsonEvent ) )
2021-11-08 12:18:49 -05:00
{
ProcessData ( true ) ;
2023-01-24 12:27:02 -05:00
_logger . LogJsonLogEvent ( jsonEvent ) ;
2021-11-08 12:18:49 -05:00
return ;
}
}
2022-03-24 16:35:00 -04:00
_buffer . AddLine ( StringUtils . ParseEscapeCodes ( Encoding . UTF8 . GetString ( data . Span ) ) ) ;
2021-11-08 12:18:49 -05:00
}
/// <summary>
/// Writes the current partial line data, with the given data appended to it, then clear the buffer
/// </summary>
private void FlushPartialLine ( )
{
2022-10-03 17:01:13 -04:00
AddLine ( _partialLine . ToByteArray ( ) ) ;
_partialLine . Clear ( ) ;
2021-11-08 12:18:49 -05:00
}
/// <summary>
/// Process any data in the buffer
/// </summary>
/// <param name="bFlush">Whether we've reached the end of the stream</param>
void ProcessData ( bool bFlush )
{
2022-06-02 10:31:43 -04:00
_activeTimer . Start ( ) ;
int startLineCount = _buffer . Length ;
2022-03-24 16:35:00 -04:00
while ( _buffer . Length > 0 )
2021-11-08 12:18:49 -05:00
{
// Try to match an event
2022-03-24 16:35:00 -04:00
List < LogEvent > ? events = null ;
2022-09-30 13:49:15 -04:00
if ( Regex . IsMatch ( _buffer [ 0 ] ! , "<-- Suspend Log Parsing -->" , RegexOptions . IgnoreCase ) )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
_matchingEnabled - - ;
2021-11-08 12:18:49 -05:00
}
2022-09-30 13:49:15 -04:00
else if ( Regex . IsMatch ( _buffer [ 0 ] ! , "<-- Resume Log Parsing -->" , RegexOptions . IgnoreCase ) )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
_matchingEnabled + + ;
2021-11-08 12:18:49 -05:00
}
2022-03-24 16:35:00 -04:00
else if ( _matchingEnabled > = 0 )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
events = MatchEvent ( ) ;
2021-11-08 12:18:49 -05:00
}
// Bail out if we need more data
2022-03-24 16:35:00 -04:00
if ( _buffer . Length < 1024 & & ! bFlush & & _buffer . NeedMoreData )
2021-11-08 12:18:49 -05:00
{
break ;
}
// If we did match something, check if it's not negated by an ignore pattern. We typically have relatively few errors and many more ignore patterns than matchers, so it's quicker
// to check them in response to an identified error than to treat them as matchers of their own.
2022-03-24 16:35:00 -04:00
if ( events ! = null )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
foreach ( Regex ignorePattern in IgnorePatterns )
2021-11-08 12:18:49 -05:00
{
2022-09-30 13:49:15 -04:00
if ( ignorePattern . IsMatch ( _buffer [ 0 ] ! ) )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
events = null ;
2021-11-08 12:18:49 -05:00
break ;
}
}
}
// Report the error to the listeners
2022-03-24 16:35:00 -04:00
if ( events ! = null )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
WriteEvents ( events ) ;
_buffer . Advance ( events . Count ) ;
2021-11-08 12:18:49 -05:00
}
else
{
2022-03-24 16:35:00 -04:00
_logger . Log ( LogLevel . Information , KnownLogEvents . None , _buffer [ 0 ] ! , null , ( state , exception ) = > state ) ;
_buffer . MoveNext ( ) ;
2021-11-08 12:18:49 -05:00
}
}
2022-06-02 10:31:43 -04:00
_linesParsed + = startLineCount - _buffer . Length ;
_activeTimer . Stop ( ) ;
const double UpdateIntervalSeconds = 30.0 ;
double elapsedSeconds = _timer . Elapsed . TotalSeconds ;
if ( elapsedSeconds > UpdateIntervalSeconds )
{
const double WarnPct = 0.5 ;
double activeSeconds = _activeTimer . Elapsed . TotalSeconds ;
double activePct = activeSeconds / elapsedSeconds ;
if ( activePct > WarnPct )
{
_logger . LogInformation ( KnownLogEvents . Systemic_LogParserBottleneck , "EpicGames.Core.LogEventParser is taking a significant amount of CPU time: {Active:n1}s/{Total:n1}s ({Pct:n1}%). Processed {NumLines} lines in last {Interval} seconds ({NumLinesInBuffer} in buffer)." , activeSeconds , elapsedSeconds , activePct * 100.0 , _linesParsed , UpdateIntervalSeconds , _buffer . Length ) ;
}
_activeTimer . Reset ( ) ;
_timer . Restart ( ) ;
_linesParsed = 0 ;
}
2021-11-08 12:18:49 -05:00
}
/// <summary>
/// Try to match an event from the current buffer
/// </summary>
/// <returns>The matched event</returns>
private List < LogEvent > ? MatchEvent ( )
{
2022-03-24 16:35:00 -04:00
LogEventMatch ? currentMatch = null ;
foreach ( ILogEventMatcher matcher in Matchers )
2021-11-08 12:18:49 -05:00
{
2022-06-03 08:52:30 -04:00
LogEventMatch ? match = null ;
try
{
match = matcher . Match ( _buffer ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( KnownLogEvents . Systemic_LogEventMatcher , ex , "Exception while parsing log events with {Type}. Buffer size {Length}." , matcher . GetType ( ) . Name , _buffer . Length ) ;
}
2022-03-24 16:35:00 -04:00
if ( match ! = null )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
if ( currentMatch = = null | | match . Priority > currentMatch . Priority )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
currentMatch = match ;
2021-11-08 12:18:49 -05:00
}
}
}
2022-03-24 16:35:00 -04:00
return currentMatch ? . Events ;
2021-11-08 12:18:49 -05:00
}
/// <summary>
/// Writes an event to the log
/// </summary>
2022-06-22 13:23:01 -04:00
/// <param name="logEvents">The event to write</param>
2022-03-24 16:35:00 -04:00
protected virtual void WriteEvents ( List < LogEvent > logEvents )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
foreach ( LogEvent logEvent in logEvents )
2021-11-08 12:18:49 -05:00
{
2022-03-24 16:35:00 -04:00
_logger . Log ( logEvent . Level , logEvent . Id , logEvent , null , ( state , exception ) = > state . ToString ( ) ) ;
2023-01-23 13:00:45 -05:00
foreach ( ILogEventSink logEventSink in _logEventSinks )
{
logEventSink . ProcessEvent ( logEvent ) ;
}
2021-11-08 12:18:49 -05:00
}
}
}
}