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.

387 lines
13 KiB

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