// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Channels; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace EpicGames.Core { /// /// Log Event Type /// public enum LogEventType { /// /// The log event is a fatal error /// Fatal = LogLevel.Critical, /// /// The log event is an error /// Error = LogLevel.Error, /// /// The log event is a warning /// Warning = LogLevel.Warning, /// /// Output the log event to the console /// Console = LogLevel.Information, /// /// Output the event to the on-disk log /// Log = LogLevel.Debug, /// /// The log event should only be displayed if verbose logging is enabled /// Verbose = LogLevel.Trace, /// /// The log event should only be displayed if very verbose logging is enabled /// VeryVerbose = LogLevel.Trace } /// /// Options for formatting messages /// [Flags] public enum LogFormatOptions { /// /// Format normally /// None = 0, /// /// Never write a severity prefix. Useful for pre-formatted messages that need to be in a particular format for, eg. the Visual Studio output window /// NoSeverityPrefix = 1, /// /// Do not output text to the console /// NoConsoleOutput = 2, } /// /// UAT/UBT Custom log system. /// /// This lets you use any TraceListeners you want, but you should only call the static /// methods below, not call Trace.XXX directly, as the static methods /// This allows the system to enforce the formatting and filtering conventions we desire. /// /// For posterity, we cannot use the Trace or TraceSource class directly because of our special log requirements: /// 1. We possibly capture the method name of the logging event. This cannot be done as a macro, so must be done at the top level so we know how many layers of the stack to peel off to get the real function. /// 2. We have a verbose filter we would like to apply to all logs without having to have each listener filter individually, which would require our string formatting code to run every time. /// 3. We possibly want to ensure severity prefixes are logged, but Trace.WriteXXX does not allow any severity info to be passed down. /// public static class Log { /// /// Singleton instance of the default output logger /// private static readonly DefaultLogger DefaultLogger = new DefaultLogger(); /// /// Logger instance which parses events and forwards them to the main logger. /// private static readonly LegacyEventLogger LegacyLogger = new LegacyEventLogger(DefaultLogger); /// /// Accessor for the global event parser from legacy events /// public static LogEventParser EventParser => LegacyLogger.Parser; /// /// Logger instance /// public static ILogger Logger => LegacyLogger; /// /// When true, verbose logging is enabled. /// public static LogEventType OutputLevel { get => (LogEventType)DefaultLogger.OutputLevel; set => DefaultLogger.OutputLevel = (LogLevel)value; } /// /// Whether to include timestamps on each line of log output /// public static bool IncludeTimestamps { get => DefaultLogger.IncludeTimestamps; set => DefaultLogger.IncludeTimestamps = value; } /// /// When true, warnings and errors will have a WARNING: or ERROR: prefix, respectively. /// public static bool IncludeSeverityPrefix { get; set; } = true; /// /// When true, warnings and errors will have a prefix suitable for display by MSBuild (avoiding error messages showing as (EXEC : Error : ") /// public static bool IncludeProgramNameWithSeverityPrefix { get; set; } /// /// When true, will detect warnings and errors and set the console output color to yellow and red. /// public static bool ColorConsoleOutput { get => DefaultLogger.ColorConsoleOutput; set => DefaultLogger.ColorConsoleOutput = value; } /// /// When true, a timestamp will be written to the log file when the first listener is added /// public static bool IncludeStartingTimestamp { get => DefaultLogger.IncludeStartingTimestamp; set => DefaultLogger.IncludeStartingTimestamp = value; } /// /// When true, create a backup of any log file that would be overwritten by a new log /// Log.txt will be backed up with its UTC creation time in the name e.g. /// Log-backup-2021.10.29-19.53.17.txt /// public static bool BackupLogFiles = true; /// /// The number of backups to be preserved - when there are more than this, the oldest backups will be deleted. /// Backups will not be deleted if BackupLogFiles is false. /// public static int LogFileBackupCount = 10; /// /// Path to the log file being written to. May be null. /// public static FileReference? OutputFile => DefaultLogger?.OutputFile; /// /// A collection of strings that have been already written once /// private static readonly HashSet s_writeOnceSet = new HashSet(); /// /// Overrides the logger used for formatting output, after event parsing /// /// public static void SetInnerLogger(ILogger logger) { LegacyLogger.SetInnerLogger(logger); } /// /// Flush the current log output /// /// public static async Task FlushAsync() { await DefaultLogger.FlushAsync(); } /// /// Adds a trace listener that writes to a log file. /// If Log.DuplicateLogFiles is true, two files will be created - one with the requested name, /// another with a timestamp appended before any extension. /// If a StartupTraceListener was in use, this function will copy its captured data to the log file(s) /// and remove the startup listener from the list of registered listeners. /// /// The file to write to /// The created trace listener public static void AddFileWriter(string name, FileReference outputFile) { Log.TraceInformation($"Log file: {outputFile}"); if (Log.BackupLogFiles && FileReference.Exists(outputFile)) { // before creating a new backup, cap the number of existing files string filenameWithoutExtension = outputFile.GetFileNameWithoutExtension(); string extension = outputFile.GetExtension(); Regex backupForm = new Regex(filenameWithoutExtension + @"-backup-\d\d\d\d\.\d\d\.\d\d-\d\d\.\d\d\.\d\d" + extension); foreach (FileReference oldBackup in DirectoryReference .EnumerateFiles(outputFile.Directory) // find files that match the way that we name backup files .Where(x => backupForm.IsMatch(x.GetFileName())) // sort them from newest to oldest .OrderByDescending(x => x.GetFileName()) // skip the newest ones that are to be kept; -1 because we're about to create another backup. .Skip(Log.LogFileBackupCount - 1)) { Log.TraceLog($"Deleting old log file: {oldBackup}"); FileReference.Delete(oldBackup); } // Ensure that the backup gets a unique name, in the extremely unlikely case that UBT was run twice during // the same second. DateTime fileTime = File.GetCreationTimeUtc(outputFile.FullName); FileReference backupFile; for (;;) { string timestamp = $"{fileTime:yyyy.MM.dd-HH.mm.ss}"; backupFile = FileReference.Combine(outputFile.Directory, $"{filenameWithoutExtension}-backup-{timestamp}{extension}"); if (!FileReference.Exists(backupFile)) { break; } fileTime = fileTime.AddSeconds(1); } FileReference.Move(outputFile, backupFile); } TextWriterTraceListener firstTextWriter = DefaultLogger.AddFileWriter(name, outputFile); // find the StartupTraceListener in the listeners that was added early on IEnumerable startupListeners = Trace.Listeners.OfType(); if (startupListeners.Any()) { StartupTraceListener startupListener = startupListeners.First(); startupListener.CopyTo(firstTextWriter); Trace.Listeners.Remove(startupListener); } } /// /// Adds a to the collection in a safe manner. /// /// The to add. public static void AddTraceListener(TraceListener traceListener) { DefaultLogger.AddTraceListener(traceListener); } /// /// Removes a from the collection in a safe manner. /// /// The to remove. public static void RemoveTraceListener(TraceListener traceListener) { DefaultLogger.RemoveTraceListener(traceListener); } /// /// Determines if a TextWriterTraceListener has been added to the list of trace listeners /// /// True if a TextWriterTraceListener has been added public static bool HasFileWriter() { return DefaultLogger.HasFileWriter(); } /// /// Converts a LogEventType into a log prefix. Only used when bLogSeverity is true. /// /// /// private static string GetSeverityPrefix(LogEventType severity) { switch (severity) { case LogEventType.Fatal: return "FATAL ERROR: "; case LogEventType.Error: return "ERROR: "; case LogEventType.Warning: return "WARNING: "; case LogEventType.Console: return ""; case LogEventType.Verbose: return "VERBOSE: "; default: return ""; } } /// /// Writes a formatted message to the console. All other functions should boil down to calling this method. /// /// If true, this message will be written only once /// Message verbosity level. We only meaningfully use values up to Verbose /// Options for formatting messages /// Message format string. /// Optional arguments [StringFormatMethod("Format")] private static void WriteLinePrivate(bool bWriteOnce, LogEventType verbosity, LogFormatOptions formatOptions, string format, params object?[] args) { if (Logger.IsEnabled((LogLevel)verbosity)) { StringBuilder message = new StringBuilder(); // Get the severity prefix for this message if (IncludeSeverityPrefix && ((formatOptions & LogFormatOptions.NoSeverityPrefix) == 0)) { message.Append(GetSeverityPrefix(verbosity)); if (message.Length > 0 && IncludeProgramNameWithSeverityPrefix) { // Include the executable name when running inside MSBuild. If unspecified, MSBuild re-formats them with an "EXEC :" prefix. message.Insert(0, $"{Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()!.Location)}: "); } } // Append the formatted string int indentLen = message.Length; if (args.Length == 0) { message.Append(format); } else { message.AppendFormat(format, args); } // Replace any Windows \r\n sequences with \n message.Replace("\r\n", "\n"); // Remove any trailing whitespace int trimLen = message.Length; while (trimLen > 0 && " \t\r\n".Contains(message[trimLen - 1])) { trimLen--; } message.Remove(trimLen, message.Length - trimLen); // Update the indent length to include any whitespace at the start of the message while (indentLen < message.Length && message[indentLen] == ' ') { indentLen++; } // If there are multiple lines, insert a prefix at the start of each one for (int idx = 0; idx < message.Length; idx++) { if (message[idx] == '\n') { message.Insert(idx + 1, " ", indentLen); idx += indentLen; } } // if we want this message only written one time, check if it was already written out if (bWriteOnce && !s_writeOnceSet.Add(message.ToString())) { return; } // Forward it on to the internal logger if (verbosity < LogEventType.Console) { Logger.Log((LogLevel)verbosity, message.ToString()); } else { lock (EventParser) { int baseIdx = 0; for (int idx = 0; idx < message.Length; idx++) { if (message[idx] == '\n') { EventParser.WriteLine(message.ToString(baseIdx, idx - baseIdx)); baseIdx = idx + 1; } } EventParser.WriteLine(message.ToString(baseIdx, message.Length - baseIdx)); } } } } /// /// Similar to Trace.WriteLineIf /// /// /// /// /// [StringFormatMethod("Format")] public static void WriteLineIf(bool condition, LogEventType verbosity, string format, params object?[] args) { if (condition) { WriteLinePrivate(false, verbosity, LogFormatOptions.None, format, args); } } /// /// Similar to Trace.WriteLine /// /// /// /// [StringFormatMethod("Format")] public static void WriteLine(LogEventType verbosity, string format, params object?[] args) { WriteLinePrivate(false, verbosity, LogFormatOptions.None, format, args); } /// /// Similar to Trace.WriteLine /// /// /// /// /// [StringFormatMethod("Format")] public static void WriteLine(LogEventType verbosity, LogFormatOptions formatOptions, string format, params object?[] args) { WriteLinePrivate(false, verbosity, formatOptions, format, args); } /// /// Formats an exception for display in the log. The exception message is shown as an error, and the stack trace is included in the log. /// /// The exception to display /// The log filename to display, if any public static void WriteException(Exception ex, string? logFileName) { string logSuffix = (logFileName == null) ? "" : String.Format("\n(see {0} for full exception trace)", logFileName); TraceLog("=============================================================================="); TraceError("{0}{1}", ExceptionUtils.FormatException(ex), logSuffix); TraceLog("\n{0}", ExceptionUtils.FormatExceptionDetails(ex)); TraceLog("=============================================================================="); } /// /// Writes an error message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceError(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Error, LogFormatOptions.None, format, args); } /// /// Writes an error message to the console, in a format suitable for Visual Studio to parse. /// /// The file containing the error /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceErrorTask(FileReference file, string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Error, LogFormatOptions.NoSeverityPrefix, "{0}: error: {1}", file, String.Format(format, args)); } /// /// Writes an error message to the console, in a format suitable for Visual Studio to parse. /// /// The file containing the error /// Line number of the error /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceErrorTask(FileReference file, int line, string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Error, LogFormatOptions.NoSeverityPrefix, "{0}({1}): error: {2}", file, line, String.Format(format, args)); } /// /// Writes a verbose message to the console. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceVerbose(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Verbose, LogFormatOptions.None, format, args); } /// /// Writes a message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceInformation(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Console, LogFormatOptions.None, format, args); } /// /// Writes a warning message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarning(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Warning, LogFormatOptions.None, format, args); } /// /// Writes a warning message to the console, in a format suitable for Visual Studio to parse. /// /// The file containing the warning /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarningTask(FileReference file, string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Warning, LogFormatOptions.NoSeverityPrefix, "{0}: warning: {1}", file, String.Format(format, args)); } /// /// Writes a warning message to the console, in a format suitable for Visual Studio to parse. /// /// The file containing the warning /// Line number of the warning /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarningTask(FileReference file, int line, string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Warning, LogFormatOptions.NoSeverityPrefix, "{0}({1}): warning: {2}", file, line, String.Format(format, args)); } /// /// Writes a message to the console. /// /// The file containing the message /// Line number of the message /// Message format string /// Optional arguments public static void TraceConsoleTask(FileReference file, int line, string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Console, LogFormatOptions.NoSeverityPrefix, "{0}({1}): {2}", file, line, String.Format(format, args)); } /// /// Writes a very verbose message to the console. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceVeryVerbose(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.VeryVerbose, LogFormatOptions.None, format, args); } /// /// Writes a message to the log only. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceLog(string format, params object?[] args) { WriteLinePrivate(false, LogEventType.Log, LogFormatOptions.None, format, args); } /// /// Similar to Trace.WriteLine /// /// /// /// [StringFormatMethod("Format")] public static void WriteLineOnce(LogEventType verbosity, string format, params object?[] args) { WriteLinePrivate(true, verbosity, LogFormatOptions.None, format, args); } /// /// Similar to Trace.WriteLine /// /// /// /// /// [StringFormatMethod("Format")] public static void WriteLineOnce(LogEventType verbosity, LogFormatOptions options, string format, params object?[] args) { WriteLinePrivate(true, verbosity, options, format, args); } /// /// Writes an error message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceErrorOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Error, LogFormatOptions.None, format, args); } /// /// Writes a verbose message to the console. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceVerboseOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Verbose, LogFormatOptions.None, format, args); } /// /// Writes a message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceInformationOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Console, LogFormatOptions.None, format, args); } /// /// Writes a warning message to the console. /// /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarningOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Warning, LogFormatOptions.None, format, args); } /// /// Writes a warning message to the console. /// /// The file containing the error /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarningOnce(FileReference file, string format, params object?[] args) { WriteLinePrivate( true, LogEventType.Warning, LogFormatOptions.NoSeverityPrefix, "{0}: warning: {1}", file, String.Format(format, args)); } /// /// Writes a warning message to the console. /// /// The file containing the error /// Line number of the error /// Message format string /// Optional arguments [StringFormatMethod("Format")] public static void TraceWarningOnce(FileReference file, int line, string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Warning, LogFormatOptions.NoSeverityPrefix, "{0}({1}): warning: {2}", file, line, String.Format(format, args)); } /// /// Writes a very verbose message to the console. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceVeryVerboseOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.VeryVerbose, LogFormatOptions.None, format, args); } /// /// Writes a message to the log only. /// /// Message format string /// Optional arguments [Conditional("TRACE")] [StringFormatMethod("Format")] public static void TraceLogOnce(string format, params object?[] args) { WriteLinePrivate(true, LogEventType.Log, LogFormatOptions.None, format, args); } /// /// Enter a scope with the given status message. The message will be written to the console without a newline, allowing it to be updated through subsequent calls to UpdateStatus(). /// The message will be written to the log immediately. If another line is written while in a status scope, the initial status message is flushed to the console first. /// /// The status message [Conditional("TRACE")] public static void PushStatus(string message) { DefaultLogger.PushStatus(message); } /// /// Updates the current status message. This will overwrite the previous status line. /// /// The status message [Conditional("TRACE")] public static void UpdateStatus(string message) { DefaultLogger.UpdateStatus(message); } /// /// Updates the Pops the top status message from the stack. The mess /// /// [Conditional("TRACE")] public static void PopStatus() { DefaultLogger.PopStatus(); } } /// /// Logger which captures the output for rendering later /// public class CaptureLogger : ILogger { class NullScope : IDisposable { public void Dispose() { } } /// /// List of captured events /// public List Events { get; } = new List(); /// /// Renders the captured events as a single string /// /// Rendered log text public string Render() => Render("\n"); /// /// Renders the captured events as a single string /// /// Rendered log text public string Render(string newLine) => String.Join(newLine, RenderLines()); /// /// Renders all the captured events /// /// List of rendered log lines public List RenderLines() => Events.ConvertAll(x => x.ToString()); /// public IDisposable BeginScope(TState state) => new NullScope(); /// public bool IsEnabled(LogLevel logLevel) => true; /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { Events.Add(LogEvent.FromState(logLevel, eventId, state, exception, formatter)); } } /// /// Wrapper around a custom logger interface which flushes the event parser when switching between legacy /// and native structured logging /// class LegacyEventLogger : ILogger { private ILogger _inner; private readonly LogEventParser _parser; public LogEventParser Parser => _parser; public LegacyEventLogger(ILogger inner) { _inner = inner; _parser = new LogEventParser(inner); } public void SetInnerLogger(ILogger inner) { lock (_parser) { _parser.Flush(); _inner = inner; _parser.Logger = inner; } } public IDisposable BeginScope(TState state) => _inner.BeginScope(state); public bool IsEnabled(LogLevel logLevel) => _inner.IsEnabled(logLevel); public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { lock (_parser) { _parser.Flush(); } _inner.Log(logLevel, eventId, state, exception, formatter); } } /// /// Default log output device /// class DefaultLogger : ILogger, IDisposable { /// /// Temporary status message displayed on the console. /// [DebuggerDisplay("{HeadingText}")] class StatusMessage { /// /// The heading for this status message. /// public string _headingText; /// /// The current status text. /// public string _currentText; /// /// Whether the heading has been written to the console. Before the first time that lines are output to the log in the midst of a status scope, the heading will be written on a line of its own first. /// public bool _hasFlushedHeadingText; /// /// Constructor /// public StatusMessage(string headingText, string currentText) { _headingText = headingText; _currentText = currentText; } } /// /// Object used for synchronization /// private readonly object _syncObject = new object(); /// /// Minimum level for outputting messages /// public LogLevel OutputLevel { get; set; } /// /// Whether to include timestamps on each line of log output /// public bool IncludeTimestamps { get; set; } /// /// When true, will detect warnings and errors and set the console output color to yellow and red. /// public bool ColorConsoleOutput { get; set; } /// /// Whether to write JSON to stdout /// public bool WriteJsonToStdOut { get; set; } /// /// When true, a timestamp will be written to the log file when the first listener is added /// public bool IncludeStartingTimestamp { get; set; } private bool _includeStartingTimestampWritten = false; /// /// Path to the log file being written to. May be null. /// public FileReference? OutputFile { get; private set; } /// /// Whether console output is redirected. This prevents writing status updates that rely on moving the cursor. /// private bool AllowStatusUpdates => !Console.IsOutputRedirected; /// /// When configured, this tracks time since initialization to prepend a timestamp to each log. /// private readonly Stopwatch _timer = Stopwatch.StartNew(); /// /// Stack of status scope information. /// private readonly Stack _statusMessageStack = new Stack(); /// /// The currently visible status text /// private string _statusText = ""; /// /// Parser for transforming legacy log output into structured events /// public LogEventParser EventParser { get; } /// /// Last time a status message was pushed to the stack /// private readonly Stopwatch _statusTimer = new Stopwatch(); /// /// Background task for writing to files /// private Task _writeTask; /// /// Channel for new log events /// private Channel _eventChannel = Channel.CreateUnbounded(); /// /// Output streams for structured log data /// private IReadOnlyList _jsonStreams = Array.Empty(); /// /// Constructor /// public DefaultLogger() { OutputLevel = LogLevel.Debug; ColorConsoleOutput = true; IncludeStartingTimestamp = true; EventParser = new LogEventParser(this); string? envVar = Environment.GetEnvironmentVariable("UE_LOG_JSON_TO_STDOUT"); if(envVar != null && Int32.TryParse(envVar, out int value) && value != 0) { WriteJsonToStdOut = true; } _writeTask = Task.Run(() => WriteFilesAsync()); } /// public void Dispose() { _eventChannel.Writer.TryComplete(); _writeTask.Wait(); } /// /// Flush the stream /// /// public async Task FlushAsync() { lock (_syncObject) { Channel prevEventChannel = _eventChannel; _eventChannel = Channel.CreateUnbounded(); prevEventChannel.Writer.TryComplete(); } await _writeTask; _writeTask = Task.Run(() => WriteFilesAsync()); } /// /// Background task to write events to sinks /// async Task WriteFilesAsync() { byte[] newline = new byte[] { (byte)'\n' }; while (await _eventChannel.Reader.WaitToReadAsync()) { IReadOnlyList streams = _jsonStreams; JsonLogEvent logEvent; while (_eventChannel.Reader.TryRead(out logEvent)) { foreach (FileStream stream in streams) { await stream.WriteAsync(logEvent.Data); await stream.WriteAsync(newline); } } foreach (FileStream stream in streams) { await stream.FlushAsync(); } } } /// /// Adds a trace listener that writes to a log file /// /// Listener name /// The file to write to /// The created trace listener public TextWriterTraceListener AddFileWriter(string name, FileReference outputFile) { try { OutputFile = outputFile; DirectoryReference.CreateDirectory(outputFile.Directory); TextWriterTraceListener logTraceListener = new TextWriterTraceListener(new StreamWriter(outputFile.FullName), name); lock (_syncObject) { Trace.Listeners.Add(logTraceListener); WriteInitialTimestamp(); List newJsonStreams = new List(_jsonStreams); newJsonStreams.Add(FileReference.Open(outputFile.ChangeExtension(".json"), FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete)); _jsonStreams = newJsonStreams; } return logTraceListener; } catch (Exception ex) { throw new Exception($"Error while creating log file \"{outputFile}\"", ex); } } /// /// Adds a to the collection in a safe manner. /// /// The to add. public void AddTraceListener(TraceListener traceListener) { lock (_syncObject) { if (!Trace.Listeners.Contains(traceListener)) { Trace.Listeners.Add(traceListener); WriteInitialTimestamp(); } } } /// /// Write a timestamp to the log, once. To be called when a new listener is added. /// private void WriteInitialTimestamp() { if (IncludeStartingTimestamp && !_includeStartingTimestampWritten) { DateTime now = DateTime.Now; this.LogDebug("{Message}", $"Log started at {now} ({now.ToUniversalTime():yyyy-MM-ddTHH\\:mm\\:ssZ})"); _includeStartingTimestampWritten = true; } } /// /// Removes a from the collection in a safe manner. /// /// The to remove. public void RemoveTraceListener(TraceListener traceListener) { lock (_syncObject) { if (Trace.Listeners.Contains(traceListener)) { Trace.Listeners.Remove(traceListener); } } } /// /// Determines if a TextWriterTraceListener has been added to the list of trace listeners /// /// True if a TextWriterTraceListener has been added public static bool HasFileWriter() { foreach (TraceListener? listener in Trace.Listeners) { if (listener is TextWriterTraceListener) { return true; } } return false; } public IDisposable BeginScope(TState state) { throw new NotImplementedException(); } public bool IsEnabled(LogLevel logLevel) { return logLevel >= OutputLevel; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { string[] lines = formatter(state, exception).Split('\n'); lock (_syncObject) { // Output to all the other trace listeners string timePrefix = String.Format("[{0:hh\\:mm\\:ss\\.fff}] ", _timer.Elapsed); foreach (TraceListener? listener in Trace.Listeners) { if (listener != null) { string timePrefixActual = IncludeTimestamps && !(listener is DefaultTraceListener) // no timestamps when writing to the Visual Studio debug window ? timePrefix : String.Empty; foreach (string line in lines) { string lineWithTime = timePrefixActual + line; listener.WriteLine(line); listener.Flush(); } } } // Handle the console output separately; we format things differently if (logLevel >= LogLevel.Information) { FlushStatusHeading(); bool bResetConsoleColor = false; if (ColorConsoleOutput) { if (logLevel == LogLevel.Warning) { Console.ForegroundColor = ConsoleColor.Yellow; bResetConsoleColor = true; } if (logLevel >= LogLevel.Error) { Console.ForegroundColor = ConsoleColor.Red; bResetConsoleColor = true; } } try { JsonLogEvent jsonLogEvent = JsonLogEvent.FromLoggerState(logLevel, eventId, state, exception, formatter); _eventChannel.Writer.TryWrite(jsonLogEvent); if (WriteJsonToStdOut) { Console.WriteLine(Encoding.UTF8.GetString(jsonLogEvent.Data.Span)); } else { foreach (string line in lines) { Console.WriteLine(line); } } } catch (IOException) { // Potential file access/sharing issue on std out // This can occur on some versions of mono (e.g. macOS 6.12.0) if writing to a full pipe // during IPC when the reader isn't consuming it quick enough } finally { // make sure we always put the console color back. if (bResetConsoleColor) { Console.ResetColor(); } } if (_statusMessageStack.Count > 0 && AllowStatusUpdates) { SetStatusText(_statusMessageStack.Peek()._currentText); } } } } /// /// Flushes the current status text before writing out a new log line or status message /// void FlushStatusHeading() { if (_statusMessageStack.Count > 0) { StatusMessage currentStatus = _statusMessageStack.Peek(); if (currentStatus._headingText.Length > 0 && !currentStatus._hasFlushedHeadingText && AllowStatusUpdates) { SetStatusText(currentStatus._headingText); Console.WriteLine(); _statusText = ""; currentStatus._hasFlushedHeadingText = true; } else { SetStatusText(""); } } } /// /// Enter a scope with the given status message. The message will be written to the console without a newline, allowing it to be updated through subsequent calls to UpdateStatus(). /// The message will be written to the log immediately. If another line is written while in a status scope, the initial status message is flushed to the console first. /// /// The status message [Conditional("TRACE")] public void PushStatus(string message) { lock (_syncObject) { FlushStatusHeading(); StatusMessage newStatusMessage = new StatusMessage(message, message); _statusMessageStack.Push(newStatusMessage); _statusTimer.Restart(); if (message.Length > 0) { this.LogDebug("{Message}", message); SetStatusText(message); } } } /// /// Updates the current status message. This will overwrite the previous status line. /// /// The status message [Conditional("TRACE")] public void UpdateStatus(string message) { lock (_syncObject) { StatusMessage currentStatusMessage = _statusMessageStack.Peek(); currentStatusMessage._currentText = message; if (AllowStatusUpdates || _statusTimer.Elapsed.TotalSeconds > 10.0) { SetStatusText(message); _statusTimer.Restart(); } } } /// /// Updates the Pops the top status message from the stack. The mess /// /// [Conditional("TRACE")] public void PopStatus() { lock (_syncObject) { StatusMessage currentStatusMessage = _statusMessageStack.Peek(); SetStatusText(currentStatusMessage._currentText); if (_statusText.Length > 0) { Console.WriteLine(); _statusText = ""; } _statusMessageStack.Pop(); } } /// /// Update the status text. For internal use only; does not modify the StatusMessageStack objects. /// /// New status text to display private void SetStatusText(string newStatusText) { if (newStatusText.Length > 0) { newStatusText = LogIndent.Current + newStatusText; } if (_statusText != newStatusText) { int numCommonChars = 0; while (numCommonChars < _statusText.Length && numCommonChars < newStatusText.Length && _statusText[numCommonChars] == newStatusText[numCommonChars]) { numCommonChars++; } if (!AllowStatusUpdates && numCommonChars < _statusText.Length) { // Prevent writing backspace characters if the console doesn't support it Console.WriteLine(); _statusText = ""; numCommonChars = 0; } StringBuilder text = new StringBuilder(); text.Append('\b', _statusText.Length - numCommonChars); text.Append(newStatusText, numCommonChars, newStatusText.Length - numCommonChars); if (newStatusText.Length < _statusText.Length) { int numChars = _statusText.Length - newStatusText.Length; text.Append(' ', numChars); text.Append('\b', numChars); } Console.Write(text.ToString()); _statusText = newStatusText; _statusTimer.Restart(); } } } /// /// Provider for default logger instances /// public class DefaultLoggerProvider : ILoggerProvider { /// public ILogger CreateLogger(string categoryName) { return new DefaultLogger(); } /// public void Dispose() { } } /// /// Extension methods to support the default logger /// public static class DefaultLoggerExtensions { /// /// Adds a regular Epic logger to the builder /// /// Logging builder public static void AddEpicDefault(this ILoggingBuilder builder) { builder.Services.AddSingleton(); } } }