Skip to content

Commit 379eabc

Browse files
authored
Merge pull request #21 from dotnet-campus/t/walterlv/thread-safe-console-logger
线程安全的控制台日志输出
2 parents 4212e79 + 573f89c commit 379eabc

File tree

9 files changed

+369
-65
lines changed

9 files changed

+369
-65
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ internal static class LoggerStartup
110110
// 设置日志级别为 Debug。
111111
.WithLevel(LogLevel.Debug)
112112
// 添加一个控制台日志写入器,这样控制台里就可以看到日志输出了。
113-
.AddWriter(new ConsoleLogger()
113+
.AddConsoleLogger(b => b
114+
.WithThreadSafe(LogWritingThreadMode.ProducerConsumer)
114115
.FilterConsoleTagsFromCommandLineArgs(args))
115116
// 如果有一些库使用了本日志框架(使用源生成器,不带依赖的那种),那么可以通过这个方法将它们的日志桥接到本日志框架中。
116117
.AddBridge(LoggerBridgeLinker.Default)

samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<OutputType>WinExe</OutputType>
4+
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
66
<DCUseGeneratedLogger>preferReference</DCUseGeneratedLogger>
77
</PropertyGroup>

samples/LoggerSample.MainApp/Program.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using dotnetCampus.Logging.Attributes;
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using dotnetCampus.Logging.Attributes;
26
using dotnetCampus.Logging.Configurations;
37
using dotnetCampus.Logging.Writers;
48

@@ -21,16 +25,27 @@ public static void Main(string[] args)
2125
{
2226
LogLevel = LogLevel.Debug,
2327
})
24-
.AddWriter(new ConsoleLogger
25-
{
26-
// Options = new ConsoleLoggerOptions
27-
// {
28-
// IncludeScopes = true,
29-
// },
30-
})
28+
.AddConsoleLogger(b => b
29+
.WithThreadSafe(LogWritingThreadMode.ProducerConsumer)
30+
.FilterConsoleTagsFromCommandLineArgs(args))
3131
.AddBridge(LoggerBridgeLinker.Default)
3232
.Build()
3333
.IntoGlobalStaticLog();
34+
35+
Run();
36+
Thread.Sleep(5000);
37+
}
38+
39+
private static void Run()
40+
{
41+
var stopwatch = Stopwatch.StartNew();
42+
Log.Debug($"[TEST] 开始 {stopwatch.ElapsedMilliseconds}ms");
43+
Parallel.For(0, 0x00004000, i =>
44+
{
45+
Thread.Sleep(0);
46+
Log.Debug($"[TEST] {DateTime.Now:HH:mm:ss}");
47+
});
48+
Log.Debug($"[TEST] 完成 {stopwatch.ElapsedMilliseconds}ms");
3449
}
3550
}
3651

src/dotnetCampus.Logger/LoggerBuilder.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using dotnetCampus.Logging.Bridges;
43
using dotnetCampus.Logging.Configurations;
5-
using dotnetCampus.Logging.Writers;
64

75
namespace dotnetCampus.Logging;
86

