Type Classes
The important types in Blindsight are Markers
, Argument
, and Message
.
Where possible, the APIs map automatically, using the ToMarkers
, ToMessage
and ToArgument
type classes, respectively.
General Principles
Type classes let you represent your domain objects as structured logging data.
Although Blindsight does provide mappings of the basic primitive types, you may want to provide some more semantic detail about what the value represents, and use the DSL with a specific field name and type – for example, rather than representing an age as an integer, logger.info("person age = {}", persion.age)
is easier if you use a specific class Age
and have a type class instance that represents that Age
as bobj("age_year" -> ageInYear)
You can of course use type classes to render any given type in logging. For example, to render a Future
as an argument:
implicit val futureToArgument: ToArgument[Future[_]] = ToArgument[Future[_]] { future =>
new Argument(future.toString)
}
logger.info("future is {}", Future.successful(()))
You may find it helpful to use Refined and Coulomb to provide type-safe validation and unit representation of data to the DSL.
Markers
You can pass in something that is not a marker, and provided you have a ToMarkers
in implicit scope, you can get it auto-converted through type annotation. The various logging statement will only take a Markers
:
sourceval marker = MarkerFactory.getDetachedMarker("SOME_MARKER")
logger.info(Markers(marker), "message with marker")
You can also combine markers:
sourceval markers1: Markers = Markers(MarkerFactory.getDetachedMarker("MARKER1"))
val markers2: Markers = Markers(MarkerFactory.getDetachedMarker("MARKER2"))
val markersOnePlusTwo: Markers = markers1 + markers2
You should not try to manipulate the marker through marker.add
or marker.remove
. It’s better to treat a org.slf4j.Marker
instance as completely immutable, and manage any composition through Markers
.
Getting at the underlying marker is a lazy val
that is computed once:
sourceval combinedMarker: org.slf4j.Marker = markersOnePlusTwo.marker
You can also convert your own objects into appropriate markers.
sourcesealed trait Weekday {
def value: String
}
case object Monday extends Weekday { val value = "MONDAY" }
object Weekday {
import scala.language.implicitConversions
implicit def weekday2Marker(weekday: Weekday): Markers = Markers(weekday)
implicit val toMarkers: ToMarkers[Weekday] = ToMarkers { weekday =>
Markers(MarkerFactory.getDetachedMarker(weekday.value))
}
}
logger.debug(Monday, "this is a test")
Or you can use the MarkersEnrichment
that adds an asMarkers
method to org.slf4j.Marker
through type enrichment:
sourceimport MarkersEnrichment._ // adds "asMarkers" to org.slf4j.Marker
val markers: Markers = marker.asMarkers
The SLF4J API is awkward to use with markers, because there are several possible variations that can confuse the compiler and stop the type class from being used directly. To avoid using the Markers(marker)
wrapper, you can use the fluent API or use contextual logging.
Argument and Arguments
Arguments must be convertible to Argument
. This is usually done with type class instances.
Default ToArgument
are determined for the primitives (String
, Int
, etc):
sourcelogger.info("one argument {}", 42) // works, because default
You can define your own argument type class instances:
sourceimport java.time.temporal.ChronoUnit
implicit val chronoUnitToArgument: ToArgument[ChronoUnit] = ToArgument[ChronoUnit] { unit =>
new Argument(unit.toString)
}
logger.info("chronounit is {}", ChronoUnit.MILLIS)
Although it’s usually better to use the DSL and map to a BObject
:
sourcecase class Person(name: String, age: Int)
implicit val personToArgument: ToArgument[Person] = ToArgument[Person] { person =>
import DSL._
Argument(("name" -> person.name) ~ ("age_year" -> person.age))
}
logger.info("person is {}", Person("steve", 12))
There is a plural of Argument
, Arguments
that is used in place of varadic arguments. If you have more than two arguments, you will need to wrap them so they are provided as a single Arguments
instance:
logger.info("arg {}, arg {}, arg {}", Arguments(1, "2", false))
Message
The Message
is usually a String, but it doesn’t have to be. Using the ToMessage
type class, you can convert any class into a message. This is probably most appropriate with string-like classes like CharSequence
and akka.util.ByteString
.
implicit val toMessage: ToMessage[CharSequence] = ToMessage[CharSequence] { charSeq =>
new Message(charSeq.toString)
}
val charSeq: CharSequence = new ArrayCharSequence(Array('1', '2', '3'))
logger.fluent.info.message(charSeq).log()
Custom JSON Mappings
You can pass through JSON directly if you already have it and recreating it through the DSL would be a waste.
For example, if you are working with json4s or play-json, you can convert to Jackson JsonNode using a type class:
import com.fasterxml.jackson.databind.JsonNode
trait ToJsonNode[T] {
def jsonNode(instance: T): JsonNode
}
object ToJsonNode {
import org.json4s._
implicit val json4sToJsonNode: ToJsonNode[JValue] = new ToJsonNode[JValue] {
import org.json4s.jackson.JsonMethods._
override def jsonNode(instance: JValue): JsonNode = asJsonNode(instance)
}
}
And then set up your own type mappings as follows:
implicit def jsonToArgument[T: ToJsonNode]: ToArgument[(String, T)] = ToArgument {
case (k, instance) =>
val node = implicitly[ToJsonNode[T]].jsonNode(instance)
Argument(StructuredArguments.keyValue(k, node)) // or raw(k, node.toPrettyString)
}
import org.json4s._
import org.json4s.jackson.JsonMethods._
logger.info("This message has json {}", parse(""" { "numbers" : [1, 2, 3, 4] } """))