Thursday, October 21, 2021

Spring boot upgrade from 2.2.x to 2.5.x - Spring Cloud Sleuth Zipkin - log message format change . . .

I recently upgraded a Spring Boot micro-service application from Spring Boot 2.2.11 to 2.5.3 and Java OpenJDK 15 to Java Amazon Corretto 16.

Upgrading Java was smooth. Some major and breaking changes that I came across from Spring Boot side include:
  • Profile changes - documented well
  • Dependency changes - Obvious
  • Spring Cloud Sleuth Zipkin - Undocumented glitch
This post is on the Spring Cloud Sleuth Zipkin - undocumented removal of exportable property.

Environment: Java 16, Spring Boot 2.5.3 on macOS Catalina 10.15.7

We have application logs collected, ingested and indexed in Datadog by Logstash scrapping Mesos application's stdout. Soon after the upgrade, logs stopped showing up in Datadog. After some investigation, figured out that the pattern parser failed to parse log string as it was expecting log string format like: [service, trace, span, exportable]. But the format after the upgrade was: [service, trace, span]. Basically, the last exportable property with value true or false was missing. 

The trace, span, exportable are injected into log messages by Spring Cloud Sleuth and Zipkin for distributed tracing via log framework MD5. The documentation of Spring Cloud Sleuth seems not updated with this details. :(

In order to get away with this issue on the Datadog, switching to JSON log message format seemed like a better solution as the JSON is better than strict string pattern matchers. This required an explicit logback definition in resources/logback-spring.xml file (yes, XML is the only way) and a new dependency for JSON logging.

src/main/resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="serviceName" source="spring.zipkin.service.name"/> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <pattern> <pattern> { "timestamp": "%d{yyyy-MM-dd HH:mm:ss.SSS}", "severity": "%level", "service": "${serviceName:-}", "pid": "${PID:-}", "thread": "%thread", "class": "%logger{40}", "trace": "%X{traceId:-}", "span": "%X{spanId:-}", "parent": "%X{parentId:-}", "exportable": "%X{sampled:-}", "logmessage": "%message" } </pattern> </pattern> <!-- Additional support needed for logging stack trace in JSON message --> <!-- https://github.com/logfellow/logstash-logback-encoder --> <stackTrace> <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter"> <maxDepthPerThrowable>30</maxDepthPerThrowable> <maxLength>4096</maxLength> <shortenedClassNameLength>20</shortenedClassNameLength> <rootCauseFirst>true</rootCauseFirst> </throwableConverter> </stackTrace> </providers> </encoder> </appender> <root level="INFO"> <appender-ref ref="console" /> </root> </configuration>

Maven build file, new dependency for JSON log message: pom.xml
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>6.6</version> </dependency>

With this a sample JSON exception log messages looks like:

{ "timestamp": "2021-12-22 12:26:26.552", "severity": "ERROR", "service": "my-service-api", "pid": "65623", "thread": "http-nio-8080-exec-6", "class": "c.g.s.e.MyServceImpl", "trace": "529f2d2d7a65dde4", "span": "529f2d2d7a65dde4", "parent": "", "exportable": "", "logmessage": "My Exception log mesage.", "stack_trace": "c.g.s.e.MyException: my exception message\n\tat c.g.s.s.MyServiceImpl.serviceMethodOne(MyServiceImpl.java:210)\n\tat c.g.s.s.MyServiceImpl.serviceMethoTwo(MyServiceImpl.java:280)\n" }

References


Wednesday, October 06, 2021

I still love Groovy . . .

I was joyfully coding in Groovy for several years. Back to Java two years ago, and have not been writing any production code in Groovy, still writing my own productive non-production utilities in Groovy whenever and wherever I can.

I am trying my best to apply neat things that I learned while working in Groovy projects by using its ecosystem frameworks like Grails framework, Gradle build tool,  Spock framework etc. Within the limitations of Java, Spring Boot, and Maven development world, I am trying hard to write less verbose, and more readable code by leveraging new Java language features including some of each of its version's preview features.

Java is evolving at a steady pace now. Better late than never ;). Still far-away compared to what Groovy was 10+ years ago, or any of current modern languages, in terms of developer's productivity.

I was bit happy to see some convenient factory methods making into Java's collection classes, version after version, since Java 9. Have been happily using one such static factory method .of() on List and Map without caring much of their internal implementations. 

Environment: Java 16, Groovy 3.0.9 on macOS Catalina 10.15.7

Today, I was happily writing code using Map.of() method and kept on adding elements, I had about a dozen of static keys and values to add. IntelliJ was also going happy with me. At some point suddenly IntelliJ turned angry (red) at me. The error was not clear, another Java classic hard-to-understand compilation error. I started to wonder what did I do wrong, was going back and forth on each element I was adding. Quickly realized I was hitting some limitation. Java language team chose the lucky number 10 for these convenient factory methods. There are actually 10 static factory methods named of() that take one to ten arguments.

