logstash-logback-encoder icon indicating copy to clipboard operation
logstash-logback-encoder copied to clipboard

Provide detection and handling of types determined "unsafe" to serialize

Open philsttr opened this issue 6 years ago • 0 comments

As discussed here and here, there is a need for logstash-logback-encoder to provide a convenient way to filter/handle types that cannot be serialized by Jackson (e.g. recursion), or that do not serialize as desired by Jackson (e.g. nesting too deep), when logged as

  • a StructuredArgument
  • a non-structured argument (with includeNonStructuredArguments=true)

One of several unsafe detection strategies could be implemented:

  • An allowlist of known "safe" types
  • A denylist of known "unsafe" types
  • Try serializing first, then add to denylist on failure
  • perhaps other strategies?

In addition, one of several unsafe handler strategies could be implemented

  • Don't serialize the type (and optionally warn?)
  • Serialize the toString() of the object instead

Currently, logstash-logback-encoder provides primitives that allow applications to implement any one of these strategies. Specifically,

  • a JsonProvider could be implemented (similar to the ArgumentsJsonProvider) to check argument types before serializing
    • This could handle the "try serializing first, then add to denylist" strategy, but has the downside that it cannot detect nested "unsafe" types
  • a BeanSerializerModifier could be added to Jackson's SerializerFactory
    • This could not handle the "try first, then add to denylist strategy", but has the advantage that it could detect nested "unsafe" types

For example, a simple allowlist detection strategy that handles unsafe types by serializing their toString() value could be implemented with a BeanSerializerModifier like this:

import java.util.HashSet;
import java.util.Set;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

public class AllowedTypeFilterDecorator implements JsonFactoryDecorator {
    
    /**
     * Set of class names deemed safe to serialize.
     * Any objects of this type (or subtype) will be serialized by the {@link JsonSerializer} selected by Jackson.  
     * Any objects NOT of this type (or subtype) will be serialized as a string by invoking {@link Object#toString()} on the object.
     */
    private Set<String> allowed = new HashSet<>();

    @Override
    public MappingJsonFactory decorate(MappingJsonFactory factory) {
        factory.getCodec().setSerializerFactory(factory.getCodec().getSerializerFactory().withSerializerModifier(new BeanSerializerModifier() {
            @Override
            public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
                for (String allowedClassName : allowed) {
                    try {
                        Class<?> allowedClass = Class.forName(allowedClassName, true, desc.getBeanClass().getClassLoader());
                        if (allowedClass.isAssignableFrom(desc.getBeanClass())) {
                            return serializer;
                        }
                    } catch (ClassNotFoundException e) {
                        // Allowed class is unknown to the bean class' classloader
                        // Therefore, the bean class cannot possibly be an instance of the allowed class
                        // Therefore, continue
                    }
                }
                return ToStringSerializer.instance;
            }
        }));
        return factory;
    }
    
    public void addAllowed(String allowedClassName) throws ClassNotFoundException {
        this.allowed.add(allowedClassName);
    }

}

and configured like this

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <includeNonStructuredArguments>true</includeNonStructuredArguments>
    <jsonFactoryDecorator class="net.logstash.logback.decorate.AllowedTypeFilterDecorator">
        <allowed>foo.Baz</allowed>
        <allowed>foo.Bar</allowed>
    </jsonFactoryDecorator>
</encoder>

Need to decide:

  • Which unsafe detection strategies to implement?
  • Which unsafe handler strategies to implement?
  • Should logstash-logback-encoder come with a default set of "safe" types configured?

philsttr avatar Sep 14 '18 23:09 philsttr