日志系统是现代软件开发中不可或缺的基础设施,它就像应用程序的"黑匣子",记录着系统运行时的每一个关键动作。在.NET生态中,日志记录已经从早期的简单文本输出,演变为支持结构化日志、分布式追踪的完整可观测性体系。
我经历过从System.Diagnostics.Trace到NLog/Serilog的完整技术演进周期。早期项目常在代码中硬编码Console.WriteLine,导致生产环境故障时束手无策。现在的主流方案如Microsoft.Extensions.Logging,通过统一的抽象接口整合了日志记录、事件ID、作用域等现代特性,配合Application Insights等工具可实现全链路监控。
当前.NET日志框架呈现三层架构:
一个健壮的日志接口需要平衡灵活性与性能。参考Microsoft.Extensions.Logging的设计,我们定义以下核心接口:
csharp复制public interface ILogger
{
void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter);
bool IsEnabled(LogLevel logLevel);
IDisposable BeginScope<TState>(TState state);
}
关键设计考量:
日志级别不仅是简单的枚举,更需要考虑运行时动态调整。我们实现分级过滤策略:
csharp复制public class LogLevelFilter
{
private volatile LogLevel _minLevel;
public void SetMinLevel(LogLevel level) => _minLevel = level;
public bool IsEnabled(LogLevel level) => level >= _minLevel;
}
重要提示:使用volatile保证多线程环境下的即时可见性,避免使用锁带来的性能损耗。
实现类似Serilog的LogContext需要解决两个技术难点:
采用AsyncLocal
csharp复制public class LogContext
{
private static readonly AsyncLocal<Dictionary<string, object>> _state = new();
public static IDisposable PushProperty(string key, object value)
{
var state = _state.Value ??= new Dictionary<string, object>();
state[key] = value;
return new Disposable(() => state.Remove(key));
}
}
直接磁盘I/O会严重拖慢应用程序,我们采用双缓冲队列:
csharp复制public class LogBuffer
{
private readonly BlockingCollection<LogEntry> _writeQueue = new();
private readonly List<LogEntry> _backBuffer = new(1024);
public void Enqueue(LogEntry entry)
{
if (!_writeQueue.TryAdd(entry, 0))
{
lock (_backBuffer)
{
_backBuffer.Add(entry);
}
}
}
}
性能优化点:
| 策略类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 同步写入 | 数据安全 | 性能差 | 金融交易系统 |
| 异步缓冲 | 高性能 | 宕机丢数据 | 高吞吐Web应用 |
| 内存映射文件 | 折中方案 | 实现复杂 | 桌面应用程序 |
实测数据(每秒日志条目):
实现类似Serilog的"User {UserId} logged in"模板语法:
csharp复制public class MessageTemplate
{
public static (string text, List<string> tokens) Parse(string template)
{
var tokens = new List<string>();
var regex = new Regex(@"\{(\w+)\}");
var text = regex.Replace(template, match =>
{
tokens.Add(match.Groups[1].Value);
return $"{{{tokens.Count - 1}}}";
});
return (text, tokens);
}
}
避免直接使用JSON.NET的全量序列化:
csharp复制public class LogProperty
{
public object Value { get; }
public string ToString()
{
if (Value is IFormattable fmt)
return fmt.ToString(null, CultureInfo.InvariantCulture);
return Value?.ToString() ?? "null";
}
}
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 日志文件不更新 | 文件被独占锁定 | 使用FileShare.ReadWrite模式打开 |
| 内存持续增长 | 日志堆积速度>写入速度 | 增加写入线程或实施背压 |
| 跨机器日志乱序 | 时钟不同步 | 部署NTP服务并添加机器标识 |
某次生产事故中,日志系统导致整个应用冻结。根本原因是:
解决方案:
避免频繁创建LogEntry对象:
csharp复制public class LogEntryPool
{
private readonly ConcurrentBag<LogEntry> _pool = new();
public LogEntry Rent()
{
return _pool.TryTake(out var entry) ? entry : new LogEntry();
}
public void Return(LogEntry entry)
{
entry.Reset();
_pool.Add(entry);
}
}
通过ref struct避免堆分配:
csharp复制public ref struct LogEntryBuilder
{
private Span<char> _buffer;
public void Append(ReadOnlySpan<char> text)
{
text.CopyTo(_buffer);
_buffer = _buffer.Slice(text.Length);
}
}
实测性能提升:
csharp复制public class MyLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new MyLogger(categoryName);
}
public void Dispose() { }
}
实现日志转指标的关键代码:
csharp复制public class MetricsLogger
{
private readonly Counter<int> _errorCounter;
public void LogError()
{
_errorCounter.Add(1);
}
}
在实现自定义日志框架的过程中,最大的收获是对.NET Core底层架构设计的理解。比如AsyncLocal的线程流动特性、Span