using IPA.Config; using IPA.Logging.Printers; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; namespace IPA.Logging { /// /// The default (and standard) implementation. /// /// /// uses a multi-threaded approach to logging. All actual I/O is done on another thread, /// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them. /// /// This logger supports child loggers. Use to safely get a child. /// The modification of printers on a parent are reflected down the chain. /// public class StandardLogger : Logger { private static readonly List defaultPrinters = new List() { new ColoredConsolePrinter() { Filter = LogLevel.DebugOnly, Color = ConsoleColor.Green, }, new ColoredConsolePrinter() { Filter = LogLevel.InfoOnly, Color = ConsoleColor.White, }, new ColoredConsolePrinter() { Filter = LogLevel.WarningOnly, Color = ConsoleColor.Yellow, }, new ColoredConsolePrinter() { Filter = LogLevel.ErrorOnly, Color = ConsoleColor.Red, }, new ColoredConsolePrinter() { Filter = LogLevel.CriticalOnly, Color = ConsoleColor.Magenta, }, new GlobalLogFilePrinter() }; /// /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger. /// /// internal static void AddDefaultPrinter(LogPrinter printer) { defaultPrinters.Add(printer); } private readonly string logName; private static bool showSourceClass; /// /// All levels defined by this filter will be sent to loggers. All others will be ignored. /// public static LogLevel PrintFilter { get; set; } = LogLevel.All; private readonly List printers = new List(); private readonly StandardLogger parent; private readonly Dictionary children = new Dictionary(); /// /// Configures internal debug settings based on the config passed in. /// /// internal static void Configure(SelfConfig cfg) { showSourceClass = cfg.Debug.ShowCallSource; PrintFilter = cfg.Debug.ShowDebug ? LogLevel.All : LogLevel.InfoUp; } private StandardLogger(StandardLogger parent, string subName) { logName = $"{parent.logName}/{subName}"; this.parent = parent; printers = new List() { new PluginSubLogPrinter(parent.logName, subName) }; if (logThread == null || !logThread.IsAlive) { logThread = new Thread(LogThread); logThread.Start(); } } internal StandardLogger(string name) { logName = name; printers.Add(new PluginLogFilePrinter(name)); if (logThread == null || !logThread.IsAlive) { logThread = new Thread(LogThread); logThread.Start(); } } /// /// Gets a child printer with the given name, either constructing a new one or using one that was already made. /// /// /// a child with the given sub-name internal StandardLogger GetChild(string name) { if (!children.TryGetValue(name, out var child)) { child = new StandardLogger(this, name); children.Add(name, child); } return child; } /// /// Adds a log printer to the logger. /// /// the printer to add public void AddPrinter(LogPrinter printer) { printers.Add(printer); } /// /// Logs a specific message at a given level. /// /// the message level /// the message to log public override void Log(Level level, string message) { if (message == null) throw new ArgumentNullException(nameof(message)); logQueue.Add(new LogMessage { Level = level, Message = message, Logger = this, Time = DateTime.Now }); } /// /// /// An override to which shows the method that called it. /// /// the message to log public override void Debug(string message) { // add source to message var stackFrame = new StackTrace().GetFrame(1); var method = stackFrame.GetMethod(); var lineNo = stackFrame.GetFileLineNumber(); var lineOffs = stackFrame.GetFileColumnNumber(); base.Debug(showSourceClass ? $"{{{method.DeclaringType?.FullName}::{method.Name}({lineNo}:{lineOffs})}} {message}" : message); } private struct LogMessage { public Level Level; public StandardLogger Logger; public string Message; public DateTime Time; } private static readonly BlockingCollection logQueue = new BlockingCollection(); private static Thread logThread; /// /// The log printer thread for . /// private static void LogThread() { var started = new HashSet(); while (logQueue.TryTake(out var msg, Timeout.Infinite)) { var logger = msg.Logger; IEnumerable printers = logger.printers; do { logger = logger.parent; if (logger != null) printers = printers.Concat(logger.printers); } while (logger != null); foreach (var printer in printers.Concat(defaultPrinters)) { try { if (((byte)msg.Level & (byte)printer.Filter) != 0) { if (!started.Contains(printer)) { printer.StartPrint(); started.Add(printer); } printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message); } } catch (Exception e) { Console.WriteLine($"printer errored: {e}"); } } if (logQueue.Count == 0) { foreach (var printer in started) { try { printer.EndPrint(); } catch (Exception e) { Console.WriteLine($"printer errored: {e}"); } } started.Clear(); } } } /// /// Stops and joins the log printer thread. /// internal static void StopLogThread() { logQueue.CompleteAdding(); logThread.Join(); } } /// /// A class providing extensions for various loggers. /// public static class LoggerExtensions { /// /// Gets a child logger, if supported. Currently the only defined and supported logger is , and most plugins will only ever receive this anyway. /// /// the parent /// the name of the child /// the child logger public static Logger GetChildLogger(this Logger logger, string name) { if (logger is StandardLogger standardLogger) return standardLogger.GetChild(name); throw new InvalidOperationException(); } } }