diff --git a/examples/gradle/build.gradle b/examples/gradle/build.gradle
index 3713f92..f7266af 100644
--- a/examples/gradle/build.gradle
+++ b/examples/gradle/build.gradle
@@ -7,7 +7,7 @@ repositories {
}
dependencies {
- implementation 'com.logtail:logback-logtail:0.3.5'
+ implementation 'com.logtail:logback-logtail:0.3.6'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'ch.qos.logback:logback-core:1.2.11'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5'
diff --git a/examples/maven/pom.xml b/examples/maven/pom.xml
index 0a8ee59..48bfaef 100644
--- a/examples/maven/pom.xml
+++ b/examples/maven/pom.xml
@@ -26,7 +26,7 @@
com.logtaillogback-logtail
- 0.3.5
+ 0.3.6ch.qos.logback
diff --git a/pom.xml b/pom.xml
index dffb4e1..e1bda28 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.logtaillogback-logtailjar
- 0.3.5
+ 0.3.6${project.groupId}:${project.artifactId}Logback Java appender for sending logs to BetterStack.com
diff --git a/src/main/java/com/logtail/logback/BestEffortSerialization.java b/src/main/java/com/logtail/logback/BestEffortSerialization.java
new file mode 100644
index 0000000..820ed9c
--- /dev/null
+++ b/src/main/java/com/logtail/logback/BestEffortSerialization.java
@@ -0,0 +1,204 @@
+package com.logtail.logback;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import com.fasterxml.jackson.databind.ser.ResolvableSerializer;
+import com.fasterxml.jackson.databind.type.ArrayType;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.fasterxml.jackson.databind.util.NameTransformer;
+import com.fasterxml.jackson.databind.util.TokenBuffer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+
+/**
+ * Jackson module making serialization of arbitrary logged values best-effort instead of all-or-nothing,
+ * in a single serialization pass. Values serialize into the same JSON as without this module, except that
+ * data Jackson cannot handle is omitted and replaced with a marker string:
+ *
+ *
a reference back to an object the serializer is currently inside (a cyclic object graph, e.g. a
+ * pooled JDBC connection) is replaced with {@code ""} while the rest of the
+ * object stays intact,
+ *
a value wrapped with {@link #guard(Object)} whose serialization fails for any other reason
+ * (typically a getter that throws) is replaced as a whole with a marker naming its class.
+ *
+ */
+public class BestEffortSerialization extends SimpleModule {
+
+ static final String CIRCULAR_REFERENCE_MARKER = "";
+
+ private static final Object ANCESTORS = new Object();
+
+ public BestEffortSerialization() {
+ addSerializer(Guarded.class, new GuardedSerializer());
+ setSerializerModifier(new BeanSerializerModifier() {
+ @Override
+ public JsonSerializer> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer> serializer) {
+ return new CycleGuard(serializer);
+ }
+
+ @Override
+ public JsonSerializer> modifyArraySerializer(SerializationConfig config, ArrayType valueType, BeanDescription beanDesc, JsonSerializer> serializer) {
+ return new CycleGuard(serializer);
+ }
+
+ @Override
+ public JsonSerializer> modifyCollectionSerializer(SerializationConfig config, CollectionType valueType, BeanDescription beanDesc, JsonSerializer> serializer) {
+ return new CycleGuard(serializer);
+ }
+
+ @Override
+ public JsonSerializer> modifyMapSerializer(SerializationConfig config, MapType valueType, BeanDescription beanDesc, JsonSerializer> serializer) {
+ return new CycleGuard(serializer);
+ }
+ });
+ }
+
+ /**
+ * Wraps a value of an unknown type so that when its serialization fails, it is replaced with a marker
+ * string instead of failing the serialization of everything around it.
+ */
+ public static Object guard(Object value) {
+ if (value == null || isSafeScalar(value)) {
+ return value;
+ }
+ return new Guarded(value);
+ }
+
+ // Only exact final JDK types whose serializers cannot fail may skip the guard - an instanceof check
+ // is not enough, e.g. a Number subclass can still throw from the toString Jackson serializes it with
+ private static boolean isSafeScalar(Object value) {
+ Class> type = value.getClass();
+ return type == String.class || type == Boolean.class || type == Character.class
+ || type == Integer.class || type == Long.class || type == Double.class
+ || type == Float.class || type == Short.class || type == Byte.class;
+ }
+
+ static final class Guarded {
+ final Object value;
+
+ Guarded(Object value) {
+ this.value = value;
+ }
+ }
+
+ static final class GuardedSerializer extends JsonSerializer {
+ @Override
+ public void serialize(Guarded guarded, JsonGenerator gen, SerializerProvider provider) throws IOException {
+ // Serialized into a buffer first, so a failure halfway through leaves no partial output behind
+ TokenBuffer buffer = new TokenBuffer(gen.getCodec(), false);
+ try {
+ provider.defaultSerializeValue(guarded.value, buffer);
+ } catch (Exception | StackOverflowError e) {
+ gen.writeString("");
+ return;
+ }
+ buffer.serialize(gen);
+ }
+
+ @Override
+ public void serializeWithType(Guarded guarded, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
+ // The wrapper is invisible in the output, so there is no type id to write for it - the
+ // wrapped value's own serializer emits its type info inside the buffer
+ serialize(guarded, gen, provider);
+ }
+ }
+
+ static final class CycleGuard extends JsonSerializer