Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Core/Log.cs
2025-05-18 13:04:45 +08:00

1552 lines
48 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
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
{
/// <summary>
/// Log Event Type
/// </summary>
#pragma warning disable CA1027 // Mark enums with FlagsAttribute
public enum LogEventType
#pragma warning restore CA1027 // Mark enums with FlagsAttribute
{
/// <summary>
/// The log event is a fatal error
/// </summary>
Fatal = LogLevel.Critical,
/// <summary>
/// The log event is an error
/// </summary>
Error = LogLevel.Error,
/// <summary>
/// The log event is a warning
/// </summary>
Warning = LogLevel.Warning,
/// <summary>
/// Output the log event to the console
/// </summary>
Console = LogLevel.Information,
/// <summary>
/// Output the event to the on-disk log
/// </summary>
Log = LogLevel.Debug,
/// <summary>
/// The log event should only be displayed if verbose logging is enabled
/// </summary>
Verbose = LogLevel.Trace,
/// <summary>
/// The log event should only be displayed if very verbose logging is enabled
/// </summary>
#pragma warning disable CA1069 // Enums values should not be duplicated
VeryVerbose = LogLevel.Trace
#pragma warning restore CA1069 // Enums values should not be duplicated
}
/// <summary>
/// Options for formatting messages
/// </summary>
[Flags]
public enum LogFormatOptions
{
/// <summary>
/// Format normally
/// </summary>
None = 0,
/// <summary>
/// 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
/// </summary>
NoSeverityPrefix = 1,
/// <summary>
/// Do not output text to the console
/// </summary>
NoConsoleOutput = 2,
}
/// <summary>
/// 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.
/// </summary>
public static class Log
{
/// <summary>
/// Singleton instance of the default output logger
/// </summary>
private static readonly DefaultLogger s_defaultLogger = new DefaultLogger();
/// <summary>
/// Logger instance which parses events and forwards them to the main logger.
/// </summary>
private static readonly LegacyEventLogger s_legacyLogger = new LegacyEventLogger(s_defaultLogger);
/// <summary>
/// Accessor for the global event parser from legacy events
/// </summary>
public static LogEventParser EventParser => s_legacyLogger.Parser;
/// <summary>
/// Logger instance
/// </summary>
public static ILogger Logger => s_legacyLogger;
/// <summary>
/// When true, verbose logging is enabled.
/// </summary>
public static LogEventType OutputLevel
{
get => (LogEventType)s_defaultLogger.OutputLevel;
set => s_defaultLogger.OutputLevel = (LogLevel)value;
}
/// <summary>
/// Whether to include timestamps on each line of log output
/// </summary>
public static bool IncludeTimestamps
{
get => s_defaultLogger.IncludeTimestamps;
set => s_defaultLogger.IncludeTimestamps = value;
}
/// <summary>
/// When true, warnings and errors will have a WARNING: or ERROR: prefix, respectively.
/// </summary>
public static bool IncludeSeverityPrefix { get; set; } = true;
/// <summary>
/// When true, warnings and errors will have a prefix suitable for display by MSBuild (avoiding error messages showing as (EXEC : Error : ")
/// </summary>
public static bool IncludeProgramNameWithSeverityPrefix { get; set; }
/// <summary>
/// When true, will detect warnings and errors and set the console output color to yellow and red.
/// </summary>
public static bool ColorConsoleOutput
{
get => s_defaultLogger.ColorConsoleOutput;
set => s_defaultLogger.ColorConsoleOutput = value;
}
/// <summary>
/// When true, a timestamp will be written to the log file when the first listener is added
/// </summary>
public static bool IncludeStartingTimestamp
{
get => s_defaultLogger.IncludeStartingTimestamp;
set => s_defaultLogger.IncludeStartingTimestamp = value;
}
/// <summary>
/// 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
/// </summary>
public static bool BackupLogFiles { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public static int LogFileBackupCount { get; set; } = 10;
/// <summary>
/// Path to the log file being written to. May be null.
/// </summary>
public static FileReference? OutputFile => s_defaultLogger?.OutputFile;
/// <summary>
/// A collection of strings that have been already written once
/// </summary>
private static readonly ConcurrentDictionary<string, bool> s_writeOnceSet = new();
/// <summary>
/// Overrides the logger used for formatting output, after event parsing
/// </summary>
/// <param name="logger"></param>
public static void SetInnerLogger(ILogger logger)
{
s_legacyLogger.SetInnerLogger(logger);
}
/// <summary>
/// Flush the current log output
/// </summary>
/// <returns></returns>
public static async Task FlushAsync()
{
await s_defaultLogger.FlushAsync();
}
/// <summary>
/// Backup an existing log file if it already exists at the outputpath
/// </summary>
/// <param name="outputFile">The file to back up</param>
public static void BackupLogFile(FileReference outputFile)
{
if (!Log.BackupLogFiles || !FileReference.Exists(outputFile))
{
return;
}
// 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))
{
Logger.LogDebug("Deleting old log file: {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);
}
/// <summary>
/// Adds a trace listener that writes to a log file.
/// 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.
/// </summary>
/// <param name="name">Identifier for the writer</param>
/// <param name="outputFile">The file to write to</param>
public static void AddFileWriter(string name, FileReference outputFile)
{
Logger.LogInformation("Log file: {OutputFile}", outputFile);
BackupLogFile(outputFile);
AddFileWriterWithoutBackup(name, outputFile);
}
/// <summary>
/// Adds a trace listener that writes to a log file.
/// 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.
/// </summary>
/// <param name="name">Identifier for the writer</param>
/// <param name="outputFile">The file to write to</param>
public static void AddFileWriterWithoutBackup(string name, FileReference outputFile)
{
s_defaultLogger.AddFileWriterWithoutBackup(name, outputFile);
}
/// <summary>
/// Adds a <see cref="TraceListener"/> to the collection in a safe manner.
/// </summary>
/// <param name="traceListener">The <see cref="TraceListener"/> to add.</param>
public static void AddTraceListener(TraceListener traceListener)
{
s_defaultLogger.AddTraceListener(traceListener);
}
/// <summary>
/// Removes a <see cref="TraceListener"/> from the collection in a safe manner.
/// </summary>
/// <param name="traceListener">The <see cref="TraceListener"/> to remove.</param>
public static void RemoveTraceListener(TraceListener traceListener)
{
s_defaultLogger.RemoveTraceListener(traceListener);
}
/// <summary>
/// Removes a <see cref="StartupTraceListener"/> from the collection in a safe manner.
/// </summary>
public static void RemoveStartupTraceListener()
{
s_defaultLogger.RemoveStartupTraceListener();
}
/// <summary>
/// Determines if a TextWriterTraceListener has been added to the list of trace listeners
/// </summary>
/// <returns>True if a TextWriterTraceListener has been added</returns>
public static bool HasFileWriter()
{
return s_defaultLogger.HasFileWriter();
}
/// <summary>
/// Converts a LogEventType into a log prefix. Only used when bLogSeverity is true.
/// </summary>
/// <param name="severity"></param>
/// <returns></returns>
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 "";
}
}
/// <summary>
/// Writes a formatted message to the console. All other functions should boil down to calling this method.
/// </summary>
/// <param name="bWriteOnce">If true, this message will be written only once</param>
/// <param name="verbosity">Message verbosity level. We only meaningfully use values up to Verbose</param>
/// <param name="formatOptions">Options for formatting messages</param>
/// <param name="format">Message format string.</param>
/// <param name="args">Optional arguments</param>
[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], StringComparison.Ordinal))
{
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.TryAdd(message.ToString(), true))
{
return;
}
// Forward it on to the internal logger
if (verbosity < LogEventType.Console)
{
Logger.Log((LogLevel)verbosity, "{Message}", 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));
}
}
}
}
/// <summary>
/// Similar to Trace.WriteLineIf
/// </summary>
/// <param name="condition"></param>
/// <param name="verbosity"></param>
/// <param name="format"></param>
/// <param name="args"></param>
[StringFormatMethod("Format")]
public static void WriteLineIf(bool condition, LogEventType verbosity, string format, params object?[] args)
{
if (condition)
{
WriteLinePrivate(false, verbosity, LogFormatOptions.None, format, args);
}
}
/// <summary>
/// Similar to Trace.WriteLine
/// </summary>
/// <param name="verbosity"></param>
/// <param name="format"></param>
/// <param name="args"></param>
[StringFormatMethod("Format")]
public static void WriteLine(LogEventType verbosity, string format, params object?[] args)
{
WriteLinePrivate(false, verbosity, LogFormatOptions.None, format, args);
}
/// <summary>
/// Similar to Trace.WriteLine
/// </summary>
/// <param name="verbosity"></param>
/// <param name="formatOptions"></param>
/// <param name="format"></param>
/// <param name="args"></param>
[StringFormatMethod("Format")]
public static void WriteLine(LogEventType verbosity, LogFormatOptions formatOptions, string format, params object?[] args)
{
WriteLinePrivate(false, verbosity, formatOptions, format, args);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ex">The exception to display</param>
/// <param name="logFileName">The log filename to display, if any</param>
public static void WriteException(Exception ex, FileReference? logFileName)
{
string logSuffix = (logFileName == null) ? "" : String.Format("\n(see {0} for full exception trace)", logFileName);
Logger.LogDebug("==============================================================================");
Logger.LogError(ex, "{Message}{Suffix}", ExceptionUtils.FormatException(ex), logSuffix);
Logger.LogDebug("");
Logger.LogDebug("{Details}", ExceptionUtils.FormatExceptionDetails(ex));
Logger.LogDebug("==============================================================================");
}
/// <summary>
/// Writes an error message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogError with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceError(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.Error, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes an error message to the console, in a format suitable for Visual Studio to parse.
/// </summary>
/// <param name="file">The file containing the error</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes an error message to the console, in a format suitable for Visual Studio to parse.
/// </summary>
/// <param name="file">The file containing the error</param>
/// <param name="line">Line number of the error</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes a verbose message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogDebug with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceVerbose(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.Verbose, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogInformation with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceInformation(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.Console, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a warning message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogWarning with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceWarning(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.Warning, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a warning message to the console, in a format suitable for Visual Studio to parse.
/// </summary>
/// <param name="file">The file containing the warning</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes a warning message to the console, in a format suitable for Visual Studio to parse.
/// </summary>
/// <param name="file">The file containing the warning</param>
/// <param name="line">Line number of the warning</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes a message to the console.
/// </summary>
/// <param name="file">The file containing the message</param>
/// <param name="line">Line number of the message</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
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));
}
/// <summary>
/// Writes a very verbose message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogTrace with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceVeryVerbose(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.VeryVerbose, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a message to the log only.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
[Obsolete("Use Logger.LogDebug with a message template instead; see https://tinyurl.com/bp96bk2r.", false)]
public static void TraceLog(string format, params object?[] args)
{
WriteLinePrivate(false, LogEventType.Log, LogFormatOptions.None, format, args);
}
/// <summary>
/// Similar to Trace.WriteLine
/// </summary>
/// <param name="verbosity"></param>
/// <param name="format"></param>
/// <param name="args"></param>
[StringFormatMethod("Format")]
public static void WriteLineOnce(LogEventType verbosity, string format, params object?[] args)
{
WriteLinePrivate(true, verbosity, LogFormatOptions.None, format, args);
}
/// <summary>
/// Similar to Trace.WriteLine
/// </summary>
/// <param name="verbosity"></param>
/// <param name="options"></param>
/// <param name="format"></param>
/// <param name="args"></param>
[StringFormatMethod("Format")]
public static void WriteLineOnce(LogEventType verbosity, LogFormatOptions options, string format, params object?[] args)
{
WriteLinePrivate(true, verbosity, options, format, args);
}
/// <summary>
/// Writes an error message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
public static void TraceErrorOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.Error, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a verbose message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
public static void TraceVerboseOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.Verbose, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
public static void TraceInformationOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.Console, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a warning message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[StringFormatMethod("Format")]
public static void TraceWarningOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.Warning, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a warning message to the console.
/// </summary>
/// <param name="file">The file containing the error</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes a warning message to the console.
/// </summary>
/// <param name="file">The file containing the error</param>
/// <param name="line">Line number of the error</param>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[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));
}
/// <summary>
/// Writes a very verbose message to the console.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
public static void TraceVeryVerboseOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.VeryVerbose, LogFormatOptions.None, format, args);
}
/// <summary>
/// Writes a message to the log only.
/// </summary>
/// <param name="format">Message format string</param>
/// <param name="args">Optional arguments</param>
[Conditional("TRACE")]
[StringFormatMethod("Format")]
public static void TraceLogOnce(string format, params object?[] args)
{
WriteLinePrivate(true, LogEventType.Log, LogFormatOptions.None, format, args);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="message">The status message</param>
[Conditional("TRACE")]
public static void PushStatus(string message)
{
s_defaultLogger.PushStatus(message);
}
/// <summary>
/// Updates the current status message. This will overwrite the previous status line.
/// </summary>
/// <param name="message">The status message</param>
[Conditional("TRACE")]
public static void UpdateStatus(string message)
{
s_defaultLogger.UpdateStatus(message);
}
/// <summary>
/// Updates the Pops the top status message from the stack. The mess
/// </summary>
[Conditional("TRACE")]
public static void PopStatus()
{
s_defaultLogger.PopStatus();
}
}
/// <summary>
/// Base implementation for loggers which handles creation of LogEvents from log commands
/// </summary>
public sealed class LoggerScopeCollection
{
class Scope : IDisposable
{
readonly LoggerScopeCollection _outer;
readonly IEnumerable<KeyValuePair<string, object?>>? _properties;
public IEnumerable<KeyValuePair<string, object?>>? Properties => _properties;
public Scope(LoggerScopeCollection outer, object? value)
{
_outer = outer;
_properties = value switch
{
IEnumerable<KeyValuePair<string, object?>> props => props,
_ => null
};
_outer._scopes.Add(this);
}
public void Dispose()
=> _outer._scopes.Remove(this);
}
readonly List<Scope> _scopes = [];
/// <summary>
/// Tests whether the collection is empty
/// </summary>
public bool IsEmpty()
=> _scopes.Count == 0;
/// <summary>
/// Creates a scope with the given state
/// </summary>
public IDisposable BeginScope<TState>(TState state)
=> new Scope(this, state);
/// <summary>
/// Enumerates all current properties
/// </summary>
public IEnumerable<KeyValuePair<string, object?>> GetProperties()
{
foreach (Scope scope in _scopes)
{
if (scope.Properties != null)
{
foreach (KeyValuePair<string, object?> property in scope.Properties)
{
yield return property;
}
}
}
}
}
/// <summary>
/// Logger which captures the output for rendering later
/// </summary>
public class CaptureLogger : ILogger
{
readonly LoggerScopeCollection _scopeCollection = new LoggerScopeCollection();
/// <summary>
/// List of captured events
/// </summary>
public List<LogEvent> Events { get; } = [];
/// <summary>
/// Renders the captured events as a single string
/// </summary>
/// <returns>Rendered log text</returns>
public string Render() => Render("\n");
/// <summary>
/// Renders the captured events as a single string
/// </summary>
/// <returns>Rendered log text</returns>
public string Render(string newLine) => String.Join(newLine, RenderLines());
/// <summary>
/// Renders all the captured events
/// </summary>
/// <returns>List of rendered log lines</returns>
public List<string> RenderLines() => Events.ConvertAll(x => x.ToString());
/// <summary>
/// Renders all the captured events to another logger
/// </summary>
/// <param name="logger"></param>
public void RenderTo(ILogger logger) => Events.ForEach( x => logger.Log(x.Level, x.Id, x, null, (s, e) => s.ToString()) );
/// <inheritdoc/>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => _scopeCollection.BeginScope(state);
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
LogEvent logEvent = LogEvent.FromState(logLevel, eventId, state, exception, formatter);
logEvent.AddProperties(_scopeCollection.GetProperties());
Events.Add(logEvent);
}
}
/// <summary>
/// Wrapper around a custom logger interface which flushes the event parser when switching between legacy
/// and native structured logging
/// </summary>
sealed class LegacyEventLogger : ILogger, IDisposable
{
private ILogger _inner;
private readonly LogEventParser _parser;
public LogEventParser Parser => _parser;
public LegacyEventLogger(ILogger inner)
{
_inner = inner;
_parser = new LogEventParser(inner);
}
public void Dispose()
{
_parser.Dispose();
}
public void SetInnerLogger(ILogger inner)
{
lock (_parser)
{
_parser.Flush();
_inner = inner;
_parser.Logger = inner;
}
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => _inner.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => _inner.IsEnabled(logLevel);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
lock (_parser)
{
_parser.Flush();
}
_inner.Log(logLevel, eventId, state, exception, formatter);
}
}
/// <summary>
/// Default log output device
/// </summary>
class DefaultLogger : ILogger, IDisposable
{
/// <summary>
/// Temporary status message displayed on the console.
/// </summary>
[DebuggerDisplay("{HeadingText}")]
class StatusMessage
{
/// <summary>
/// The heading for this status message.
/// </summary>
public string _headingText;
/// <summary>
/// The current status text.
/// </summary>
public string _currentText;
/// <summary>
/// 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.
/// </summary>
public bool _hasFlushedHeadingText;
/// <summary>
/// Constructor
/// </summary>
public StatusMessage(string headingText, string currentText)
{
_headingText = headingText;
_currentText = currentText;
}
}
private readonly LoggerScopeCollection _scopeCollection = new LoggerScopeCollection();
private readonly object _syncObject = new object();
/// <summary>
/// Minimum level for outputting messages
/// </summary>
public LogLevel OutputLevel
{
get; set;
}
/// <summary>
/// Whether to include timestamps on each line of log output
/// </summary>
public bool IncludeTimestamps
{
get; set;
}
/// <summary>
/// When true, will detect warnings and errors and set the console output color to yellow and red.
/// </summary>
public bool ColorConsoleOutput
{
get; set;
}
/// <summary>
/// Whether to write JSON to stdout
/// </summary>
public bool WriteJsonToStdOut
{
get; set;
}
/// <summary>
/// When true, a timestamp will be written to the log file when the first listener is added
/// </summary>
public bool IncludeStartingTimestamp
{
get; set;
}
private bool _includeStartingTimestampWritten = false;
/// <summary>
/// Path to the log file being written to. May be null.
/// </summary>
public FileReference? OutputFile
{
get; private set;
}
/// <summary>
/// Whether console output is redirected. This prevents writing status updates that rely on moving the cursor.
/// </summary>
private static bool AllowStatusUpdates { get; set; } = !Console.IsOutputRedirected;
/// <summary>
/// When configured, this tracks time since initialization to prepend a timestamp to each log.
/// </summary>
private readonly Stopwatch _timer = Stopwatch.StartNew();
/// <summary>
/// Stack of status scope information.
/// </summary>
private readonly Stack<StatusMessage> _statusMessageStack = new Stack<StatusMessage>();
/// <summary>
/// The currently visible status text
/// </summary>
private string _statusText = "";
/// <summary>
/// Parser for transforming legacy log output into structured events
/// </summary>
public LogEventParser EventParser { get; }
/// <summary>
/// Last time a status message was pushed to the stack
/// </summary>
private readonly Stopwatch _statusTimer = new Stopwatch();
/// <summary>
/// Background task for writing to files
/// </summary>
private Task _writeTask;
/// <summary>
/// Channel for new log events
/// </summary>
private Channel<JsonLogEvent> _eventChannel = Channel.CreateUnbounded<JsonLogEvent>();
/// <summary>
/// Output streams for structured log data
/// </summary>
private IReadOnlyList<FileStream> _jsonStreams = [];
/// <summary>
/// Constructor
/// </summary>
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());
}
/// <inheritdoc/>
public void Dispose()
{
_eventChannel.Writer.TryComplete();
_writeTask.Wait();
}
/// <summary>
/// Flush the stream
/// </summary>
/// <returns></returns>
public async Task FlushAsync()
{
lock (_syncObject)
{
Channel<JsonLogEvent> prevEventChannel = _eventChannel;
_eventChannel = Channel.CreateUnbounded<JsonLogEvent>();
prevEventChannel.Writer.TryComplete();
}
await _writeTask;
_writeTask = Task.Run(() => WriteFilesAsync());
}
/// <summary>
/// Background task to write events to sinks
/// </summary>
async Task WriteFilesAsync()
{
byte[] newline = [(byte)'\n'];
while (await _eventChannel.Reader.WaitToReadAsync())
{
IReadOnlyList<FileStream> 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();
}
}
}
/// <summary>
/// Adds a trace listener that writes to a log file
/// </summary>
/// <param name="name">Listener name</param>
/// <param name="outputFile">The file to write to</param>
/// <returns>The created trace listener</returns>
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<FileStream> newJsonStreams = new List<FileStream>(_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);
}
}
/// <summary>
/// Adds a <see cref="TraceListener"/> to the collection in a safe manner.
/// </summary>
/// <param name="traceListener">The <see cref="TraceListener"/> to add.</param>
public void AddTraceListener(TraceListener traceListener)
{
lock (_syncObject)
{
if (!Trace.Listeners.Contains(traceListener))
{
Trace.Listeners.Add(traceListener);
WriteInitialTimestamp();
}
}
}
/// <summary>
/// Write a timestamp to the log, once. To be called when a new listener is added.
/// </summary>
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;
}
}
/// <summary>
/// Removes a <see cref="TraceListener"/> from the collection in a safe manner.
/// </summary>
/// <param name="traceListener">The <see cref="TraceListener"/> to remove.</param>
public void RemoveTraceListener(TraceListener traceListener)
{
lock (_syncObject)
{
if (Trace.Listeners.Contains(traceListener))
{
Trace.Listeners.Remove(traceListener);
}
}
}
/// <summary>
/// Removes <see cref="StartupTraceListener"/> from the collection in a safe manner.
/// </summary>
public void RemoveStartupTraceListener()
{
lock (_syncObject)
{
IEnumerable<StartupTraceListener> startupListeners = Trace.Listeners.OfType<StartupTraceListener>();
if (startupListeners.Any())
{
Trace.Listeners.Remove(startupListeners.First());
}
}
}
/// <summary>
/// Adds a trace listener that writes to a log file.
/// 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.
/// </summary>
/// <param name="name">Identifier for the writer</param>
/// <param name="outputFile">The file to write to</param>
public void AddFileWriterWithoutBackup(string name, FileReference outputFile)
{
TextWriterTraceListener firstTextWriter = AddFileWriter(name, outputFile);
lock (_syncObject)
{
// find the StartupTraceListener in the listeners that was added early on
IEnumerable<StartupTraceListener> startupListeners = Trace.Listeners.OfType<StartupTraceListener>();
if (startupListeners.Any())
{
StartupTraceListener startupListener = startupListeners.First();
startupListener.CopyTo(firstTextWriter);
Trace.Listeners.Remove(startupListeners.First());
}
}
}
/// <summary>
/// Determines if a TextWriterTraceListener has been added to the list of trace listeners
/// </summary>
/// <returns>True if a TextWriterTraceListener has been added</returns>
public bool HasFileWriter()
{
lock (_syncObject)
{
return Trace.Listeners.OfType<TextWriterTraceListener>().Any();
}
}
/// <inheritdoc/>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> _scopeCollection.BeginScope(state);
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= OutputLevel;
}
/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
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)
{
if (listener is DefaultTraceListener && logLevel < LogLevel.Information)
{
continue;
}
string timePrefixActual =
IncludeTimestamps &&
listener is not 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(lineWithTime);
listener.Flush();
}
}
}
Activity? activity = Activity.Current;
JsonLogEvent jsonLogEvent;
if (activity == null && _scopeCollection.IsEmpty())
{
jsonLogEvent = JsonLogEvent.FromLoggerState(logLevel, eventId, state, exception, formatter);
}
else
{
LogEvent logEvent = LogEvent.FromState(logLevel, eventId, state, exception, formatter);
logEvent.AddProperties(_scopeCollection.GetProperties());
if (activity != null)
{
logEvent.AddProperty("Activity", activity);
}
jsonLogEvent = new JsonLogEvent(logEvent);
}
_eventChannel.Writer.TryWrite(jsonLogEvent);
// 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
{
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
}
finally
{
// make sure we always put the console color back.
if (bResetConsoleColor)
{
Console.ResetColor();
}
}
if (_statusMessageStack.Count > 0 && AllowStatusUpdates)
{
SetStatusText(_statusMessageStack.Peek()._currentText);
}
}
}
}
/// <summary>
/// Flushes the current status text before writing out a new log line or status message
/// </summary>
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("");
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="message">The status message</param>
[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);
}
}
}
/// <summary>
/// Updates the current status message. This will overwrite the previous status line.
/// </summary>
/// <param name="message">The status message</param>
[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();
}
}
}
/// <summary>
/// Updates the Pops the top status message from the stack. The mess
/// </summary>
[Conditional("TRACE")]
public void PopStatus()
{
lock (_syncObject)
{
StatusMessage currentStatusMessage = _statusMessageStack.Peek();
SetStatusText(currentStatusMessage._currentText);
if (_statusText.Length > 0)
{
Console.WriteLine();
_statusText = "";
}
_statusMessageStack.Pop();
}
}
/// <summary>
/// Update the status text. For internal use only; does not modify the StatusMessageStack objects.
/// </summary>
/// <param name="newStatusText">New status text to display</param>
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 && _statusText.Length > 0)
{
// 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();
}
}
}
/// <summary>
/// Provider for default logger instances
/// </summary>
public sealed class DefaultLoggerProvider : ILoggerProvider
{
/// <inheritdoc/>
public ILogger CreateLogger(string categoryName)
{
return new DefaultLogger();
}
/// <inheritdoc/>
public void Dispose()
{
}
}
/// <summary>
/// Extension methods to support the default logger
/// </summary>
public static class DefaultLoggerExtensions
{
/// <summary>
/// Adds a regular Epic logger to the builder
/// </summary>
/// <param name="builder">Logging builder</param>
public static void AddEpicDefault(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, DefaultLoggerProvider>();
}
}
}