using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Unosquare.Swan { /// /// A console terminal helper to create nicer output and receive input from the user /// This class is thread-safe :). /// public static partial class Terminal { #region Private Declarations private static UInt64 _loggingSequence; #endregion #region Events /// /// Occurs asynchronously, whenever a logging message is received by the terminal. /// Only called when Terminal writes data via Info, Error, Trace, Warn, Fatal, Debug methods, regardless of whether or not /// the console is present. Subscribe to this event to pass data on to your own logger. /// public static event EventHandler OnLogMessageReceived; /// /// Occurs synchronously (so handle quickly), whenever a logging message is about to be enqueued to the /// console output. Setting the CancelOutput to true in the event arguments prevents the /// logging message to be written out to the console. /// Message filtering only works with logging methods such as Trace, Debug, Info, Warn, Fatal, Error and Dump /// Standard Write methods do not get filtering capabilities. /// public static event EventHandler OnLogMessageDisplaying; #endregion #region Main Logging Method /// /// Logs a message. /// /// Type of the message. /// The text. /// Name of the source. /// The extended data. Could be an exception, or a dictionary of properties or anything the user specifies. /// Name of the caller member. /// The caller file path. /// The caller line number. private static void LogMessage( LogMessageType messageType, String message, String sourceName, Object extendedData, String callerMemberName, String callerFilePath, Int32 callerLineNumber) { lock(SyncLock) { if(!Settings.GlobalLoggingMessageType.HasFlag(messageType)) { return; } String prefix = GetConsoleColorAndPrefix(messageType, out ConsoleColor color); #region Create and Format the Output UInt64 sequence = _loggingSequence; DateTime date = DateTime.UtcNow; _loggingSequence++; String loggerMessage = String.IsNullOrWhiteSpace(message) ? String.Empty : message.RemoveControlCharsExcept('\n'); String outputMessage = CreateOutputMessage(sourceName, loggerMessage, prefix, date); // Log the message asynchronously with the appropriate event args LogMessageReceivedEventArgs eventArgs = new LogMessageReceivedEventArgs( sequence, messageType, date, sourceName, loggerMessage, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Fire Up External Logging Logic (Asynchronously) if(OnLogMessageReceived != null) { _ = Task.Run(() => { try { OnLogMessageReceived?.Invoke(sourceName, eventArgs); } catch { // Ignore } }); } #endregion #region Display the Message by Writing to the Output Queue // Check if we are skipping these messages to be displayed based on settings if(!Settings.DisplayLoggingMessageType.HasFlag(messageType)) { return; } Write(messageType, sourceName, eventArgs, outputMessage, color); #endregion } } private static void Write( LogMessageType messageType, String sourceName, LogMessageReceivedEventArgs eventArgs, String outputMessage, ConsoleColor color) { // Select the writer based on the message type TerminalWriters writer = IsConsolePresent ? messageType.HasFlag(LogMessageType.Error) ? TerminalWriters.StandardError : TerminalWriters.StandardOutput : TerminalWriters.None; // Set the writer to Diagnostics if appropriate (Error and Debugging data go to the Diagnostics debugger // if it is attached at all if(IsDebuggerAttached && (IsConsolePresent == false || messageType.HasFlag(LogMessageType.Debug) || messageType.HasFlag(LogMessageType.Error))) { writer |= TerminalWriters.Diagnostics; } // Check if we really need to write this out if(writer == TerminalWriters.None) { return; } // Further format the output in the case there is an exception being logged if(writer.HasFlag(TerminalWriters.StandardError) && eventArgs.Exception != null) { try { outputMessage = $"{outputMessage}{Environment.NewLine}{eventArgs.Exception.Stringify().Indent()}"; } catch { // Ignore } } // Filter output messages via events LogMessageDisplayingEventArgs displayingEventArgs = new LogMessageDisplayingEventArgs(eventArgs); OnLogMessageDisplaying?.Invoke(sourceName, displayingEventArgs); if(displayingEventArgs.CancelOutput == false) { outputMessage.WriteLine(color, writer); } } private static String GetConsoleColorAndPrefix(LogMessageType messageType, out ConsoleColor color) { String prefix; // Select color and prefix based on message type // and settings switch(messageType) { case LogMessageType.Debug: color = Settings.DebugColor; prefix = Settings.DebugPrefix; break; case LogMessageType.Error: color = Settings.ErrorColor; prefix = Settings.ErrorPrefix; break; case LogMessageType.Info: color = Settings.InfoColor; prefix = Settings.InfoPrefix; break; case LogMessageType.Trace: color = Settings.TraceColor; prefix = Settings.TracePrefix; break; case LogMessageType.Warning: color = Settings.WarnColor; prefix = Settings.WarnPrefix; break; case LogMessageType.Fatal: color = Settings.FatalColor; prefix = Settings.FatalPrefix; break; default: color = Settings.DefaultColor; prefix = new String(' ', Settings.InfoPrefix.Length); break; } return prefix; } private static String CreateOutputMessage(String sourceName, String loggerMessage, String prefix, DateTime date) { String friendlySourceName = String.IsNullOrWhiteSpace(sourceName) ? String.Empty : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); String outputMessage = String.IsNullOrWhiteSpace(sourceName) ? loggerMessage : $"[{friendlySourceName}] {loggerMessage}"; return String.IsNullOrWhiteSpace(Settings.LoggingTimeFormat) ? $" {prefix} >> {outputMessage}" : $" {date.ToLocalTime().ToString(Settings.LoggingTimeFormat)} {prefix} >> {outputMessage}"; } #endregion #region Standard Public API #region Debug /// /// Logs a debug message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Debug( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a debug message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Debug( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Debug, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a debug message to the console. /// /// The exception. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Debug( this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Trace /// /// Logs a trace message to the console. /// /// The text. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Trace( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a trace message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Trace( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Trace, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a trace message to the console. /// /// The extended data. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Trace( this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Warn /// /// Logs a warning message to the console. /// /// The text. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Warn( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a warning message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Warn( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Warning, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a warning message to the console. /// /// The extended data. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Warn( this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Fatal /// /// Logs a warning message to the console. /// /// The text. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Fatal( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a warning message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Fatal( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Fatal, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a warning message to the console. /// /// The extended data. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Fatal( this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Info /// /// Logs an info message to the console. /// /// The text. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Info( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an info message to the console. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Info( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Info, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an info message to the console. /// /// The extended data. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Info( this Exception extendedData, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); #endregion #region Error /// /// Logs an error message to the console's standard error. /// /// The text. /// The source. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Error( this String message, String source = null, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Error, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an error message to the console's standard error. /// /// The message. /// The source. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Error( this String message, Type source, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Error, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an error message to the console's standard error. /// /// The exception. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Error( this Exception ex, String source, String message, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Error, message, source, ex, callerMemberName, callerFilePath, callerLineNumber); #endregion #endregion #region Extended Public API /// /// Logs the specified message. /// /// The message. /// The source. /// Type of the message. /// The extended data. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Log( this String message, String source, LogMessageType messageType, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(messageType, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs the specified message. /// /// The message. /// The source. /// Type of the message. /// The extended data. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Log( this String message, Type source, LogMessageType messageType, Object extendedData = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(messageType, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an error message to the console's standard error. /// /// The ex. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Log( this Exception ex, String source = null, String message = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Error, message ?? ex.Message, source ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs an error message to the console's standard error. /// /// The ex. /// The source. /// The message. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Log( this Exception ex, Type source = null, String message = null, [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) => LogMessage(LogMessageType.Error, message ?? ex.Message, source?.FullName ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); /// /// Logs a trace message showing all possible non-null properties of the given object /// This method is expensive as it uses Stringify internally. /// /// The object. /// The source. /// The title. /// Name of the caller member. This is automatically populated. /// The caller file path. This is automatically populated. /// The caller line number. This is automatically populated. public static void Dump( this Object obj, String source, String text = "Object Dump", [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) { if(obj == null) { return; } String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; LogMessage(LogMessageType.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber); } /// /// Logs a trace message showing all possible non-null properties of the given object /// This method is expensive as it uses Stringify internally. /// /// The object. /// The source. /// The text. /// Name of the caller member. /// The caller file path. /// The caller line number. public static void Dump( this Object obj, Type source, String text = "Object Dump", [CallerMemberName] String callerMemberName = "", [CallerFilePath] String callerFilePath = "", [CallerLineNumber] Int32 callerLineNumber = 0) { if(obj == null) { return; } String message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; LogMessage(LogMessageType.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); } #endregion } }