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(()))
Note

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
Note

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] } """))
The source code for this page can be found here.