You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改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设计初衷是增强属性,而非修改事件核心结构),具体实现如下:

核心思路

  1. 解析原消息模板的Token,替换命名属性的名称为带后缀的版本
  2. 基于新的消息模板创建新LogEvent,复制原事件的时间戳、日志级别、异常、作用域等所有元数据
  3. 同步修改属性字典中的键,确保占位符和属性名完全匹配
  4. 通过自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 21:10:28