OperationLogConfigWatcher.cs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. using System;
  2. using System.IO;
  3. using System.Threading;
  4. namespace Aivfo.OperationLog
  5. {
  6. /// <summary>
  7. /// 配置热加载(G3-3 / 14§10):后台定时轮询一个 JSON 配置文件,文件变化即热应用到
  8. /// <see cref="OperationLogOptions"/>(全局开关/级别 + 模块级开关),无需改代码重编。
  9. /// <para>
  10. /// "集中下发"落地方式:日志微服务 / 运维把统一配置文件下发(覆盖)到各端
  11. /// <see cref="OperationLogOptions.ConfigFilePath"/> 指向的路径即可;本组件负责读取并热生效。
  12. /// </para>
  13. /// 全 try 兜底:读/解析失败沿用旧配置,绝不抛、绝不影响业务。
  14. /// </summary>
  15. internal sealed class OperationLogConfigWatcher : IDisposable
  16. {
  17. private readonly OperationLogOptions _options;
  18. private readonly Action<string> _onSelfError;
  19. private readonly Timer _timer;
  20. private DateTime _lastWriteUtc = DateTime.MinValue;
  21. private long _lastLength = -1;
  22. private volatile bool _disposed;
  23. public OperationLogConfigWatcher(OperationLogOptions options, Action<string> onSelfError)
  24. {
  25. _options = options;
  26. _onSelfError = onSelfError;
  27. var periodMs = Math.Max(3, _options.ConfigReloadSeconds) * 1000;
  28. // 启动即先读一次(dueTime=0),随后周期轮询。
  29. _timer = new Timer(Poll, null, 0, periodMs);
  30. }
  31. private void Poll(object state)
  32. {
  33. if (_disposed) return;
  34. try
  35. {
  36. var path = _options.ConfigFilePath;
  37. if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return;
  38. // 仅在 文件大小或写时间 变化时才重读重应用(省 IO、避免无谓刷新)。
  39. var info = new FileInfo(path);
  40. var writeUtc = info.LastWriteTimeUtc;
  41. var len = info.Length;
  42. if (writeUtc == _lastWriteUtc && len == _lastLength) return;
  43. string json = SafeReadAllText(path);
  44. if (json == null) return;
  45. _options.ApplyConfigJson(json);
  46. _lastWriteUtc = writeUtc;
  47. _lastLength = len;
  48. _onSelfError?.Invoke($"操作日志配置已热加载: {path}");
  49. }
  50. catch (Exception ex)
  51. {
  52. try { _onSelfError?.Invoke("配置热加载失败(沿用旧配置): " + ex.Message); } catch { }
  53. }
  54. }
  55. /// <summary>容错读取:文件可能正被写,读失败返回 null(下轮再试)。</summary>
  56. private static string SafeReadAllText(string path)
  57. {
  58. try
  59. {
  60. using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  61. using (var sr = new StreamReader(fs, System.Text.Encoding.UTF8))
  62. {
  63. return sr.ReadToEnd();
  64. }
  65. }
  66. catch { return null; }
  67. }
  68. public void Dispose()
  69. {
  70. if (_disposed) return;
  71. _disposed = true;
  72. try { _timer?.Dispose(); } catch { /* ignore */ }
  73. }
  74. }
  75. }