Skip to content

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

@philsttr

Description

@philsttr

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions