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.

443 lines
16 KiB

3 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
3 years ago
3 years ago
4 years ago
4 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
  1. using Apewer.Internals;
  2. using Apewer.Models;
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Diagnostics;
  7. using System.IO;
  8. using System.Text;
  9. using System.Threading;
  10. using static System.ConsoleColor;
  11. namespace Apewer
  12. {
  13. /// <summary>日志记录程序。</summary>
  14. public sealed class Logger
  15. {
  16. private bool _useconsole = true;
  17. private bool _usefile = false;
  18. private bool _uselock = false;
  19. private int _reserved = -1;
  20. private LogCollector _collector = null;
  21. private string _lastdate = null;
  22. /// <summary>当前日志记录器的名称。</summary>
  23. public string Name { get; set; }
  24. /// <summary>已启用。</summary>
  25. public bool Enabled { get; set; }
  26. /// <summary>在后台线程处理日志。默认值:FALSE。</summary>
  27. internal bool Background { get; set; } = false;
  28. /// <summary>使用控制台输出。默认值:TRUE。</summary>
  29. public bool UseConsole
  30. {
  31. get { return _useconsole; }
  32. set { if (!_uselock) _useconsole = value; }
  33. }
  34. /// <summary>使用日志文件。默认值:FALSE。</summary>
  35. public bool UseFile
  36. {
  37. get { return _usefile; }
  38. set
  39. {
  40. if (_uselock) return;
  41. if (_cache_count > 0) Flush();
  42. _usefile = value;
  43. }
  44. }
  45. /// <summary>日志文件的保留天数(不包含今天)。指定 <see cref="FilePathGetter"/> 时此属性无效。</summary>
  46. /// <remarks>默认值:-1,保留所有日志。</remarks>
  47. public int FileReserved { get => _reserved; set => _reserved = value; }
  48. /// <summary>设置过期日志文件的回收程序。指定 <see cref="FilePathGetter"/> 时此属性无效。</summary>
  49. /// <remarks>默认值:NULL,删除文件。</remarks>
  50. public LogCollector Collector { get => _collector; set => _collector = value; }
  51. private void Invoke(Action action)
  52. {
  53. if (Background)
  54. {
  55. RuntimeUtility.InBackground(action, true);
  56. return;
  57. }
  58. try { action.Invoke(); } catch { }
  59. }
  60. internal void Colorful(object sender, string tag, Nullable<ConsoleColor> color, object[] content, Exception exception = null)
  61. {
  62. if (!Enabled) return;
  63. Invoke(() =>
  64. {
  65. var item = new LogItem();
  66. item.Sender = sender;
  67. item.Tag = tag;
  68. item.Color = color;
  69. item.Content = MergeContent(content);
  70. item.Exception = exception;
  71. if (UseConsole) ToConsole(item);
  72. if (UseFile)
  73. {
  74. var plain = ToText(item);
  75. lock (_cache_locker)
  76. {
  77. if (_cache_capacity > 0)
  78. {
  79. _cache_array[_cache_count] = plain;
  80. _cache_count += 1;
  81. if (_cache_count == _cache_capacity) Flush();
  82. }
  83. else
  84. {
  85. ToFile(plain, this);
  86. }
  87. }
  88. }
  89. });
  90. }
  91. /// <summary>记录异常。</summary>
  92. internal void InnerException(object sender, Exception exception)
  93. {
  94. if (!Enabled) return;
  95. var type = null as string;
  96. var content = null as string;
  97. if (exception != null)
  98. {
  99. try
  100. {
  101. type = exception.GetType().Name;
  102. content = MergeContent(new object[] { type, exception.Message });
  103. }
  104. catch { }
  105. }
  106. if (content == null) content = "无效的 Exception 实例。";
  107. Colorful(sender, "Exception", DarkMagenta, new object[] { type, content }, exception);
  108. }
  109. /// <summary>创建新实例。</summary>
  110. public Logger()
  111. {
  112. Enabled = true;
  113. }
  114. private Logger(string name, bool useConsole, bool useFile, bool enabled)
  115. {
  116. Name = name;
  117. UseConsole = useConsole;
  118. UseFile = useFile;
  119. Enabled = enabled;
  120. }
  121. #region 文件输出缓存。
  122. private object _cache_locker = new object();
  123. private int _cache_capacity = 0;
  124. private int _cache_count = 0;
  125. private string[] _cache_array = null;
  126. /// <summary>设置缓存容量,指定为 0 可取消缓存,较大的日志缓存可能会耗尽内存。</summary>
  127. public void SetCache(int capacity)
  128. {
  129. lock (_cache_locker)
  130. {
  131. if (_cache_count > 0) Flush();
  132. if (capacity == _cache_capacity) return;
  133. _cache_capacity = capacity > 0 ? capacity : 0;
  134. _cache_array = (capacity < 1) ? null : new string[capacity];
  135. }
  136. }
  137. /// <summary>将缓存的日志写入文件。</summary>
  138. public void Flush()
  139. {
  140. lock (_cache_locker)
  141. {
  142. if (_cache_count < 1) return;
  143. var sb = new StringBuilder();
  144. for (var i = 0; i < _cache_count; i++)
  145. {
  146. sb.Append(_cache_array[i]);
  147. sb.Append("\r\n");
  148. }
  149. ToFile(sb.ToString(), this, false);
  150. _cache_count = 0;
  151. _cache_array = new string[_cache_capacity];
  152. }
  153. }
  154. #endregion
  155. #region 输出。
  156. internal static object FileLocker = new object();
  157. internal static object ConsoleLocker = new object();
  158. /// <summary>获取用于保存日志文件的路径。</summary>
  159. public static Func<Logger, string> FilePathGetter { get; set; }
  160. private static string MergeContent(object[] content) => TextUtility.Join(" | ", content);
  161. private static string FormatSender(object sender)
  162. {
  163. if (sender == null) return null;
  164. if (sender is string) return sender as string;
  165. if (sender is Type) return ((Type)sender).Name;
  166. return sender.GetType().Name;
  167. }
  168. // 向控制台输出。
  169. private static void ToConsole(LogItem item)
  170. {
  171. var hasTag = !string.IsNullOrEmpty(item.Tag);
  172. var sender = FormatSender(item.Sender);
  173. var colorful = item.Color != null;
  174. lock (ConsoleLocker)
  175. {
  176. if (!colorful)
  177. {
  178. System.Console.WriteLine(ToText(item));
  179. return;
  180. }
  181. System.Console.ResetColor();
  182. System.Console.ForegroundColor = DarkGray;
  183. System.Console.Write(item.Clock);
  184. System.Console.ResetColor();
  185. if (hasTag)
  186. {
  187. System.Console.Write(" ");
  188. if (item.Color != null)
  189. {
  190. System.Console.BackgroundColor = item.Color.Value;
  191. System.Console.ForegroundColor = White;
  192. }
  193. System.Console.Write(" ");
  194. System.Console.Write(item.Tag);
  195. System.Console.Write(" ");
  196. }
  197. System.Console.ResetColor();
  198. if (!string.IsNullOrEmpty(sender))
  199. {
  200. System.Console.Write(" <");
  201. System.Console.Write(sender);
  202. System.Console.Write(">");
  203. }
  204. if (!string.IsNullOrEmpty(item.Content))
  205. {
  206. System.Console.Write(" ");
  207. System.Console.Write(item.Content);
  208. }
  209. System.Console.WriteLine();
  210. }
  211. }
  212. private static string ToText(LogItem item)
  213. {
  214. var sb = new StringBuilder();
  215. var sender = FormatSender(item.Sender);
  216. sb.Append(item.Clock);
  217. if (!string.IsNullOrEmpty(item.Tag))
  218. {
  219. sb.Append(" [");
  220. sb.Append(item.Tag);
  221. sb.Append("]");
  222. }
  223. if (!string.IsNullOrEmpty(sender))
  224. {
  225. sb.Append(" <");
  226. sb.Append(sender);
  227. sb.Append(">");
  228. }
  229. if (!string.IsNullOrEmpty(item.Content))
  230. {
  231. sb.Append(" ");
  232. sb.Append(item.Content);
  233. }
  234. return sb.ToString();
  235. }
  236. // 向日志文件输出文本,文件名按日期自动生成。
  237. private static string ToFile(string plain, Logger logger, bool crlf = true)
  238. {
  239. lock (FileLocker)
  240. {
  241. var path = GetFilePath(logger);
  242. if (string.IsNullOrEmpty(path)) return "写入日志文件失败:无法获取日志文件路径。";
  243. var bytes = TextUtility.Bytes(crlf ? TextUtility.Merge(plain, "\r\n") : plain);
  244. if (!StorageUtility.AppendFile(path, bytes)) return "写入日志文件失败。";
  245. }
  246. return null;
  247. }
  248. /// <summary>获取日志文件路径发生错误时返回 NULL 值。</summary>
  249. /// <remarks>默认例:<br/>d:\app\log\1970-01-01.log<br/>d:\www\app_data\log\1970-01-01.log</remarks>
  250. public static string GetFilePath(Logger logger = null)
  251. {
  252. var getter = FilePathGetter;
  253. if (getter != null) try { return getter.Invoke(logger); } catch { }
  254. // 找到 App_Data 目录。
  255. var appDir = RuntimeUtility.ApplicationPath;
  256. var dataDir = Path.Combine(appDir, "app_data");
  257. if (StorageUtility.DirectoryExists(dataDir)) appDir = dataDir;
  258. // 检查 Log 目录,不存在时创建,创建失败时返回。
  259. var logDir = Path.Combine(appDir, "log");
  260. if (!StorageUtility.AssureDirectory(logDir)) return null;
  261. // 文件不存在时创建新文件,无法创建时返回。
  262. var now = DateTime.Now;
  263. var date = now.Lucid(true, false, false, false);
  264. var filePath = Path.Combine(logDir, date + FileExt);
  265. if (!StorageUtility.FileExists(filePath))
  266. {
  267. StorageUtility.WriteFile(filePath, TextUtility.Bom);
  268. if (!StorageUtility.FileExists(filePath)) return null;
  269. }
  270. // 检查过期文件。
  271. if (logger != null && getter == null && logger._reserved > -1)
  272. {
  273. if (date != logger._lastdate)
  274. {
  275. logger._lastdate = date;
  276. RuntimeUtility.StartThread(() => CollectFiles(logger, now, logDir));
  277. }
  278. }
  279. // 返回 log 文件路径。
  280. return filePath;
  281. }
  282. static void CollectFiles(Logger logger, DateTime now, string logDir)
  283. {
  284. var reserved = logger._reserved;
  285. if (reserved < 0) return;
  286. var collector = logger._collector;
  287. var today = DateTime.Now.Date;
  288. var paths = StorageUtility.GetSubFiles(logDir);
  289. foreach (var path in paths)
  290. {
  291. var fileName = Path.GetFileNameWithoutExtension(path);
  292. var fileExt = Path.GetExtension(path);
  293. if (fileName.Length != 10) continue;
  294. if (fileName[4] != '-') continue;
  295. if (fileName[7] != '-') continue;
  296. if (fileExt != FileExt) continue;
  297. var dt = ClockUtility.ParseLucid(fileName);
  298. if (dt == null || !dt.HasValue) continue;
  299. var days = Convert.ToInt32(Convert.ToInt64((today - dt.Value).TotalMilliseconds) / 86400000L);
  300. if (days > reserved)
  301. {
  302. if (collector == null) StorageUtility.DeleteFile(path);
  303. else collector(path, days);
  304. }
  305. }
  306. }
  307. #endregion
  308. #region 默认实列。
  309. private static Logger _default = new Logger("Apewer.Logger.Default", true, false, true);
  310. private static Logger _console = new Logger("Apewer.Logger.Console", true, false, true);
  311. private static Logger _web = new Logger("Apewer.Logger.Web", true, false, true);
  312. #if DEBUG
  313. internal static Logger _internals = new Logger("Apewer.Logger.Internals", true, false, true);
  314. #else
  315. internal static Logger _internals = new Logger("Apewer.Logger.Internals", true, false, false);
  316. #endif
  317. /// <summary>内部的日志记录程序。</summary>
  318. public static Logger Internals { get => _internals; }
  319. /// <summary>默认的日志记录程序。</summary>
  320. public static Logger Default { get => _default; }
  321. /// <summary>仅输出到控制台的日志记录程序。</summary>
  322. public static Logger Console { get => _console; }
  323. /// <summary>用于 Web 的日志记录程序。</summary>
  324. public static Logger Web { get => _web; }
  325. #endregion
  326. #region 静态。
  327. const string FileExt = ".log";
  328. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
  329. public static void Write(params object[] content) => Default.Colorful(null, null, null, content, null);
  330. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期,多个 Content 参数将以“ | ”分隔。</summary>
  331. public static void Write<T>(params object[] content) => Default.Colorful(typeof(T), null, null, content, null);
  332. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期。</summary>
  333. public static void Write(Exception exception) => Default.InnerException(null, exception);
  334. /// <summary>使用 Logger.Default 写入日志,自动添加时间和日期。</summary>
  335. public static void Write<T>(Exception exception) => Default.InnerException(typeof(T), exception);
  336. /// <summary>压缩日志文件的内容,另存为 ZIP 文件,并删除原日志文件。</summary>
  337. public static void CollectToZip(string path)
  338. {
  339. if (!File.Exists(path)) return;
  340. var bytes = StorageUtility.ReadFile(path);
  341. if (bytes.Length > 0)
  342. {
  343. var zipDict = new Dictionary<string, byte[]>();
  344. zipDict.Add(Path.GetFileName(path), bytes);
  345. var zipData = BytesUtility.ToZip(zipDict);
  346. if (zipData != null && zipData.Length > 0)
  347. {
  348. var zipPath = path + ".zip";
  349. StorageUtility.WriteFile(zipPath, zipData);
  350. }
  351. }
  352. StorageUtility.DeleteFile(path);
  353. }
  354. /// <summary>压缩日志文件的内容,另存为 GZIP 文件,并删除原日志文件。</summary>
  355. public static void CollectToGZip(string path)
  356. {
  357. if (!File.Exists(path)) return;
  358. var bytes = StorageUtility.ReadFile(path);
  359. if (bytes.Length > 0)
  360. {
  361. var gzipData = BytesUtility.ToGzip(bytes);
  362. if (gzipData != null || gzipData.Length > 0)
  363. {
  364. var gzipPath = path + ".gzip";
  365. StorageUtility.WriteFile(gzipPath, gzipData);
  366. }
  367. }
  368. StorageUtility.DeleteFile(path);
  369. }
  370. #endregion
  371. }
  372. }