如何修改Serilog LogEvent的消息模板?自定义Enricher实现困境
问题描述
我在代码中大量使用日志作用域(scopes)和结构化日志(structured logs),所有日志会发送至Logstash并可在Kibana中查看。但遇到Logstash索引问题:同一属性名的日志值类型必须一致。为缓解该问题,我计划给结构化日志和作用域的属性名添加后缀,但手动操作繁琐易遗漏,因此尝试编写自定义Enricher自动添加后缀。
我希望自动将以下代码:
logger.LogInformation("Some log message with a {NamedProperty}", 42);
转换为:
logger.LogInformation("Some log message with a {NamedProperty.MyCustomSuffix}", 42);
当前自定义Enricher可查找日志中的所有命名属性并修改属性字典中的名称,但无法修改消息模板(MessageTemplate),因它是不可变的。代码如下:
public class NamedPropertiesTransformer : ILogEventEnricher { readonly string _suffix; public NamedPropertiesTransformer(string suffix) => _suffix = suffix; public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var propertyTokenNames = logEvent.MessageTemplate.Tokens .Where(_ => _ is PropertyToken {IsPositional: false}) .Cast<PropertyToken>() .Select(_ => _.PropertyName) .ToHashSet(); var properties = logEvent.Properties.Where(_ => propertyTokenNames.Contains(_.Key)); foreach (var (name, value) in properties) { logEvent.RemovePropertyIfPresent(name); logEvent.AddOrUpdateProperty(new($"{name}.{_suffix}", value)); } } }
请问是否有办法实现修改消息模板的需求?
解决方案
Serilog的MessageTemplate是不可变类型,无法直接修改,但可以通过创建并替换整个LogEvent实例实现需求,更可靠的方式是使用自定义Sink而非Enricher(因为Enricher设计初衷是增强属性,而非修改事件核心结构),具体实现如下:
核心思路
- 解析原消息模板的Token,替换命名属性的名称为带后缀的版本
- 基于新的消息模板创建新LogEvent,复制原事件的时间戳、日志级别、异常、作用域等所有元数据
- 同步修改属性字典中的键,确保占位符和属性名完全匹配
- 通过自定义Sink输出修改后的LogEvent
自定义Sink实现代码
public class SuffixAddingSink : ILogEventSink { private readonly ILogEventSink _innerSink; private readonly string _suffix; public SuffixAddingSink(ILogEventSink innerSink, string suffix) { _innerSink = innerSink; _suffix = suffix; } public void Emit(LogEvent logEvent) { // 1. 处理消息模板:替换命名属性Token var newTokens = logEvent.MessageTemplate.Tokens.Select(token => { if (token is PropertyToken propToken && !propToken.IsPositional) { return new PropertyToken( propertyName: $"{propToken.PropertyName}.{_suffix}", format: propToken.Format, alignment: propToken.Alignment, isPositional: false); } return token; }).ToList(); var newMessageTemplate = new MessageTemplate(newTokens); // 2. 处理属性字典:重命名对应属性 var newProperties = new Dictionary<string, LogEventProperty>(); foreach (var prop in logEvent.Properties) { if (logEvent.MessageTemplate.Tokens .OfType<PropertyToken>() .Any(t => !t.IsPositional && t.PropertyName == prop.Key)) { newProperties.Add($"{prop.Key}.{_suffix}", prop.Value); } else { newProperties.Add(prop.Key, prop.Value); } } // 3. 创建新的LogEvent实例,复制原事件的所有元数据 var newLogEvent = new LogEvent( timestamp: logEvent.Timestamp, level: logEvent.Level, exception: logEvent.Exception, messageTemplate: newMessageTemplate, properties: newProperties.Values); // 4. 复制原事件的日志作用域 foreach (var scope in logEvent.Scopes) { newLogEvent.AddOrUpdateProperty(scope); } // 5. 传递给内部Sink(比如ElasticsearchSink)输出 _innerSink.Emit(newLogEvent); } }
配置方式
在Serilog配置中,将自定义Sink包装原有输出Sink(比如ElasticsearchSink):
Log.Logger = new LoggerConfiguration() .WriteTo.Sink(new SuffixAddingSink( innerSink: new ElasticsearchSink(new ElasticsearchSinkOptions(new Uri("http://your-logstash-url:9200")) { // ElasticsearchSink的配置参数 }, ".MyCustomSuffix")) .CreateLogger();
关键说明
- 避免用Enricher修改MessageTemplate:标准Enricher无法直接替换LogEvent实例,强行通过反射修改不可变字段可能引发未知问题,自定义Sink是更合规的方案。
- 作用域处理:如果需要给作用域中的属性也添加后缀,可以在遍历
logEvent.Scopes时,对作用域内的属性执行同样的重命名逻辑。 - 类型一致性:修改后消息模板的占位符与属性字典的键完全匹配,能彻底解决Logstash的索引字段类型冲突问题。
内容的提问来源于stack exchange,提问作者Aleksey Usatov
相关产品推荐
相关产品推荐

