logfellow / logstash-logback-encoder

Logback JSON encoder and appenders
Apache License 2.0
2.44k stars 407 forks source link

jsonFactoryDecorator appears to be ignored by LoggingEventCompositeJsonEncoder #993

Closed joca-bt closed 11 months ago

joca-bt commented 11 months ago

I am trying to use jsonFactoryDecorator with LoggingEventCompositeJsonEncoder through XML configuration but the decorator appears to be ignored. I defined a custom JsonFactoryDecorator that makes null fields not be serialized. However, it doesn't seem to be running as fields with null are still serialized. When I define all my configuration through code with no xml, it works fine.

XML + Java:

elk-logger.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="name" value="elk"/>
    <property name="file" value="elk.log"/>

    <logger name="${name}" additivity="false" level="all">
        <appender-ref ref="appender"/>
    </logger>

    <appender name="appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${file}</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${file}.%d.%i</fileNamePattern>
            <maxFileSize>100 MB</maxFileSize>
            <maxHistory>1</maxHistory>
        </rollingPolicy>

        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <jsonFactoryDecorator class="...ElkLogger.ObjectMapperCustomizer"/>

            <providers>
                <arguments/>
            </providers>
        </encoder>
    </appender>
</configuration>
ElkLogger.java

public class ElkLogger {
    private final Logger logger;

    public ElkLogger() {
        this.logger = getLogger();
    }

    public void log(Map<String, ?> map) {
        logger.info(null, StructuredArguments.entries(map));
    }

    private Logger getLogger() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(context);

        try (var stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("elk-logger.xml")) {
            configurator.doConfigure(stream);
        } catch (Exception exception) {
            throw new InitializationException(exception);
        }

        return context.getLogger("elk");
    }

    public static class ObjectMapperCustomizer implements JsonFactoryDecorator {
        @Override
        public JsonFactory decorate(JsonFactory factory) {
            ObjectMapper objectMapper = (ObjectMapper) factory.getCodec();
            objectMapper.setSerializationInclusion(Include.NON_NULL);
            return factory;
        }
    }
}

Only Java:

ElkLogger.java

public class ElkLogger {
    private final Logger logger;

    private ElkLogger(Builder builder) {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        this.logger = context.getLogger(builder.name);
        logger.addAppender(newAppender(context, builder.file));
        logger.setAdditive(false);
        logger.setLevel(ALL);
    }

    public void log(Map<String, ?> map) {
        logger.info(null, StructuredArguments.entries(map));
    }

    private RollingFileAppender<ILoggingEvent> newAppender(Context context, Path file) {
        JsonProviders<ILoggingEvent> providers = new JsonProviders<>();
        providers.addProvider(new ArgumentsJsonProvider());
        providers.setContext(context);

        LoggingEventCompositeJsonEncoder encoder = new LoggingEventCompositeJsonEncoder();
        encoder.setProviders(providers);
        encoder.setContext(context);

        ObjectMapperCustomizer factoryDecorator = new ObjectMapperCustomizer();
        encoder.setJsonFactoryDecorator(factoryDecorator);

        SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = new SizeAndTimeBasedRollingPolicy<>();
        policy.setFileNamePattern("%s.%%d.%%i".formatted(file.toString()));
        policy.setMaxFileSize(new FileSize(100 * MB_COEFFICIENT));
        policy.setMaxHistory(1);
        policy.setContext(context);

        RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
        appender.setFile(file.toString());
        appender.setRollingPolicy(policy);
        appender.setEncoder(encoder);
        appender.setContext(context);

        policy.setParent(appender);

        providers.start();
        encoder.start();
        policy.start();
        appender.start();

        return appender;
    }

    public static class Builder {
        private Path file;
        private String name;

        public ElkLogger build() {
            Objects.requireNonNull(file);
            Objects.requireNonNull(name);
            return new ElkLogger(this);
        }

        public Builder file(Path file) {
            this.file = file;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }
    }

    private static class ObjectMapperCustomizer implements JsonFactoryDecorator {
        @Override
        public JsonFactory decorate(JsonFactory factory) {
            ObjectMapper objectMapper = (ObjectMapper) factory.getCodec();
            objectMapper.setSerializationInclusion(Include.NON_NULL);
            return factory;
        }
    }
}

Test:

record Entry(String key, String anotherKey) {};

ElkLogger elk = new ElkLogger.Builder()
    .name("elk")
    .file(Path.of("elk.log"))
    .build();

elk.log(Map.of("@input", new Entry("value", "anotherValue")));
elk.log(Map.of("@input", new Entry("value", null)));
joca-bt commented 11 months ago

The class must be the internal, so for inner classes using $ instead of .. Closing.