LogStage is a zero-cost structural logging framework.

Key features:

  1. LogStage extracts structure from ordinary string interpolations in your log messages with zero changes to code.
  2. LogStage uses macros to extract log structure, its faster at runtime than a typical reflective structural logging frameworks,
  3. Log contexts
  4. Console, File and SLF4J sinks included, File sink supports log rotation,
  5. Human-readable output and JSON output included,
  6. Method-level logging granularity. Can configure methods com.example.Service.start and com.example.Service.doSomething independently,
  7. Slf4J adapters: route legacy Slf4J logs into LogStage router


The following snippet:

import logstage._
import scala.util.Random

val logger = IzLogger()

val justAnArg = "example"
val justAList = List[Any](10, "green", "bottles")

logger.trace(s"Argument: $justAnArg, another arg: $justAList")

// custom name, not based on `val` name
logger.info(s"Named expression: ${Random.nextInt() -> "random number"}")

// print result without a name
logger.warn(s"Invisible argument: ${Random.nextInt() -> "random number" -> null}")

// add following fields to all messages printed by a new logger value
val ctxLogger = logger("userId" -> "user@google.com", "company" -> "acme")
val delta = Random.nextInt(1000)

ctxLogger.info(s"Processing time: $delta")

Will look like this in string form:


And like this in JSON:



  1. JSON formatter is type aware!
  2. Each JSON message contains @class field with holds a unique event class identifier. All events produced by the same source code line will share the same event class.


// LogStage API, you need it to use the logger
libraryDependencies += Izumi.R.logstage_core

// LogStage machinery
libraryDependencies ++= Seq(
  // json output
  // router from Slf4j to LogStage
  , Izumi.R.logstage_adapter-slf4j


val izumi_version = "0.8.0"
// LogStage API, you need it to use the logger
libraryDependencies += "io.7mind.izumi" %% "logstage-core" % izumi_version

// LogStage machinery
libraryDependencies ++= Seq(
  // json output
    "io.7mind.izumi" %% "logstage-rendering-circe" % izumi_version
  // router from Slf4j to LogStage
  , "io.7mind.izumi" %% "logstage-adapter-slf4j" % izumi_version    

If you’re not using sbt-izumi-deps plugin.

Basic setup

import logstage._
import logstage.circe._

val jsonSink = ConsoleSink.json(prettyPrint = true)
val textSink = ConsoleSink.text(colored = true)

val sinks = List(jsonSink, textSink)

val logger: IzLogger = IzLogger(Trace, sinks)
val contextLogger: IzLogger = logger(Map("key" -> "value"))



SLF4J Router

By default logstage_adapter-slf4j will route into stderr, due to the global mutable nature of slf4j you’ll have to configure a global singleton to use the same LogRouter as your logger:

import logstage._
import com.github.pshirshov.izumi.logstage.api.routing.StaticLogRouter

val myLogger = IzLogger()
// myLogger: IzLogger = com.github.pshirshov.izumi.logstage.api.IzLogger@1c0812ce

// configure SLF4j to use the same router as `myLogger`

Log algebras

LogIO and LogBIO algebras provide a purely-functional API for one- and two-parameter effect types respectively:

import logstage._
import cats.effect.IO

val logger = IzLogger()
// logger: IzLogger = com.github.pshirshov.izumi.logstage.api.IzLogger@44f8953

val log = LogIO.fromLogger[IO](logger)
// log: LogIO[IO] = logstage.LogIO$$anon$1@1a7bb51e

log.info(s"Hey! I'm logging with ${log}stage!").unsafeRunSync()
I 2019-03-29T23:21:48.693Z[Europe/Dublin] r.S.App7.res8 ...main-12:5384  (00_logstage.md:92) Hey! I'm logging with log=logstage.LogIO$$anon$1@72736f25stage!

LogstageZIO.withFiberId provides an LogBIO instance that always logs the current fiber ID in addition to usual logging of thread ID:


import logstage.LogstageZIO
import scalaz.zio.{IO, DefaultRuntime}

val log = LogstageZIO.withFiberId(logger)
// log: LogBIO[IO] = logstage.LogstageZIO$$anon$1@44119c2

val rts = new DefaultRuntime {}
// rts: AnyRef with DefaultRuntime = repl.Session$App11$$anon$1@bf31fef
rts.unsafeRun {
  log.info(s"Hey! I'm logging with ${log}stage!")
I 2019-03-29T23:21:48.760Z[Europe/Dublin] r.S.App9.res10 ...main-12:5384  (00_logstage.md:123) {fiberId=0} Hey! I'm logging with log=logstage.LogstageZIO$$anon$1@c39104astage!

LogIO/LogBIO algebras can be extended with custom context, same as IzLogger:

import cats.effect.IO
import cats.implicits._
import logstage._
import io.circe.syntax._

def importEntity(entity: Entity)(implicit log: LogIO[IO]): IO[Unit] = {
  val ctxLog = log("ID" -> someEntity.id, "entityAsJSON" -> entity.asJson.pretty(Printer.spaces2))

  IO(???).handleErrorWith {
    case error =>
      ctxLog.error(s"Failed to import entity: $error.").void
      // message includes `ID` and `entityAsJSON` fields