You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
9.3 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. using IPA.Config;
  2. using IPA.Logging.Printers;
  3. using System;
  4. using System.Collections.Concurrent;
  5. using System.Collections.Generic;
  6. using System.Diagnostics;
  7. using System.Linq;
  8. using System.Threading;
  9. namespace IPA.Logging
  10. {
  11. /// <summary>
  12. /// The default (and standard) <see cref="Logger"/> implementation.
  13. /// </summary>
  14. /// <remarks>
  15. /// <see cref="StandardLogger"/> uses a multi-threaded approach to logging. All actual I/O is done on another thread,
  16. /// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them.
  17. ///
  18. /// This logger supports child loggers. Use <see cref="LoggerExtensions.GetChildLogger"/> to safely get a child.
  19. /// The modification of printers on a parent are reflected down the chain.
  20. /// </remarks>
  21. public class StandardLogger : Logger
  22. {
  23. private static readonly List<LogPrinter> defaultPrinters = new List<LogPrinter>()
  24. {
  25. new ColoredConsolePrinter()
  26. {
  27. Filter = LogLevel.DebugOnly,
  28. Color = ConsoleColor.Green,
  29. },
  30. new ColoredConsolePrinter()
  31. {
  32. Filter = LogLevel.InfoOnly,
  33. Color = ConsoleColor.White,
  34. },
  35. new ColoredConsolePrinter()
  36. {
  37. Filter = LogLevel.WarningOnly,
  38. Color = ConsoleColor.Yellow,
  39. },
  40. new ColoredConsolePrinter()
  41. {
  42. Filter = LogLevel.ErrorOnly,
  43. Color = ConsoleColor.Red,
  44. },
  45. new ColoredConsolePrinter()
  46. {
  47. Filter = LogLevel.CriticalOnly,
  48. Color = ConsoleColor.Magenta,
  49. },
  50. new GlobalLogFilePrinter()
  51. };
  52. /// <summary>
  53. /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger.
  54. /// </summary>
  55. /// <param name="printer"></param>
  56. internal static void AddDefaultPrinter(LogPrinter printer)
  57. {
  58. defaultPrinters.Add(printer);
  59. }
  60. private readonly string logName;
  61. private static bool showSourceClass;
  62. /// <summary>
  63. /// All levels defined by this filter will be sent to loggers. All others will be ignored.
  64. /// </summary>
  65. public static LogLevel PrintFilter { get; set; } = LogLevel.All;
  66. private readonly List<LogPrinter> printers = new List<LogPrinter>();
  67. private readonly StandardLogger parent;
  68. private readonly Dictionary<string, StandardLogger> children = new Dictionary<string, StandardLogger>();
  69. /// <summary>
  70. /// Configures internal debug settings based on the config passed in.
  71. /// </summary>
  72. /// <param name="cfg"></param>
  73. internal static void Configure(SelfConfig cfg)
  74. {
  75. showSourceClass = cfg.Debug.ShowCallSource;
  76. PrintFilter = cfg.Debug.ShowDebug ? LogLevel.All : LogLevel.InfoUp;
  77. }
  78. private StandardLogger(StandardLogger parent, string subName)
  79. {
  80. logName = $"{parent.logName}/{subName}";
  81. this.parent = parent;
  82. printers = new List<LogPrinter>()
  83. {
  84. new PluginSubLogPrinter(parent.logName, subName)
  85. };
  86. if (logThread == null || !logThread.IsAlive)
  87. {
  88. logThread = new Thread(LogThread);
  89. logThread.Start();
  90. }
  91. }
  92. internal StandardLogger(string name)
  93. {
  94. logName = name;
  95. printers.Add(new PluginLogFilePrinter(name));
  96. if (logThread == null || !logThread.IsAlive)
  97. {
  98. logThread = new Thread(LogThread);
  99. logThread.Start();
  100. }
  101. }
  102. /// <summary>
  103. /// Gets a child printer with the given name, either constructing a new one or using one that was already made.
  104. /// </summary>
  105. /// <param name="name"></param>
  106. /// <returns>a child <see cref="StandardLogger"/> with the given sub-name</returns>
  107. internal StandardLogger GetChild(string name)
  108. {
  109. if (!children.TryGetValue(name, out var child))
  110. {
  111. child = new StandardLogger(this, name);
  112. children.Add(name, child);
  113. }
  114. return child;
  115. }
  116. /// <summary>
  117. /// Adds a log printer to the logger.
  118. /// </summary>
  119. /// <param name="printer">the printer to add</param>
  120. public void AddPrinter(LogPrinter printer)
  121. {
  122. printers.Add(printer);
  123. }
  124. /// <summary>
  125. /// Logs a specific message at a given level.
  126. /// </summary>
  127. /// <param name="level">the message level</param>
  128. /// <param name="message">the message to log</param>
  129. public override void Log(Level level, string message)
  130. {
  131. if (message == null)
  132. throw new ArgumentNullException(nameof(message));
  133. logQueue.Add(new LogMessage
  134. {
  135. Level = level,
  136. Message = message,
  137. Logger = this,
  138. Time = DateTime.Now
  139. });
  140. }
  141. /// <inheritdoc />
  142. /// <summary>
  143. /// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
  144. /// </summary>
  145. /// <param name="message">the message to log</param>
  146. public override void Debug(string message)
  147. {
  148. // add source to message
  149. var stackFrame = new StackTrace().GetFrame(1);
  150. var method = stackFrame.GetMethod();
  151. var lineNo = stackFrame.GetFileLineNumber();
  152. var lineOffs = stackFrame.GetFileColumnNumber();
  153. base.Debug(showSourceClass
  154. ? $"{{{method.DeclaringType?.FullName}::{method.Name}({lineNo}:{lineOffs})}} {message}"
  155. : message);
  156. }
  157. private struct LogMessage
  158. {
  159. public Level Level;
  160. public StandardLogger Logger;
  161. public string Message;
  162. public DateTime Time;
  163. }
  164. private static readonly BlockingCollection<LogMessage> logQueue = new BlockingCollection<LogMessage>();
  165. private static Thread logThread;
  166. /// <summary>
  167. /// The log printer thread for <see cref="StandardLogger"/>.
  168. /// </summary>
  169. private static void LogThread()
  170. {
  171. var started = new HashSet<LogPrinter>();
  172. while (logQueue.TryTake(out var msg, Timeout.Infinite))
  173. {
  174. var logger = msg.Logger;
  175. IEnumerable<LogPrinter> printers = logger.printers;
  176. do
  177. {
  178. logger = logger.parent;
  179. if (logger != null)
  180. printers = printers.Concat(logger.printers);
  181. } while (logger != null);
  182. foreach (var printer in printers.Concat(defaultPrinters))
  183. {
  184. try
  185. {
  186. if (((byte)msg.Level & (byte)printer.Filter) != 0)
  187. {
  188. if (!started.Contains(printer))
  189. {
  190. printer.StartPrint();
  191. started.Add(printer);
  192. }
  193. printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message);
  194. }
  195. }
  196. catch (Exception e)
  197. {
  198. Console.WriteLine($"printer errored: {e}");
  199. }
  200. }
  201. if (logQueue.Count == 0)
  202. {
  203. foreach (var printer in started)
  204. {
  205. try
  206. {
  207. printer.EndPrint();
  208. }
  209. catch (Exception e)
  210. {
  211. Console.WriteLine($"printer errored: {e}");
  212. }
  213. }
  214. started.Clear();
  215. }
  216. }
  217. }
  218. /// <summary>
  219. /// Stops and joins the log printer thread.
  220. /// </summary>
  221. internal static void StopLogThread()
  222. {
  223. logQueue.CompleteAdding();
  224. logThread.Join();
  225. }
  226. }
  227. /// <summary>
  228. /// A class providing extensions for various loggers.
  229. /// </summary>
  230. public static class LoggerExtensions
  231. {
  232. /// <summary>
  233. /// Gets a child logger, if supported. Currently the only defined and supported logger is <see cref="StandardLogger"/>, and most plugins will only ever receive this anyway.
  234. /// </summary>
  235. /// <param name="logger">the parent <see cref="Logger"/></param>
  236. /// <param name="name">the name of the child</param>
  237. /// <returns>the child logger</returns>
  238. public static Logger GetChildLogger(this Logger logger, string name)
  239. {
  240. if (logger is StandardLogger standardLogger)
  241. return standardLogger.GetChild(name);
  242. throw new InvalidOperationException();
  243. }
  244. }
  245. }