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.

367 lines
13 KiB

  1. using Apewer.Internals;
  2. using Apewer.Models;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Text;
  8. using System.Threading;
  9. using cc = System.ConsoleColor;
  10. namespace Apewer
  11. {
  12. /// <summary>日志记录程序。</summary>
  13. public class Logger
  14. {
  15. private string _key = Guid.NewGuid().ToString("n");
  16. private string _target = "";
  17. private bool _enabled = true;
  18. private Func<string, bool> _preoutput = null;
  19. private bool _useconsole = true;
  20. private bool _usefile = false;
  21. private bool _uselock = false;
  22. /// <summary>已启用。</summary>
  23. public virtual bool Enabled { get; set; }
  24. /// <summary>在后台线程处理日志。默认值:FALSE。</summary>
  25. internal virtual bool Background { get; set; } = false;
  26. /// <summary>捕获并丢弃事件中的异常。默认值:FALSE。</summary>
  27. public virtual bool Catch { get; set; } = false;
  28. /// <summary>使用控制台输出。默认值:TRUE。</summary>
  29. public virtual bool UseConsole
  30. {
  31. get { return _useconsole; }
  32. set { if (!_uselock) _useconsole = value; }
  33. }
  34. /// <summary>使用日志文件。默认值:FALSE。</summary>
  35. public virtual bool UseFile
  36. {
  37. get { return _usefile; }
  38. set { if (!_uselock) _usefile = value; }
  39. }
  40. /// <summary>异常。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  41. public virtual Event<Exception> OnException { get; set; }
  42. /// <summary>错误。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  43. public virtual Event<string> OnError { get; set; }
  44. /// <summary>注意。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  45. public virtual Event<string> OnWarning { get; set; }
  46. /// <summary>注意。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  47. public virtual Event<string> OnInfo { get; set; }
  48. /// <summary>文本。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  49. public virtual Event<string> OnText { get; set; }
  50. /// <summary>调试。设置处理方法以替代 UseConsole 和 UseFile。</summary>
  51. public virtual Event<string> OnDebug { get; set; }
  52. private void Invoke(Action action)
  53. {
  54. if (Background)
  55. {
  56. RuntimeUtility.InBackground(action);
  57. return;
  58. }
  59. action.Invoke();
  60. }
  61. /// <summary>记录异常。</summary>
  62. public virtual void Exception(object sender, Exception exception) => Invoke(() =>
  63. {
  64. if (OnException != null)
  65. {
  66. OnException(sender, exception);
  67. return;
  68. }
  69. var item = new LogItem();
  70. item.Tag = "EXCEPTION";
  71. item.Color = ConsoleColor.DarkMagenta;
  72. item.Sender = sender;
  73. item.Exception = exception;
  74. if (exception == null)
  75. {
  76. item.Content = "无效异常。";
  77. }
  78. else
  79. {
  80. var type = exception.GetType().FullName;
  81. try
  82. {
  83. item.Content = MergeContent(type, exception.Message);
  84. }
  85. catch
  86. {
  87. item.Content = $"获取异常 {type} 的消息时再次引发了异常。";
  88. }
  89. }
  90. if (UseConsole) ToConsole(item);
  91. if (UseFile) ToFile(item);
  92. });
  93. private void Colorful(object sender, string tag, Nullable<ConsoleColor> color, object[] content, Event<string> defined) => Invoke(() =>
  94. {
  95. if (defined != null)
  96. {
  97. defined.Invoke(sender, MergeContent(content));
  98. return;
  99. }
  100. var item = new LogItem();
  101. item.Sender = sender;
  102. item.Tag = tag;
  103. if (color != null) item.Color = color.Value;
  104. item.Content = MergeContent(content);
  105. if (UseConsole) ToConsole(item);
  106. if (UseFile) ToFile(item);
  107. });
  108. /// <summary>记录错误。多个 Content 参数将以“ | ”分隔。</summary>
  109. public virtual void Error(object sender, params object[] content) => Colorful(sender, "Error", cc.DarkRed, content, OnError);
  110. /// <summary>记录警告。多个 Content 参数将以“ | ”分隔。</summary>
  111. public virtual void Warning(object sender, params object[] content) => Colorful(sender, "Warning", cc.DarkYellow, content, OnWarning);
  112. /// <summary>记录警告。多个 Content 参数将以“ | ”分隔。</summary>
  113. public virtual void Info(object sender, params object[] content) => Colorful(sender, "Info", cc.DarkBlue, content, OnInfo);
  114. /// <summary>记录文本。多个 Content 参数将以“ | ”分隔。</summary>
  115. public virtual void Text(object sender, params object[] content) => Colorful(sender, "Text", null, content, OnText);
  116. /// <summary>记录调试。多个 Content 参数将以“ | ”分隔。</summary>
  117. [Conditional("DEBUG")]
  118. public virtual void Debug(object sender, params object[] content) => Colorful(sender, "Debug", null, content, OnDebug);
  119. /// <summary>创建新实例。</summary>
  120. public Logger() { }
  121. internal Logger(bool useConsole, bool useFile, bool enabled)
  122. {
  123. _useconsole = useConsole;
  124. _usefile = useFile;
  125. _uselock = true;
  126. _enabled = enabled;
  127. }
  128. #region 输出。
  129. internal static object FileLocker = new object();
  130. internal static object ConsoleLocker = new object();
  131. private static string MergeContent(params object[] content) => TextUtility.Join(" | ", content);
  132. private static string FormatSender(object sender)
  133. {
  134. if (sender == null) return null;
  135. if (sender is string) return sender as string;
  136. if (sender is Type) return ((Type)sender).FullName;
  137. var type = sender.GetType();
  138. var name = type.FullName;
  139. return name;
  140. }
  141. // 向控制台输出。
  142. private static void ToConsole(LogItem item)
  143. {
  144. var hasTag = !string.IsNullOrEmpty(item.Tag);
  145. var sender = FormatSender(item.Sender);
  146. var colorful = item.Color != null;
  147. lock (ConsoleLocker)
  148. {
  149. if (!colorful)
  150. {
  151. System.Console.WriteLine(ToText(item));
  152. return;
  153. }
  154. System.Console.ResetColor();
  155. System.Console.ForegroundColor = ConsoleColor.DarkGray;
  156. System.Console.Write(item.Clock);
  157. System.Console.ResetColor();
  158. if (hasTag)
  159. {
  160. System.Console.Write(" ");
  161. if (item.Color != null)
  162. {
  163. System.Console.BackgroundColor = item.Color.Value;
  164. System.Console.ForegroundColor = ConsoleColor.White;
  165. }
  166. System.Console.Write(" ");
  167. System.Console.Write(item.Tag);
  168. System.Console.Write(" ");
  169. }
  170. System.Console.ResetColor();
  171. if (!string.IsNullOrEmpty(sender))
  172. {
  173. System.Console.Write(" <");
  174. System.Console.Write(sender);
  175. System.Console.Write(">");
  176. }
  177. if (!string.IsNullOrEmpty(item.Content))
  178. {
  179. System.Console.Write(" ");
  180. System.Console.Write(item.Content);
  181. }
  182. System.Console.WriteLine();
  183. }
  184. }
  185. private static string ToText(LogItem item)
  186. {
  187. var sb = new StringBuilder();
  188. var sender = FormatSender(item.Sender);
  189. sb.Append(item.Clock);
  190. if (!string.IsNullOrEmpty(item.Tag))
  191. {
  192. sb.Append(" [");
  193. sb.Append(item.Tag);
  194. sb.Append("]");
  195. }
  196. if (!string.IsNullOrEmpty(sender))
  197. {
  198. sb.Append(" <");
  199. sb.Append(sender);
  200. sb.Append(">");
  201. }
  202. if (!string.IsNullOrEmpty(item.Content))
  203. {
  204. sb.Append(" ");
  205. sb.Append(item.Content);
  206. }
  207. return sb.ToString();
  208. }
  209. // 向日志文件输出文本,文件名按日期自动生成。
  210. private static string ToFile(string plain)
  211. {
  212. var text2 = TextUtility.Trim(plain) + "\r\n";
  213. lock (FileLocker)
  214. {
  215. var path = GetFileDir();
  216. if (string.IsNullOrEmpty(path)) return "写入日志文件失败:无法获取日志文件路径。";
  217. var bytes = TextUtility.ToBinary(TextUtility.Merge(plain, "\r\n"));
  218. if (!StorageUtility.AppendFile(path, bytes)) return "写入日志文件失败。";
  219. }
  220. return null;
  221. }
  222. // 向日志文件输出文本,文件名按日期自动生成。
  223. private static string ToFile(LogItem item) => ToFile(ToText(item));
  224. /// <summary>获取日志文件路径发生错误时返回 NULL 值。</summary>
  225. /// <remarks>d:\app\log\2020-02-02.log</remarks>
  226. /// <remarks>d:\www\app_data\log\2020-02-02.log</remarks>
  227. public static string GetFileDir()
  228. {
  229. // 找到 App_Data 目录。
  230. var appDir = RuntimeUtility.ApplicationPath;
  231. var dataDir = Path.Combine(appDir, "app_data");
  232. if (StorageUtility.DirectoryExists(dataDir)) appDir = dataDir;
  233. // 检查 Log 目录,不存在时创建,创建失败时返回。
  234. var logDir = Path.Combine(appDir, "log");
  235. if (!StorageUtility.AssureDirectory(logDir)) return null;
  236. // 文件不存在时创建新文件,无法创建时返回。
  237. var date = DateTime.Now.ToLucid(true, false, false, false);
  238. var filePath = Path.Combine(logDir, date + ".log");
  239. if (!StorageUtility.FileExists(filePath))
  240. {
  241. StorageUtility.WriteFile(filePath, TextUtility.Bom);
  242. if (!StorageUtility.FileExists(filePath)) return null;
  243. }
  244. // 返回 log 文件路径。
  245. return filePath;
  246. }
  247. #endregion
  248. #region 默认实列。
  249. private static Logger _default = new Logger(true, false, true);
  250. private static Logger _console = new Logger(true, false, true);
  251. private static Logger _web = new Logger(true, false, true);
  252. #if DEBUG
  253. internal static Logger _internals = new Logger(true, true, true);
  254. #else
  255. internal static Logger _internals = new Logger(true, false, false);
  256. #endif
  257. /// <summary>内部的日志记录程序。</summary>
  258. public static Logger Internals { get => _internals; }
  259. /// <summary>默认的日志记录程序。</summary>
  260. public static Logger Default { get => _default; }
  261. /// <summary>仅输出到控制台的日志记录程序。</summary>
  262. public static Logger Console { get => _console; }
  263. /// <summary>用于 Web 的日志记录程序。</summary>
  264. public static Logger Web { get => _web; }
  265. #endregion
  266. #region 静态调用。
  267. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
  268. public static void Write(params object[] content) => Default.Invoke(() =>
  269. {
  270. var console = Default.UseConsole;
  271. var file = Default.UseFile;
  272. if (console || file)
  273. {
  274. var item = new LogItem();
  275. item.Content = MergeContent(content);
  276. if (console) ToConsole(item);
  277. if (file) ToFile(item);
  278. }
  279. });
  280. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
  281. public static void Write<T>(params object[] content) => Default.Invoke(() =>
  282. {
  283. var console = Default.UseConsole;
  284. var file = Default.UseFile;
  285. if (console || file)
  286. {
  287. var item = new LogItem();
  288. item.Sender = typeof(T).FullName;
  289. item.Content = MergeContent(content);
  290. if (console) ToConsole(item);
  291. if (file) ToFile(item);
  292. }
  293. });
  294. #endregion
  295. }
  296. }