-
Notifications
You must be signed in to change notification settings - Fork 523
/
TextFileLog.cs
351 lines (297 loc) · 11.5 KB
/
TextFileLog.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
using System.Collections.Concurrent;
using System.Text;
using NewLife.Threading;
namespace NewLife.Log;
/// <summary>文本文件日志类。提供向文本文件写日志的能力</summary>
/// <remarks>
/// 两大用法:
/// 1,Create(path, fileFormat) 指定日志目录和文件名格式
/// 2,CreateFile(path) 指定文件,一直往里面写
///
/// 2015-06-01 为了继承TextFileLog,增加了无参构造函数,修改了异步写日志方法为虚方法,可以进行重载
/// </remarks>
public class TextFileLog : Logger, IDisposable
{
#region 属性
/// <summary>日志目录</summary>
public String LogPath { get; set; }
/// <summary>日志文件格式。默认{0:yyyy_MM_dd}.log</summary>
public String FileFormat { get; set; }
/// <summary>日志文件上限。超过上限后拆分新日志文件,默认10MB,0表示不限制大小</summary>
public Int32 MaxBytes { get; set; } = 10;
/// <summary>日志文件备份。超过备份数后,最旧的文件将被删除,默认100,0表示不限制个数</summary>
public Int32 Backups { get; set; } = 100;
private readonly Boolean _isFile = false;
/// <summary>是否当前进程的第一次写日志</summary>
private Boolean _isFirst = false;
#endregion
#region 构造
/// <summary>该构造函数没有作用,为了继承而设置</summary>
public TextFileLog()
{
LogPath = String.Empty;
var set = Setting.Current;
FileFormat = set.LogFileFormat;
}
internal TextFileLog(String path, Boolean isfile, String? fileFormat = null)
{
LogPath = path;
_isFile = isfile;
var set = Setting.Current;
if (!fileFormat.IsNullOrEmpty())
FileFormat = fileFormat;
else
FileFormat = set.LogFileFormat;
MaxBytes = set.LogFileMaxBytes;
Backups = set.LogFileBackups;
_Timer = new TimerX(DoWriteAndClose, null, 0_000, 5_000) { Async = true };
}
private static readonly ConcurrentDictionary<String, TextFileLog> cache = new(StringComparer.OrdinalIgnoreCase);
/// <summary>每个目录的日志实例应该只有一个,所以采用静态创建</summary>
/// <param name="path">日志目录或日志文件路径</param>
/// <param name="fileFormat"></param>
/// <returns></returns>
public static TextFileLog Create(String path, String? fileFormat = null)
{
//if (path.IsNullOrEmpty()) path = XTrace.LogPath;
if (path.IsNullOrEmpty()) path = "Log";
var key = (path + fileFormat).ToLower();
return cache.GetOrAdd(key, k => new TextFileLog(path, false, fileFormat));
}
/// <summary>每个目录的日志实例应该只有一个,所以采用静态创建</summary>
/// <param name="path">日志目录或日志文件路径</param>
/// <returns></returns>
public static TextFileLog CreateFile(String path)
{
if (path.IsNullOrEmpty()) throw new ArgumentNullException(nameof(path));
return cache.GetOrAdd(path, k => new TextFileLog(k, true));
}
/// <summary>销毁</summary>
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
/// <summary>销毁</summary>
/// <param name="disposing"></param>
protected virtual void Dispose(Boolean disposing)
{
_Timer.TryDispose();
// 销毁前把队列日志输出
if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0) WriteAndClose(DateTime.MinValue);
}
#endregion
#region 内部方法
private StreamWriter? LogWriter;
private String? CurrentLogFile;
private Int32 _logFileError;
/// <summary>初始化日志记录文件</summary>
private StreamWriter? InitLog(String logfile)
{
try
{
logfile.EnsureDirectory(true);
var stream = new FileStream(logfile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
var writer = new StreamWriter(stream, Encoding.UTF8);
// 写日志头
if (!_isFirst)
{
_isFirst = true;
// 因为指定了编码,比如UTF8,开头就会写入3个字节,所以这里不能拿长度跟0比较
if (writer.BaseStream.Length > 10) writer.WriteLine();
writer.Write(GetHead());
}
_logFileError = 0;
return LogWriter = writer;
}
catch (Exception ex)
{
_logFileError++;
Console.WriteLine("创建日志文件失败:{0}", ex.Message);
return null;
}
}
/// <summary>获取日志文件路径</summary>
/// <returns></returns>
private String? GetLogFile()
{
// 单日志文件
if (_isFile) return LogPath.GetBasePath();
// 目录多日志文件
var logfile = LogPath.CombinePath(String.Format(FileFormat, TimerX.Now.AddHours(Setting.Current.UtcIntervalHours), Level)).GetBasePath();
// 是否限制文件大小
if (MaxBytes == 0) return logfile;
// 找到今天第一个未达到最大上限的文件
var max = MaxBytes * 1024L * 1024L;
var ext = Path.GetExtension(logfile);
var name = logfile.TrimEnd(ext);
for (var i = 1; i < 1024; i++)
{
if (i > 1) logfile = $"{name}_{i}{ext}";
var fi = logfile.AsFile();
if (!fi.Exists || fi.Length < max) return logfile;
}
return null;
}
#endregion
#region 异步写日志
private readonly TimerX? _Timer;
private readonly ConcurrentQueue<String> _Logs = new();
private volatile Int32 _logCount;
private Int32 _writing;
private DateTime _NextClose;
/// <summary>写文件</summary>
protected virtual void WriteFile()
{
var writer = LogWriter;
var now = TimerX.Now.AddHours(Setting.Current.UtcIntervalHours);
var logFile = GetLogFile();
if (logFile.IsNullOrEmpty()) return;
if (!_isFile && logFile != CurrentLogFile)
{
writer.TryDispose();
writer = null;
CurrentLogFile = logFile;
_logFileError = 0;
}
// 错误过多时不再尝试创建日志文件。下一天更换日志文件名后,将会再次尝试
if (writer == null && _logFileError >= 3) return;
// 初始化日志读写器
writer ??= InitLog(logFile);
if (writer == null) return;
// 依次把队列日志写入文件
while (_Logs.TryDequeue(out var str))
{
Interlocked.Decrement(ref _logCount);
// 写日志。TextWriter.WriteLine内需要拷贝,浪费资源
//writer.WriteLine(str);
writer.Write(str);
writer.WriteLine();
}
// 写完一批后,刷一次磁盘
writer?.Flush();
// 连续5秒没日志,就关闭
_NextClose = now.AddSeconds(5);
}
/// <summary>关闭文件</summary>
private void DoWriteAndClose(Object? state)
{
// 同步写日志
if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0) WriteAndClose(_NextClose);
// 检查文件是否超过上限
if (!_isFile && Backups > 0)
{
// 判断日志目录是否已存在
var di = LogPath.GetBasePath().AsDirectory();
if (di.Exists)
{
// 删除*.del
try
{
var dels = di.GetFiles("*.del");
if (dels != null && dels.Length > 0)
{
foreach (var item in dels)
{
item.Delete();
}
}
}
catch { }
var ext = Path.GetExtension(FileFormat);
var fis = di.GetFiles("*" + ext);
if (fis != null && fis.Length > Backups)
{
// 删除最旧的文件
var retain = fis.Length - Backups;
fis = fis.OrderBy(e => e.CreationTime).Take(retain).ToArray();
foreach (var item in fis)
{
OnWrite(LogLevel.Info, "日志文件达到上限 {0},删除 {1},大小 {2:n0}Byte", Backups, item.Name, item.Length);
try
{
item.Delete();
}
catch
{
item.MoveTo(item.FullName + ".del");
}
}
}
}
}
}
/// <summary>写入队列日志并关闭文件</summary>
protected virtual void WriteAndClose(DateTime closeTime)
{
try
{
// 处理残余
var writer = LogWriter;
if (!_Logs.IsEmpty) WriteFile();
// 连续5秒没日志,就关闭
if (writer != null && closeTime < TimerX.Now.AddHours(Setting.Current.UtcIntervalHours))
{
writer.TryDispose();
LogWriter = null;
}
}
finally
{
_writing = 0;
}
}
#endregion
#region 写日志
/// <summary>写日志</summary>
/// <param name="level"></param>
/// <param name="format"></param>
/// <param name="args"></param>
protected override void OnWrite(LogLevel level, String format, params Object?[] args)
{
// 据@夏玉龙反馈,如果不给Log目录写入权限,日志队列积压将会导致内存暴增
if (_logCount > 100) return;
var e = WriteLogEventArgs.Current.Set(level);
// 特殊处理异常对象
if (args != null && args.Length == 1 && args[0] is Exception ex && (format.IsNullOrEmpty() || format == "{0}"))
e = e.Set(null, ex);
else
e = e.Set(Format(format, args), null);
// 推入队列
_Logs.Enqueue(e.GetAndReset());
Interlocked.Increment(ref _logCount);
// 异步写日志,实时。即使这里错误,定时器那边仍然会补上
if (Interlocked.CompareExchange(ref _writing, 1, 0) == 0)
{
// 调试级别 或 致命错误 同步写日志
if (Setting.Current.LogLevel <= LogLevel.Debug || Level >= LogLevel.Error)
{
try
{
WriteFile();
}
finally
{
_writing = 0;
}
}
else
{
ThreadPool.UnsafeQueueUserWorkItem(s =>
{
try
{
WriteFile();
}
catch { }
finally
{
_writing = 0;
}
}, null);
}
}
}
#endregion
#region 辅助
/// <summary>已重载。</summary>
/// <returns></returns>
public override String ToString() => $"{GetType().Name} {LogPath}";
#endregion
}