LocalFileWriter.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. using System;
  2. using System.IO;
  3. using System.Text;
  4. namespace Aivfo.OperationLog
  5. {
  6. /// <summary>
  7. /// 本地文件写入:调试级日志(14§4)+ 运行兜底(Kafka 发不出、队列满)+ 组件自身错误。
  8. /// 按天滚动;内部加锁串行写;全 try 兜底,绝不抛给业务。
  9. /// </summary>
  10. internal sealed class LocalFileWriter
  11. {
  12. private readonly string _dir;
  13. private readonly object _lock = new object();
  14. public LocalFileWriter(string dir)
  15. {
  16. _dir = string.IsNullOrEmpty(dir) ? "oplog_local" : dir;
  17. try { Directory.CreateDirectory(_dir); } catch { /* ignore */ }
  18. }
  19. /// <summary>调试级一行(结构化文本,不进 Kafka)。</summary>
  20. public void WriteDebug(OperationLogMessage m)
  21. {
  22. var line = $"{DateTimeOffset.FromUnixTimeMilliseconds(m.Time):yyyy-MM-dd HH:mm:ss.fff}\t" +
  23. $"[DEBUG]\ttrace={m.TraceId}\tmodule={m.Module}\top={m.Operation}\t" +
  24. $"house={m.HouseSn}\twell={m.WellSn}\tresult={m.Result}\t" +
  25. $"in={Truncate(m.Input)}\tout={Truncate(m.Output)}\terr={m.Error}";
  26. Write("debug", line);
  27. }
  28. /// <summary>兜底:完整 JSON 落本地(Kafka 不可用 / 队列满丢弃前保留)。</summary>
  29. public void WriteFallback(string json)
  30. {
  31. Write("fallback", json);
  32. }
  33. /// <summary>组件自身错误(连不上 Kafka、序列化失败等)。</summary>
  34. public void WriteSelfError(string msg)
  35. {
  36. Write("error", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}\t{msg}");
  37. }
  38. private void Write(string kind, string line)
  39. {
  40. try
  41. {
  42. var path = Path.Combine(_dir, $"oplog-{kind}-{DateTime.Now:yyyyMMdd}.log");
  43. lock (_lock)
  44. {
  45. File.AppendAllText(path, line + Environment.NewLine, Encoding.UTF8);
  46. }
  47. }
  48. catch { /* 本地写失败也不能拖垮业务 */ }
  49. }
  50. // ===== G3-3 / 14§11 兜底补送原语 =====
  51. /// <summary>列出待补送的兜底文件(不含正在补送的 .resending)。按文件名升序≈时间升序。</summary>
  52. public string[] ListFallbackFiles()
  53. {
  54. try
  55. {
  56. lock (_lock)
  57. {
  58. if (!Directory.Exists(_dir)) return Array.Empty<string>();
  59. var files = Directory.GetFiles(_dir, "oplog-fallback-*.log");
  60. Array.Sort(files, StringComparer.OrdinalIgnoreCase);
  61. return files;
  62. }
  63. }
  64. catch { return Array.Empty<string>(); }
  65. }
  66. /// <summary>
  67. /// 认领一个兜底文件:原子重命名为 .resending(避免与 append 竞争、避免并发重复补送)。
  68. /// 成功返回新路径,失败(已被认领/被占用)返回 null。
  69. /// </summary>
  70. public string ClaimFallback(string path)
  71. {
  72. try
  73. {
  74. lock (_lock)
  75. {
  76. if (!File.Exists(path)) return null;
  77. var dst = path + "." + DateTime.Now.ToString("HHmmssfff") + ".resending";
  78. File.Move(path, dst);
  79. return dst;
  80. }
  81. }
  82. catch { return null; }
  83. }
  84. /// <summary>读取认领文件的所有非空行。</summary>
  85. public string[] ReadLines(string path)
  86. {
  87. try { return File.ReadAllLines(path, Encoding.UTF8); }
  88. catch { return Array.Empty<string>(); }
  89. }
  90. /// <summary>删除文件(补送成功后清理认领文件)。</summary>
  91. public void DeleteQuietly(string path)
  92. {
  93. try { if (File.Exists(path)) File.Delete(path); }
  94. catch { /* ignore */ }
  95. }
  96. private static string Truncate(string s, int max = 500)
  97. {
  98. if (string.IsNullOrEmpty(s)) return s;
  99. return s.Length <= max ? s : s.Substring(0, max) + "...(" + s.Length + ")";
  100. }
  101. }
  102. }