I released echopraxia-plusscala last week – it's a Scala API for structured logging that co-exists peacefully with Logback or Log4J 2, for applications that want a richer logging experience.
Tuples and Functions
The first enhancement is richer support for tuples and functions. For Scala, it comes out of the box:
Requiring a function as a second argument is relatively low impact, because of the placeholder
_ variable. I tried working with various infix DSLs and found them to be more clunky than useful.
Type Classes in Field Builders
Where it really starts getting fun is in the field builder.
Most type classes frameworks, including Blindsight, have a pattern that places the type class instance in a companion object:
This is convenient, but results in a "globally available" type class instance. Once your code depends on companion objects throughout the codebase, it can be very hard to limit and scope that behavior.
This behavior can be very counterproductive for JSON serialization – consider a situation where you may have a public API representation of an object, and a private representation. Rather than having two different JSON serializations of the same object, you'll sometimes see two almost identical case class representations of the same data, just so that the JSON will match up.
The default field builder API has the type class defined as dependent types, with the primitives defined inline:
And the FieldBuilder takes the type class:
Defining your own type class instance is likewise done inside the trait:
Inside the field builder function, there's no import tax applied. This is because the type inference is to
Logger[SomeFieldBuilder.type], meaning that the
SomeFieldBuilder singleton object is within the implicit resolution scope of the function – by contrast,
Logger[SomeFieldBuilder] will not work, as implicit resolution won't follow the trait alone.
There's no possibility of ambiguous or conflicting type class instances, and you can control what's being logged by swapping out the field builder. The field builder is operating as an object capability for logging arbitrary objects, instead of the capability being provided by the companion object or the logger itself.
This means that if convenience methods or functions are defined, it's easiest to create another singleton object for it:
And then the
method is available for use in that context.
This is a bit different from the usual idioms, but it's very handy.
Type Class Derivation
The second thing possible in Scala is using automatic type class derivation with Magnolia. Magnolia is a "shortcut" for defining type class behavior for case classes and sealed traits, and what makes Magnolia really cool is that it does this using Scala macros, meaning that the Scala compiler is literally filling in the implementation for
Magnolia comes with both fully automatic derivation, and semi-automatic derivation, which requires a
gen[T] marker for the macro:
It is possible to abuse auto-derivation, particularly with logging – dumping an fully automatic case class AST can result in very large logs, but again, the field builder is providing the capability, so you can limit or truncate your output at a single point.
Miscellanous Scala Support
There's a number of small tweaks throughout the code that only show up on direct use. For example, Scala has a number of built-in classes like
Either. These are treated as normal case classes, but there can be cases where users would rather log the contained value directly:
In addition, the
LoggingContext interfaces used in Echopraxia all return Scala collections, so
findList returns a
Map[String, Any] for applicable objects:
Trace and Flow Loggers
Finally, there are two additional loggers available for Scala.
The trace logger leverages sourcecode to extract as much information as possible from the method to provide trace information on entry and exit:
The flow logger renders entry and exit information, but doesn't leverage sourcecode data. Otherwise, the API is the same.
This was probably the part that I had the hardest time with.
The reason? Adding source code information has a runtime overhead, even if the logger is disabled. It's minor, but it's there – and in a situation involving tracing, you could reasonably expect every single method inside a hot loop to be called repeatedly.
I've benchmarked and optimized the code repeatedly to minimize object allocation and ensure everything runs as close to native speed as possible, as I know the first question of any logging framework is going to be "is it slow?" But the really important question is "what does my logging framework do when it's turned off?"
SLF4J sets the bar at 2 nanoseconds for an
isEnabled check. Echopraxia takes 7.7ns for an
isEnabled check (or 0.5ns if you have
Condition.never set). But adding sourcecode implicit parameters adds 20 nanoseconds to a logging statement before it even gets tot he
Adding two versions of the same logger that differ only in their implicit parameters was the best compromise; if the app is slow or seems under GC pressure, then switch to a flow logger and/or use
Condition.never to do a direct passthrough.
The other thing I'm happy about in the flow logging API is that again, all control goes through the field builder API. For example, the
TraceLogger takes a field builder with
TraceFieldBuilder determines how the source info is represented and how the
exit statements will look:
and the default can be swapped out:
The logger itself barely knows what's going on. It's stupidly simple:
One of the things that did matter to me was that source code information should be available as a condition, so I did have to make some tweaks to the Java API to ensure that the core logger could take in some extra fields:
This means that you have the option of turning on logging at a
TRACE level, and can add a condition that only actually traces for the methods you select. Using the condition API may be around 80 to 500 nanoseconds per call depending on your choice of methods – well worth the cost for throttling your logging output to only what you need.