I fell in love with it, the very first-time I started using it as it's little more concise and readable (not as concise and readable as groovy, but close), but quickly ran into limitations.
  • Map.of() method introduced in Java 9 allows to create an immutable map with up to 10 keys-value pairs.
  • It return an immutable map.
So, use it when you are ok with immutable small Map of up to 10 elements.

The following groovy snippet shows how close (still little more verbose) Java got to Groovy from the painful-finger-typing way to create and initialize a Map with a fixed set of elements. 

// Groovy def groovyMap = [ 'a' : [1], 'b' : [1,2], 'c' : [1,2,3], 'd' : [1,2,3,4], 'e' : [1,2,3,4,5], 'f' : [1,2,3,4,5,6], 'g' : [1,2,3,4,5,6,7], 'h' : [1,2,3,4,5,6,7,8], 'i' : [1,2,3,4,5,6,7,8,9], 'j' : [1,2,3,4,5,6,7,8,9,10], 'k' : [1,2,3,4,5,6,7,8,9,10,11], 'l' : [1,2,3,4,5,6,7,8,9,10,11,12], ] println groovyMap // Java var javaMap = Map.of( "a" , List.of(1), "b" , List.of(1,2), "c" , List.of(1,2,3), "d" , List.of(1,2,3,4), "e" , List.of(1,2,3,4,5), "f" , List.of(1,2,3,4,5,6), "g" , List.of(1,2,3,4,5,6,7), "h" , List.of(1,2,3,4,5,6,7,8), "i" , List.of(1,2,3,4,5,6,7,8,9), "j" , List.of(1,2,3,4,5,6,7,8,9,10), "k" , List.of(1,2,3,4,5,6,7,8,9,10,11), "l" , List.of(1,2,3,4,5,6,7,8,9,10,11,12) ) println javaMap

IntelliJ goes unhappy, with error:
Cannot resolve method 'of(java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>, java.lang.String, java.util.List<E>

Java compiler stays unhappy with compilation error:

no suitable method found for of(java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>,java.lang.String,java.util.List<java.lang.Integer>) method java.util.Map.<K,V>of() is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) method java.util.Map.<K,V>of(K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V,K,V) is not applicable (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length))

When I executed the above code snippet in groovyconsole Groovy compiler at least gave me little better message pointing at the line (29: "k" , List.of(1,2,3,4,5,6,7,8,9,10,11),) that failed compilation and made me think of exceeding some limitation.

groovy.lang.MissingMethodException: No signature of method: static java.util.Map.of() is applicable for argument types: (String, List12, String, List12, String, ListN, String, ListN, String...) values: [a, [1], b, [1, 2], c, [1, 2, 3], d, [1, 2, 3, 4], e, [1, 2, ...], ...] at ConsoleScript9.run(ConsoleScript9:29)

The workaround, I had to go more verbose; at least, better than old way of painful-finger-typing. ;)
import static java.util.Map.entry; // Groovy def groovyMap = [ 'a' : [1], 'b' : [1,2], 'c' : [1,2,3], 'd' : [1,2,3,4], 'e' : [1,2,3,4,5], 'f' : [1,2,3,4,5,6], 'g' : [1,2,3,4,5,6,7], 'h' : [1,2,3,4,5,6,7,8], 'i' : [1,2,3,4,5,6,7,8,9], 'j' : [1,2,3,4,5,6,7,8,9,10], 'k' : [1,2,3,4,5,6,7,8,9,10,11], 'l' : [1,2,3,4,5,6,7,8,9,10,11,12], ] println groovyMap // Java var javaMap = Map.ofEntries( entry("a" , List.of(1)), entry("b" , List.of(1,2)), entry("c" , List.of(1,2,3)), entry("d" , List.of(1,2,3,4)), entry("e" , List.of(1,2,3,4,5)), entry("f" , List.of(1,2,3,4,5,6)), entry("g" , List.of(1,2,3,4,5,6,7)), entry("h" , List.of(1,2,3,4,5,6,7,8)), entry("i" , List.of(1,2,3,4,5,6,7,8,9)), entry("j" , List.of(1,2,3,4,5,6,7,8,9,10)), entry("k" , List.of(1,2,3,4,5,6,7,8,9,10,11)), entry("l" , List.of(1,2,3,4,5,6,7,8,9,10,11,12)) ) println javaMap

NOTE: It's only the Map.of() that has this limitation, the methods List.of(), Set.of() do not have.

Gotcha

  • The method map.of() returns an immutable map though the method signature says it returns simply a Map.
  • In other words it is an unmodifiable map, keys and values cannot be added, removed or updated.
  • When operation to modify the returned Map like put(), replace(), or remove() are performed, they would result with an UnsupportedOperationException with a null exception exception message ;)

Conclusion

I still love Groovy for its simple, less confusing, yet more expressive syntax.

References