97
/// <summary>
108
/// 辅助创建日志记录器的构建器。
119
/// </summary>
12-
public class LoggerBuilder
10+
public sealed class LoggerBuilder
1311
{
1412
private LogOptions? _options;
1513
private readonly List<ILogger> _writers = [];

src/dotnetCampus.Logger/Writers/ConsoleLogger.cs

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
namespace dotnetCampus.Logging.Writers;
1010

11+
/// <summary>
12+
/// 在控制台输出日志的日志记录器。
13+
/// </summary>
1114
public class ConsoleLogger : ILogger
1215
{
1316
/// <summary>
@@ -16,18 +19,40 @@ public class ConsoleLogger : ILogger
1619
private int _isCursorMovementEnabled = 3;
1720

1821
private readonly RepeatLoggerDetector _repeat;
19-
private TagFilterManager? _tagFilterManager;
2022

2123
/// <summary>
22-
/// 高于或等于此级别的日志才会被记录
24+
/// 创建一个 <see cref="ConsoleLogger"/> 的新实例
2325
/// </summary>
24-
public LogLevel Level { get; set; }
26+
/// <param name="threadMode">指定控制台日志的线程安全模式。</param>
27+
/// <param name="mainArgs">Main 方法的参数。</param>
28+
public ConsoleLogger(LogWritingThreadMode threadMode = LogWritingThreadMode.NotThreadSafe, string[]? mainArgs = null)
29+
: this(threadMode.CreateCoreLogWriter(), TagFilterManager.FromCommandLineArgs(mainArgs ?? []))
30+
{
31+
}
2532

26-
public ConsoleLogger()
33+
internal ConsoleLogger(ICoreLogWriter coreWriter, TagFilterManager? tagManager)
2734
{
28-
_repeat = new(ClearAndMoveToLastLine);
35+
_repeat = new RepeatLoggerDetector(ClearAndMoveToLastLine);
36+
CoreWriter = coreWriter;
37+
TagManager = tagManager;
2938
}
3039

40+
/// <summary>
41+
/// 高于或等于此级别的日志才会被记录。
42+
/// </summary>
43+
public LogLevel Level { get; init; }
44+
45+
/// <summary>
46+
/// 最终日志写入器。
47+
/// </summary>
48+
private ICoreLogWriter CoreWriter { get; }
49+
50+
/// <summary>
51+
/// 管理控制台日志的标签过滤。
52+
/// </summary>
53+
private TagFilterManager? TagManager { get; }
54+
55+
/// <inheritdoc />
3156
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
3257
{
3358
if (logLevel < Level)
@@ -36,24 +61,37 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
3661
}
3762

3863
var message = formatter(state, exception);
39-
if (_tagFilterManager?.IsTagEnabled(message) is false)
64+
if (TagManager?.IsTagEnabled(message) is false)
4065
{
4166
return;
4267
}
4368

69+
var traceTag = TraceTag;
70+
var debugTag = DebugTag;
71+
var informationTag = InformationTag;
72+
var warningTag = WarningTag;
73+
var errorTag = ErrorTag;
74+
var criticalTag = CriticalTag;
4475
LogCore(logLevel, exception, message, m => logLevel switch
4576
{
46-
LogLevel.Trace => $"{TraceTag} {TraceText}{m}{Reset}",
47-
LogLevel.Debug => $"{DebugTag} {DebugText}{m}{Reset}",
48-
LogLevel.Information => $"{InformationTag} {InformationText}{m}{Reset}",
49-
LogLevel.Warning => $"{WarningTag} {WarningText}{m}{Reset}",
50-
LogLevel.Error => $"{ErrorTag} {ErrorText}{m}{Reset}",
51-
LogLevel.Critical => $"{CriticalTag} {CriticalText}{m}{Reset}",
77+
LogLevel.Trace => $"{traceTag} {TraceText}{m}{Reset}",
78+
LogLevel.Debug => $"{debugTag} {DebugText}{m}{Reset}",
79+
LogLevel.Information => $"{informationTag} {InformationText}{m}{Reset}",
80+
LogLevel.Warning => $"{warningTag} {WarningText}{m}{Reset}",
81+
LogLevel.Error => $"{errorTag} {ErrorText}{m}{Reset}",
82+
LogLevel.Critical => $"{criticalTag} {CriticalText}{m}{Reset}",
5283
_ => null,
5384
});
5485
}
5586

56-
private void LogCore(LogLevel logLevel, Exception? exception, string message, Func<string, string?> formatter)
87+
/// <summary>
88+
/// 记录日志。在必要的情况下会保证线程安全。
89+
/// </summary>
90+
/// <param name="logLevel"></param>
91+
/// <param name="exception"></param>
92+
/// <param name="message"></param>
93+
/// <param name="formatter"></param>
94+
private void LogCore(LogLevel logLevel, Exception? exception, string message, Func<string, string?> formatter) => CoreWriter.Do(() =>
5795
{
5896
if (_repeat.RepeatOrResetLastLog(logLevel, message, exception) is var count and > 1)
5997
{
@@ -77,9 +115,15 @@ private void LogCore(LogLevel logLevel, Exception? exception, string message, Fu
77115
{tag}{exception}
78116
""", formatter);
79117
}
80-
}
118+
});
81119

82-
private static void ConsoleMultilineMessage(string message, Func<string, string?> formatter, bool forceSingleLine = false)
120+
/// <summary>
121+
/// 记录多行日志。
122+
/// </summary>
123+
/// <param name="message"></param>
124+
/// <param name="formatter"></param>
125+
/// <param name="forceSingleLine"></param>
126+
private void ConsoleMultilineMessage(string message, Func<string, string?> formatter, bool forceSingleLine = false)
83127
{
84128
if (forceSingleLine || !message.Contains('\n'))
85129
{
@@ -96,46 +140,34 @@ private static void ConsoleMultilineMessage(string message, Func<string, string?
96140
}
97141

98142
/// <summary>
99-
/// 高于或等于此级别的日志才会被记录
143+
/// 清空当前行并移动光标到上一行
100144
/// </summary>
101-
public ConsoleLogger UseLevel(LogLevel level)
102-
{
103-
Level = level;
104-
return this;
105-
}
106-
107-
/// <summary>
108-
/// 从命令行参数中提取过滤标签。
109-
/// </summary>
110-
/// <param name="args">命令行参数。</param>
111-
public ConsoleLogger FilterConsoleTagsFromCommandLineArgs(string[] args)
112-
{
113-
_tagFilterManager = TagFilterManager.FromCommandLineArgs(args);
114-
return this;
115-
}
116-
145+
/// <param name="repeatCount">此移动光标,是因为日志已重复第几次。</param>
117146
private void ClearAndMoveToLastLine(int repeatCount)
118147
{
119-
if (_isCursorMovementEnabled > 0 && repeatCount > 2)
148+
if (_isCursorMovementEnabled <= 0 || repeatCount <= 2)
120149
{
121-
try
122-
{
123-
var desiredY = Console.CursorTop - 1;
124-
var y = Math.Clamp(desiredY, 0, Console.WindowHeight - 1);
125-
Console.SetCursorPosition(0, y);
126-
Console.Write(new string(' ', Console.WindowWidth));
127-
Console.SetCursorPosition(0, y);
128-
}
129-
catch (IOException)
130-
{
131-
// 日志记录时,如果无法移动光标,说明可能当前输出位置不在缓冲区内。
132-
// 如果多次尝试失败,则认为当前控制台缓冲区不支持光标移动,遂放弃。
133-
_isCursorMovementEnabled--;
134-
}
135-
catch (ArgumentException)
136-
{
137-
// 日志记录时,有可能已经移动到头了,就不要移动了。
138-
}
150+
// 如果光标控制不可用,或者还没有重复次数,则不尝试移动光标。
151+
return;
152+
}
153+
154+
try
155+
{
156+
var desiredY = Console.CursorTop - 1;
157+
var y = Math.Clamp(desiredY, 0, Console.WindowHeight - 1);
158+
Console.SetCursorPosition(0, y);
159+
Console.Write(new string(' ', Console.WindowWidth));
160+
Console.SetCursorPosition(0, y);
161+
}
162+
catch (IOException)
163+
{
164+
// 日志记录时,如果无法移动光标,说明可能当前输出位置不在缓冲区内。
165+
// 如果多次尝试失败,则认为当前控制台缓冲区不支持光标移动,遂放弃。
166+
_isCursorMovementEnabled--;
167+
}
168+
catch (ArgumentException)
169+
{
170+
// 日志记录时,有可能已经移动到头了,就不要移动了。
139171
}
140172
}
141173

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using dotnetCampus.Logging.Writers.Helpers;
3+
4+
namespace dotnetCampus.Logging.Writers;
5+
6+
/// <summary>
7+
/// 辅助创建控制台日志记录器的构建器。
8+
/// </summary>
9+
public sealed class ConsoleLoggerBuilder
10+
{
11+
private TagFilterManager? _tagFilterManager;
12+
private ICoreLogWriter _coreWriter = new NotThreadSafeLogWriter();
13+
14+
/// <summary>
15+
/// 高于或等于此级别的日志才会被记录。
16+
/// </summary>
17+
public LogLevel Level { get; set; }
18+
19+
/// <summary>
20+
/// 高于或等于此级别的日志才会被记录。
21+
/// </summary>
22+
public ConsoleLoggerBuilder WithLevel(LogLevel level)
23+
{
24+
Level = level;
25+
return this;
26+
}
27+
28+
/// <summary>
29+
/// 指定控制台日志的线程安全模式。
30+
/// </summary>
31+
/// <param name="threadMode">线程安全模式。</param>
32+
/// <returns>构造器模式。</returns>
33+
/// <exception cref="ArgumentOutOfRangeException">线程安全模式不支持。</exception>
34+
public ConsoleLoggerBuilder WithThreadSafe(LogWritingThreadMode threadMode)
35+
{
36+
_coreWriter = threadMode switch
37+
{
38+
LogWritingThreadMode.NotThreadSafe => new NotThreadSafeLogWriter(),
39+
LogWritingThreadMode.Lock => new LockLogWriter(),
40+
LogWritingThreadMode.ProducerConsumer => new ProducerConsumerLogWriter(),
41+
_ => throw new ArgumentOutOfRangeException(nameof(threadMode)),
42+
};
43+
return this;
44+
}
45+
46+
/// <summary>
47+
/// 从命令行参数中提取过滤标签,使得控制台日志支持过滤标签行为。
48+
/// </summary>
49+
/// <param name="args">命令行参数。</param>
50+
/// <returns>构造器模式。</returns>
51+
public ConsoleLoggerBuilder FilterConsoleTagsFromCommandLineArgs(string[] args)
52+
{
53+
_tagFilterManager = TagFilterManager.FromCommandLineArgs(args);
54+
return this;
55+
}
56+
57+
/// <summary>
58+
/// 创建控制台日志记录器。
59+
/// </summary>
60+
/// <returns>控制台日志记录器。</returns>
61+
internal ConsoleLogger Build() => new(_coreWriter, _tagFilterManager)
62+
{
63+
Level = Level,
64+
};
65+
}
66+
67+
/// <summary>
68+
/// 辅助创建控制台日志记录器。
69+
/// </summary>
70+
public static class ConsoleLoggerBuilderExtensions
71+
{
72+
/// <summary>
73+
/// 添加控制台日志记录器。
74+
/// </summary>
75+
/// <param name="builder">日志构建器。</param>
76+
/// <param name="configure">配置控制台日志记录器。</param>
77+
/// <returns>日志构建器。</returns>
78+
public static LoggerBuilder AddConsoleLogger(this LoggerBuilder builder, Action<ConsoleLoggerBuilder> configure)
79+
{
80+
var consoleLoggerBuilder = new ConsoleLoggerBuilder();
81+
configure(consoleLoggerBuilder);
82+
return builder.AddWriter(consoleLoggerBuilder.Build());
83+
}
84+
}

0 commit comments

Comments
 (0)