Semantic API

A semantic logging API is strongly typed and does not have the same construction oriented approach as the fluent API. Instead, the type of the instance is presumed to have a mapping directly to the attributes being logged.

The semantic API works against Statement directly. The application is expected to handle the type class mapping to Statement.

Here is an example:

object SemanticMain {

  sealed trait UserEvent {
    def name: String
  }

  final case class UserLoggedInEvent(name: String, ipAddr: String) extends UserEvent

  object UserLoggedInEvent {
    implicit val toStatement: ToStatement[UserLoggedInEvent] = ToStatement { instance =>
      import com.tersesystems.blindsight.DSL._
      Statement()
        .withMessage("UserLoggedInEvent message with args {}")
        .withArguments(
          Arguments(
            bobj(
              "user-logged-out-event" ->
                ("name"     -> instance.name) ~
                  ("ipAddr" -> instance.ipAddr)
            )
          )
        )
    }
  }

  final case class UserLoggedOutEvent(name: String, reason: String) extends UserEvent

  object UserLoggedOutEvent {
    implicit val toStatement: ToStatement[UserLoggedOutEvent] = ToStatement { instance =>
      import com.tersesystems.blindsight.DSL._
      Statement()
        .withMessage("UserLoggedOutEvent message with args {}")
        .withArguments(
          Arguments(
            bobj(
              "user-logged-out-event" ->
                ("name"     -> instance.name) ~
                  ("reason" -> instance.reason)
            )
          )
        )
    }
  }

  final case class UserIsUpLateEvent(name: String, excuse: String) extends UserEvent

  object UserIsUpLateEvent {
    implicit val toStatement: ToStatement[UserIsUpLateEvent] = ToStatement { instance =>
      import com.tersesystems.blindsight.DSL._

      Statement()
        .withMessage(instance.toString)
        .withArguments(
          Arguments(
            bobj(
              "user-is-up-late-event" ->
                ("name"     -> instance.name) ~
                  ("excuse" -> instance.excuse)
            )
          )
        )
    }
  }

  def main(args: Array[String]): Unit = {

    val userEventLogger: SemanticLogger[UserEvent] =
      LoggerFactory.getLogger(getClass).semantic[UserEvent]

    userEventLogger.info(UserLoggedInEvent("steve", "127.0.0.1"))
    userEventLogger.info(UserLoggedOutEvent("steve", "timeout"))

    userEventLogger.warn.when(LocalTime.now().isAfter(LocalTime.of(23, 0))) { log =>
      log(UserIsUpLateEvent("will", "someone is WRONG on the internet"))
    }

    val onlyLoggedInEventLogger: SemanticLogger[UserLoggedInEvent] =
      userEventLogger.refine[UserLoggedInEvent]
    onlyLoggedInEventLogger.info(UserLoggedInEvent("mike", "10.0.0.1"))
  }
}

in plain text:

FgEdhil2znw6O0Qbm7EAAA 2020-04-05T23:09:08.359+0000 [INFO ] example.semantic.SemanticMain$ in main  - UserLoggedInEvent(steve,127.0.0.1)
FgEdhil2zsg6O0Qbm7EAAA 2020-04-05T23:09:08.435+0000 [INFO ] example.semantic.SemanticMain$ in main  - UserLoggedOutEvent(steve,timeout)
FgEdhil2zsk6O0Qbm7EAAA 2020-04-05T23:09:08.436+0000 [INFO ] example.semantic.SemanticMain$ in main  - UserLoggedInEvent(mike,10.0.0.1)

and in JSON:

{
  "id": "FgEdhil2znw6O0Qbm7EAAA",
  "relative_ns": -298700,
  "tse_ms": 1586128148359,
  "start_ms": null,
  "@timestamp": "2020-04-05T23:09:08.359Z",
  "@version": "1",
  "message": "UserLoggedInEvent(steve,127.0.0.1)",
  "logger_name": "example.semantic.SemanticMain$",
  "thread_name": "main",
  "level": "INFO",
  "level_value": 20000,
  "name": "steve",
  "ipAddr": "127.0.0.1"
}

Refinement Types

Semantic Logging works very well with refinement types.

For example, you can add compile time limitations on the kinds of messages that are passed in:

object RefinedMain {
  import com.tersesystems.blindsight._
  import com.tersesystems.blindsight.semantic._
  import eu.timepit.refined._
  import eu.timepit.refined.api.Refined
  import eu.timepit.refined.collection.NonEmpty
  import eu.timepit.refined.string._

  implicit def stringToStatement[R]: ToStatement[Refined[String, R]] =
    ToStatement { str =>
      Statement().withMessage(str.value)
    }

  def main(args: Array[String]): Unit = {
    val logger = LoggerFactory.getLogger

    val notEmptyLogger: SemanticLogger[String Refined NonEmpty] =
      logger.semantic[String Refined NonEmpty]
    notEmptyLogger.info(refineMV[NonEmpty]("this is a statement"))
    // will not compile
    //notEmptyLogger.info(refineMV(""))

    val urlLogger: SemanticLogger[String Refined Url] = logger.semantic[String Refined Url]
    urlLogger.info(refineMV[Url]("http://google.com"))
    // will not compile
    //urlLogger.info(refineMV("this is a statement"))
  }

}
The source code for this page can be found here.