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

如何优雅实现Logback MDC数值类型自动识别,避免Elasticsearch类型映射错误

我们的应用使用SLF4J的MDC和Logback的JSON编码器将日志行输出为JSON格式,这些日志行随后会被日志采集管道处理,作为文档写入Elasticsearch。我们的logback.xml配置如下:

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <provider class="net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider"/>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

MDC的使用方式如下:

MDC.put("stringVal", "val");
MDC.put("decimalVal", "1.23");
MDC.put("longVal", "123498559348792879267942876");
logger.info("foo");

问题在于MDC的接口定义是void put(String key, String val),所有值都必须是字符串或者可自动装箱为字符串,这会生成如下格式的日志:

{"@timestamp":"2021-08-23T16:32:04.231+01:00","@version":1,"message":"foo","logger_name":"MDCTest","thread_name":"main","level":"INFO","level_value":20000,"stringVal":"val","decimalVal":"1.23","longVal":"123498559348792879267942876"}

字符串类型的decimalVal和longVal会被Elasticsearch的自动类型映射识别为字符串类型,导致我们无法对这些字段执行数值运算。
当前这些数值大多来自用日志传递指标的不合理使用,不过这个混淆问题正在其他环节处理。
我不希望开发者新增日志时还要更新Elasticsearch索引模板或者编写日志采集管道的配置,因此一直在寻找自动实现类型转换的方法。我已经实现了一个方案,通过替换Logback的Encoder和Formatter内部逻辑,替换负责将MDC序列化为JSON的MdcJsonProvider类来实现需求,但该方案既脆弱性能也不佳。
有没有更优雅的实现方法,或者其他可以达成相同效果的路径?我已经调研过ch.qos.logback.contrib.jackson.JacksonJsonFormatter,但它仍需要在logback配置文件中罗列数值类型的MDC属性,这是我想要避免的。

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.collect.Maps;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.spi.ContextAware;
import net.logstash.logback.LogstashFormatter;
import net.logstash.logback.composite.CompositeJsonFormatter;
import net.logstash.logback.composite.JsonProvider;
import net.logstash.logback.composite.JsonWritingUtils;
import net.logstash.logback.composite.loggingevent.MdcJsonProvider;
import net.logstash.logback.encoder.LogstashEncoder;

/**
 * 该类是一个临时hack方案
 *
 * 它的存在是因为Logback的MDC接口仅支持String到String的映射,且该规则无法修改。因此我们只能修改日志器本身,检查字符串值是否为数值类型,若是则按数值类型输出日志。
 *
 * 由于我们直接将日志文档写入Elasticsearch,因此需要注意:如果同一个键对应的类型频繁变化,或者偶尔写入全数值格式的字符串,可能会出现映射冲突问题。
 *
 * 该类实现了协同工作的Encoder、Formatter和JsonProvider。编码器指定了格式化器,格式化器会将原本负责序列化MDC值的MdcJsonProvider替换为自定义实现,尝试按数值类型输出MDC值。
 *
 * 使用该类会有额外性能开销:会产生更多短生命周期对象,因此不适用于高频日志场景。
 */
public class JsonMdcTypePreservingEncoder extends LogstashEncoder {
    protected CompositeJsonFormatter<ILoggingEvent> createFormatter() {
        return new JsonMdcTypePreservingFormatter(this);
    }

    protected JsonMdcTypePreservingFormatter getFormatter() {
        return (JsonMdcTypePreservingFormatter) super.getFormatter();
    }

    /**
     * 该类的作用是移除默认负责将MDC序列化为JSON的MdcJsonProvider,替换为自定义实现。
     */
    public static class JsonMdcTypePreservingFormatter extends LogstashFormatter {
        public JsonMdcTypePreservingFormatter(ContextAware declaredOrigin) {
            super(declaredOrigin);

            Optional<JsonProvider<ILoggingEvent>> oldProvider = getProviders().getProviders()
                    .stream()
                    .filter(o -> o.getClass() == MdcJsonProvider.class)
                    .findFirst();

            if (oldProvider.isPresent()) {
                getProviders().removeProvider(oldProvider.get());
                getProviders().addProvider(new TypePreservingMdcJsonProvider());
            }
        }

        /**
         * 该类复制了MdcJsonProvider.writeTo的实现,仅做了一处小修改:不再直接序列化MDC的Map<String, String>,而是生成一个修改后的Map<String, Object>,其中可能同时包含BigDecimal、BigInteger类型和字符串类型,再序列化该Map。
         *
         * 该方法中唯一新增的代码是getStringObjectMap的调用。
         */
        public static class TypePreservingMdcJsonProvider extends MdcJsonProvider {
            private Map<String, Object> convertedProperties = Maps.newHashMap();

            @Override
            public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
                Map<String, String> mdcProperties = event.getMDCPropertyMap();

                if (mdcProperties != null && !mdcProperties.isEmpty()) {
                    if (getFieldName() != null) {
                        generator.writeObjectFieldStart(getFieldName());
                    }

                    if (!getIncludeMdcKeyNames().isEmpty()) {
                        mdcProperties = new HashMap(mdcProperties);
                        ((Map) mdcProperties).keySet().retainAll(getIncludeMdcKeyNames());
                    }

                    if (!getExcludeMdcKeyNames().isEmpty()) {
                        mdcProperties = new HashMap(mdcProperties);
                        ((Map) mdcProperties).keySet().removeAll(getExcludeMdcKeyNames());
                    }

                    Map<String, Object> convertedProperties = getStringObjectMap(mdcProperties);

                    JsonWritingUtils.writeMapEntries(generator, convertedProperties);
                    if (getFieldName() != null) {
                        generator.writeEndObject();
                    }
                }
            }

            private Map<String, Object> getStringObjectMap(Map<String, String> mdcProperties) {
                convertedProperties.clear();

                for (String key : mdcProperties.keySet()) {
                    String value = mdcProperties.get(key);

                    // 如果大多数MDC值都不是数值,该逻辑会避免不必要的解析步骤,但会为每个值增加逐字符检查的开销。
                    try {
                        BigInteger parsed = new BigInteger(value);
                        convertedProperties.put(key, parsed);
                    } catch (NumberFormatException e) {
                        try {
                            BigDecimal parsed = new BigDecimal(value);
                            convertedProperties.put(key, parsed);
                        } catch (NumberFormatException f) {
                            // No-op
                        }
                    }

                    if (!convertedProperties.containsKey(key)) {
                        convertedProperties.put(key, value);
                    }
                }
                return convertedProperties;
            }
        }
    }
}

感谢各位提供的建议!


内容的提问来源于stack exchange,提问作者Martin Foot

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 23:18:02