在.NET生态中,日志记录是系统可观测性的基石。现代日志框架通常由四大核心组件构成:
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 FileLoggerProvider : ILoggerProvider
{
private readonly StreamWriter _writer;
public FileLoggerProvider(string filePath)
{
_writer = new StreamWriter(filePath, append: true);
}
public ILogger CreateLogger(string categoryName) => new FileLogger(_writer);
public void Dispose() => _writer.Dispose();
}
json复制{
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Warning",
"Microsoft": "Error"
}
}
}
csharp复制string FormatLog(LogLevel level, string message, Exception ex)
{
return $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}" +
(ex != null ? $"\n{ex}" : "");
}
关键设计原则:通过接口抽象各组件,使日志记录、处理和输出解耦,这是所有现代日志框架的共通架构思想。
我们从零实现一个支持多输出的日志框架:
csharp复制public interface ICustomLogger
{
void Log(LogLevel level, string message, Exception ex = null);
void AddProvider(ILogProvider provider);
}
public interface ILogProvider
{
void WriteLog(LogEntry entry);
}
csharp复制public class MemoryLogProvider : ILogProvider
{
private readonly ConcurrentQueue<LogEntry> _logs = new();
public void WriteLog(LogEntry entry)
{
_logs.Enqueue(entry);
if (_logs.Count > 1000) _logs.TryDequeue(out _);
}
public IEnumerable<LogEntry> GetLogs() => _logs.ToArray();
}
为避免日志写入阻塞主线程,添加异步处理机制:
csharp复制public class AsyncLogProcessor : IDisposable
{
private readonly BlockingCollection<LogEntry> _queue = new();
private readonly CancellationTokenSource _cts = new();
public AsyncLogProcessor()
{
Task.Run(() => ProcessLogs(_cts.Token));
}
private void ProcessLogs(CancellationToken token)
{
foreach (var entry in _queue.GetConsumingEnumerable(token))
{
foreach (var provider in entry.Providers)
{
try { provider.WriteLog(entry); }
catch { /* 记录失败但不影响主流程 */ }
}
}
}
public void AddLog(LogEntry entry) => _queue.Add(entry);
public void Dispose()
{
_cts.Cancel();
_queue.CompleteAdding();
}
}
模仿Serilog的LogContext,实现上下文信息传递:
csharp复制public class LogScope : IDisposable
{
private static readonly AsyncLocal<Stack<LogScope>> _currentScope = new();
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
public LogScope()
{
(_currentScope.Value ??= new Stack<LogScope>()).Push(this);
}
public static LogScope Current => _currentScope.Value?.Peek();
public void Dispose() => _currentScope.Value?.Pop();
}
传统字符串拼接方式存在性能瓶颈:
csharp复制// 不推荐:频繁字符串分配
logger.LogInfo($"User {userId} accessed {resource} at {DateTime.Now}");
改用结构化日志模板:
csharp复制// 推荐:预编译模板,运行时只传参数
logger.LogInfo("User {UserId} accessed {Resource} at {Timestamp}",
userId, resource, DateTime.Now);
实现原理:
csharp复制public class StructuredLogTemplate
{
private readonly string _template;
private readonly string[] _parameterNames;
public StructuredLogTemplate(string messageTemplate)
{
// 解析类似 {UserId} 的占位符
_parameterNames = ParseTemplate(messageTemplate, out _template);
}
public string Format(object[] parameters)
{
// 使用StringBuilder高效拼接
var sb = new StringBuilder(_template);
for (int i = 0; i < _parameterNames.Length; i++)
{
sb.Replace("{" + _parameterNames[i] + "}", parameters[i]?.ToString());
}
return sb.ToString();
}
}
高频创建LogEntry对象会导致GC压力,采用对象池优化:
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);
}
}
// 使用示例
var entry = _pool.Rent();
try
{
entry.Level = LogLevel.Info;
entry.Message = "Processing completed";
_processor.AddLog(entry);
}
finally
{
_pool.Return(entry);
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 日志丢失 | 异步队列满被丢弃 | 增大队列容量或改用阻塞写入 |
| 性能下降 | 同步写文件/网络 | 检查是否配置了异步处理器 |
| 格式错误 | 多线程共享格式化器 | 确保格式化器线程安全或实例隔离 |
| 内存泄漏 | 未释放日志作用域 | 确保所有LogScope使用using块 |
csharp复制using var activity = new Activity("ProcessOrder");
activity.AddTag("OrderId", orderId);
logger.LogInfo("Starting order processing");
csharp复制// 运行时动态提升日志级别
DiagnosticListener.AllListeners.Subscribe(new DynamicLogLevelObserver());
csharp复制if (_sampler.ShouldSample()) // 基于随机或条件采样
{
logger.LogDebug("Detailed trace data: {Data}", expensiveData);
}
在不修改核心代码情况下添加新功能:
csharp复制public class BufferedLoggerDecorator : ICustomLogger
{
private readonly ICustomLogger _inner;
private readonly List<LogEntry> _buffer = new();
public BufferedLoggerDecorator(ICustomLogger inner) => _inner = inner;
public void Log(LogLevel level, string message, Exception ex = null)
{
_buffer.Add(new LogEntry(level, message, ex));
if (_buffer.Count >= 100) Flush();
}
public void Flush()
{
foreach (var entry in _buffer)
{
_inner.Log(entry.Level, entry.Message, entry.Exception);
}
_buffer.Clear();
}
}
根据日志内容动态选择输出目标:
csharp复制public class SmartRouter : ILogProvider
{
private readonly Dictionary<Func<LogEntry, bool>, ILogProvider> _routes = new();
public void AddRoute(Func<LogEntry, bool> predicate, ILogProvider provider)
{
_routes[predicate] = provider;
}
public void WriteLog(LogEntry entry)
{
foreach (var route in _routes)
{
if (route.Key(entry))
{
route.Value.WriteLog(entry);
break;
}
}
}
}
日志分级策略:
敏感信息过滤:
csharp复制public class SensitiveDataFilter : ILogProcessor
{
private readonly string[] _patterns = { "password", "creditcard" };
public LogEntry Process(LogEntry entry)
{
foreach (var pattern in _patterns)
{
entry.Message = Regex.Replace(entry.Message, $"{pattern}=[^&]+", $"{pattern}=***");
}
return entry;
}
}
csharp复制public class RollingFileProvider : ILogProvider
{
private readonly string _basePath;
private DateTime _currentDate;
private StreamWriter _writer;
public RollingFileProvider(string basePath)
{
_basePath = basePath;
RollFile();
}
private void RollFile()
{
_writer?.Dispose();
_currentDate = DateTime.Today;
_writer = new StreamWriter($"{_basePath}-{_currentDate:yyyyMMdd}.log", append: true);
}
public void WriteLog(LogEntry entry)
{
if (DateTime.Today != _currentDate) RollFile();
_writer.WriteLine(entry.ToString());
}
}
在实现自定义日志框架时,建议先明确业务场景需求。对于中小型应用,直接使用成熟的Serilog或NLog可能是更经济的选择。但当需要深度定制日志处理流水线或学习框架设计原理时,从零开始构建无疑是最佳的学习路径。