// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Tools.DotNETCommon
{
///
/// Log Event Type
///
public enum LogEventType
{
///
/// The log event is a fatal error
///
Fatal = 1,
///
/// The log event is an error
///
Error = 2,
///
/// The log event is a warning
///
Warning = 4,
///
/// Output the log event to the console
///
Console = 8,
///
/// Output the event to the on-disk log
///
Log = 16,
///
/// The log event should only be displayed if verbose logging is enabled
///
Verbose = 32,
///
/// The log event should only be displayed if very verbose logging is enabled
///
VeryVerbose = 64
}
///
/// 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,
}
///
/// 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.
///
static public class Log
{
///
/// Guard our initialization. Mainly used by top level exception handlers to ensure its safe to call a logging function.
/// In general user code should not concern itself checking for this.
///
private static bool bIsInitialized = false;
///
/// When true, verbose logging is enabled.
///
private static LogEventType LogLevel = LogEventType.VeryVerbose;
///
/// When true, warnings and errors will have a WARNING: or ERROR: prexifx, respectively.
///
private static bool bLogSeverity = false;
///
/// When true, warnings and errors will have a prefix suitable for display by MSBuild (avoiding error messages showing as (EXEC : Error : ")
///
private static bool bLogProgramNameWithSeverity = false;
///
/// When true, logs will have the calling mehod prepended to the output as MethodName:
///
private static bool bLogSources = false;
///
/// When true, console output will have the calling mehod prepended to the output as MethodName:
///
private static bool bLogSourcesToConsole = false;
///
/// When true, will detect warnings and errors and set the console output color to yellow and red.
///
private static bool bColorConsoleOutput = false;
///
/// When configured, this tracks time since initialization to prepend a timestamp to each log.
///
private static Stopwatch Timer;
///
/// Expose the log level. This is a hack for ProcessResult.LogOutput, which wants to bypass our normal formatting scheme.
///
public static bool bIsVerbose { get { return LogLevel >= LogEventType.Verbose; } }
///
/// A collection of strings that have been already written once
///
private static List WriteOnceSet = new List();
///
/// Indent added to every output line
///
public static string Indent
{
get;
set;
}
///
/// Static initializer
///
static Log()
{
Indent = "";
}
///
/// Allows code to check if the log system is ready yet.
/// End users should NOT need to use this. It pretty much exists
/// to work around startup issues since this is a global singleton.
///
///
public static bool IsInitialized()
{
return bIsInitialized;
}
///
/// Allows code to check if the log system is using console output color.
///
///
public static bool ColorConsoleOutput()
{
return bColorConsoleOutput;
}
///
/// Allows us to change verbosity after initializing. This can happen since we initialize logging early,
/// but then read the config and command line later, which could change this value.
///
public static void SetLoggingLevel(LogEventType InLogLevel)
{
Log.LogLevel = InLogLevel;
}
///
/// This class allows InitLogging to be called more than once to work around chicken and eggs issues with logging and parsing command lines (see UBT startup code).
///
/// If true, the timestamp from Log init time will be prepended to all logs.
///
/// If true, warnings and errors will have a WARNING: and ERROR: prefix to them.
/// If true, includes the program name with any severity prefix
/// If true, logs will have the originating method name prepended to them.
/// If true, console output will have the originating method name appended to it.
///
/// Collection of trace listeners to attach to the Trace.Listeners, in addition to the Default listener. The existing listeners (except the Default listener) are cleared first.
public static void InitLogging(bool bLogTimestamps, LogEventType InLogLevel, bool bLogSeverity, bool bLogProgramNameWithSeverity, bool bLogSources, bool bLogSourcesToConsole, bool bColorConsoleOutput, IEnumerable TraceListeners)
{
bIsInitialized = true;
Timer = (bLogTimestamps && Timer == null) ? Stopwatch.StartNew() : null;
Log.LogLevel = InLogLevel;
Log.bLogSeverity = bLogSeverity;
Log.bLogProgramNameWithSeverity = bLogProgramNameWithSeverity;
Log.bLogSources = bLogSources;
Log.bLogSourcesToConsole = bLogSourcesToConsole;
Log.bColorConsoleOutput = bColorConsoleOutput;
// ensure that if InitLogging is called more than once we don't stack listeners.
// but always leave the default listener around.
for (int ListenerNdx = 0; ListenerNdx < Trace.Listeners.Count;)
{
if (Trace.Listeners[ListenerNdx].GetType() != typeof(DefaultTraceListener))
{
Trace.Listeners.RemoveAt(ListenerNdx);
}
else
{
++ListenerNdx;
}
}
// don't add any null listeners
Trace.Listeners.AddRange(TraceListeners.Where(l => l != null).ToArray());
Trace.AutoFlush = true;
}
///
/// Gets the name of the Method N levels deep in the stack frame. Used to trap what method actually made the logging call.
/// Only used when bLogSources is true.
///
///
/// ClassName.MethodName
[MethodImplAttribute(MethodImplOptions.NoInlining)]
private static string GetSource(int StackFramesToSkip)
{
StackFrame Frame = new StackFrame(2 + StackFramesToSkip);
System.Reflection.MethodBase Method = Frame.GetMethod();
return String.Format("{0}.{1}", Method.DeclaringType.Name, Method.Name);
}
///
/// 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: ";
case LogEventType.VeryVerbose:
return "VVERBOSE: ";
default:
return "";
}
}
///
/// Converts a LogEventType into a message code
///
///
///
private static int GetMessageCode(LogEventType Severity)
{
return (int)Severity;
}
///
/// Formats message for logging. Enforces the configured options.
///
/// Number of frames to skip to get to the originator of the log request.
/// Message verbosity level
/// Options for formatting this string
/// Whether the message is intended for console output
/// Message text format string
/// Message text parameters
/// Formatted message
[MethodImplAttribute(MethodImplOptions.NoInlining)]
private static List FormatMessage(int StackFramesToSkip, LogEventType Verbosity, LogFormatOptions Options, bool bForConsole, string Format, params object[] Args)
{
string TimePrefix = (Timer != null) ? String.Format("[{0:hh\\:mm\\:ss\\.fff}] ", Timer.Elapsed) : "";
string SourcePrefix = (bForConsole ? bLogSourcesToConsole : bLogSources) ? string.Format("{0}: ", GetSource(StackFramesToSkip)) : "";
string SeverityPrefix = (bLogSeverity && ((Options & LogFormatOptions.NoSeverityPrefix) == 0)) ? GetSeverityPrefix(Verbosity) : "";
// Include the executable name when running inside MSBuild. If unspecified, MSBuild re-formats them with an "EXEC :" prefix.
if(SeverityPrefix.Length > 0 && bLogProgramNameWithSeverity)
{
SeverityPrefix = String.Format("{0}: {1}", Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location), SeverityPrefix);
}
// If there are no extra args, don't try to format the string, in case it has any format control characters in it (our LOCTEXT strings tend to).
string[] Lines = ((Args.Length > 0) ? String.Format(Format, Args) : Format).TrimEnd(' ', '\t', '\r', '\n').Split('\n');
List FormattedLines = new List();
FormattedLines.Add(String.Format("{0}{1}{2}{3}{4}", TimePrefix, SourcePrefix, Indent, SeverityPrefix, Lines[0].TrimEnd('\r')));
if (Lines.Length > 1)
{
int PaddingLength = 0;
while(PaddingLength < Lines[0].Length && Char.IsWhiteSpace(Lines[0][PaddingLength]))
{
PaddingLength++;
}
string Padding = new string(' ', SeverityPrefix.Length) + Lines[0].Substring(0, PaddingLength);
for (int Idx = 1; Idx < Lines.Length; Idx++)
{
FormattedLines.Add(String.Format("{0}{1}{2}{3}{4}", TimePrefix, SourcePrefix, Indent, Padding, Lines[Idx].TrimEnd('\r')));
}
}
return FormattedLines;
}
///
/// Writes a formatted message to the console. All other functions should boil down to calling this method.
///
/// Number of frames to skip to get to the originator of the log request.
/// 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
[MethodImplAttribute(MethodImplOptions.NoInlining)]
private static void WriteLinePrivate(int StackFramesToSkip, bool bWriteOnce, LogEventType Verbosity, LogFormatOptions FormatOptions, string Format, params object[] Args)
{
if (!bIsInitialized)
{
throw new InvalidOperationException("Tried to using Logging system before it was ready");
}
// if we want this message only written one time, check if it was already written out
if (bWriteOnce)
{
string Formatted = string.Format(Format, Args);
if (WriteOnceSet.Contains(Formatted))
{
return;
}
WriteOnceSet.Add(Formatted);
}
if (Verbosity <= LogLevel)
{
// Do console color highlighting here.
ConsoleColor DefaultColor = ConsoleColor.Gray;
bool bIsWarning = false;
bool bIsError = false;
// don't try to touch the console unless we are told to color the output.
if (bColorConsoleOutput)
{
DefaultColor = Console.ForegroundColor;
bIsWarning = Verbosity == LogEventType.Warning;
bIsError = Verbosity <= LogEventType.Error;
// @todo mono - mono doesn't seem to initialize the ForegroundColor properly, so we can't restore it properly.
// Avoid touching the console color unless we really need to.
if (bIsWarning || bIsError)
{
Console.ForegroundColor = bIsWarning ? ConsoleColor.Yellow : ConsoleColor.Red;
}
}
try
{
// @todo mono: mono has some kind of bug where calling mono recursively by spawning
// a new process causes Trace.WriteLine to stop functioning (it returns, but does nothing for some reason).
// work around this by simulating Trace.WriteLine on mono.
// We use UAT to spawn UBT instances recursively a lot, so this bug can effectively
// make all build output disappear outside of the top level UAT process.
// #if MONO
lock (((System.Collections.ICollection)Trace.Listeners).SyncRoot)
{
foreach (TraceListener l in Trace.Listeners)
{
bool bIsConsole = l is Tools.DotNETCommon.ConsoleTraceListener;
if (Verbosity != LogEventType.Log || !bIsConsole || LogLevel >= LogEventType.Verbose)
{
List Lines = FormatMessage(StackFramesToSkip + 1, Verbosity, FormatOptions, bIsConsole, Format, Args);
foreach (string Line in Lines)
{
l.WriteLine(Line);
}
}
l.Flush();
}
}
// #else
// Call Trace directly here. Trace ensures that our logging is threadsafe using the GlobalLock.
// Trace.WriteLine(FormatMessage(StackFramesToSkip + 1, CustomSource, Verbosity, Format, Args));
// #endif
}
finally
{
// make sure we always put the console color back.
if (bColorConsoleOutput && (bIsWarning || bIsError))
{
Console.ForegroundColor = DefaultColor;
}
}
}
}
///
/// Similar to Trace.WriteLineIf
///
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLineIf(bool Condition, LogEventType Verbosity, string Format, params object[] Args)
{
if (Condition)
{
WriteLinePrivate(1, false, Verbosity, LogFormatOptions.None, Format, Args);
}
}
///
/// Mostly an internal function, but expose StackFramesToSkip to allow UAT to use existing wrapper functions and still get proper formatting.
///
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(int StackFramesToSkip, LogEventType Verbosity, string Format, params object[] Args)
{
WriteLinePrivate(StackFramesToSkip + 1, false, Verbosity, LogFormatOptions.None, Format, Args);
}
///
/// Mostly an internal function, but expose StackFramesToSkip to allow UAT to use existing wrapper functions and still get proper formatting.
///
///
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(int StackFramesToSkip, LogEventType Verbosity, LogFormatOptions FormatOptions, string Format, params object[] Args)
{
WriteLinePrivate(StackFramesToSkip + 1, false, Verbosity, FormatOptions, Format, Args);
}
///
/// Similar to Trace.WriteLine
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(LogEventType Verbosity, string Format, params object[] Args)
{
WriteLinePrivate(1, false, Verbosity, LogFormatOptions.None, Format, Args);
}
///
/// Similar to Trace.WriteLine
///
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(LogEventType Verbosity, LogFormatOptions FormatOptions, string Format, params object[] Args)
{
WriteLinePrivate(1, 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
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceError(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.Error, LogFormatOptions.None, Format, Args);
}
///
/// Writes a verbose message to the console.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceVerbose(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.Verbose, LogFormatOptions.None, Format, Args);
}
///
/// Writes a message to the console.
///
/// Message format string
/// Optional arguments
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceInformation(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.Console, LogFormatOptions.None, Format, Args);
}
///
/// Writes a warning message to the console.
///
/// Message format string
/// Optional arguments
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceWarning(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.Warning, LogFormatOptions.None, Format, Args);
}
///
/// Writes a very verbose message to the console.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceVeryVerbose(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.VeryVerbose, LogFormatOptions.None, Format, Args);
}
///
/// Writes a message to the log only.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceLog(string Format, params object[] Args)
{
WriteLinePrivate(1, false, LogEventType.Log, LogFormatOptions.None, Format, Args);
}
///
/// Similar to Trace.WriteLine
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLineOnce(LogEventType Verbosity, string Format, params object[] Args)
{
WriteLinePrivate(1, true, Verbosity, LogFormatOptions.None, Format, Args);
}
///
/// Similar to Trace.WriteLine
///
///
///
///
///
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLineOnce(LogEventType Verbosity, LogFormatOptions Options, string Format, params object[] Args)
{
WriteLinePrivate(1, true, Verbosity, Options, Format, Args);
}
///
/// Writes an error message to the console.
///
/// Message format string
/// Optional arguments
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceErrorOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.Error, LogFormatOptions.None, Format, Args);
}
///
/// Writes a verbose message to the console.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceVerboseOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.Verbose, LogFormatOptions.None, Format, Args);
}
///
/// Writes a message to the console.
///
/// Message format string
/// Optional arguments
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceInformationOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.Console, LogFormatOptions.None, Format, Args);
}
///
/// Writes a warning message to the console.
///
/// Message format string
/// Optional arguments
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceWarningOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.Warning, LogFormatOptions.None, Format, Args);
}
///
/// Writes a very verbose message to the console.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceVeryVerboseOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.VeryVerbose, LogFormatOptions.None, Format, Args);
}
///
/// Writes a message to the log only.
///
/// Message format string
/// Optional arguments
[Conditional("TRACE")]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void TraceLogOnce(string Format, params object[] Args)
{
WriteLinePrivate(1, true, LogEventType.Log, LogFormatOptions.None, Format, Args);
}
}
///
/// Class to apply a log indent for the lifetime of an object
///
public class ScopedLogIndent : IDisposable
{
///
/// The previous indent
///
string PrevIndent;
///
/// Constructor
///
/// Indent to append to the existing indent
public ScopedLogIndent(string Indent)
{
PrevIndent = Log.Indent;
Log.Indent += Indent;
}
///
/// Restore the log indent to normal
///
public void Dispose()
{
if (PrevIndent != null)
{
Log.Indent = PrevIndent;
PrevIndent = null;
}
}
}
}