LogStage

LogStage is a zero-cost structural logging framework for Scala & Scala.js

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

Dependencies

libraryDependencies ++= Seq(
  // LogStage core library
  "io.7mind.izumi" %% "logstage-core" % "0.10.2-SNAPSHOT",
  // Json output
  "io.7mind.izumi" %% "logstage-rendering-circe" % "0.10.2-SNAPSHOT",
  // Router from Slf4j to LogStage
  "io.7mind.izumi" %% "logstage-adapter-slf4j" % "0.10.2-SNAPSHOT",
  // Configure LogStage with Typesafe Config
  "io.7mind.izumi" %% "logstage-config" % "0.10.2-SNAPSHOT",
  // LogStage integration with DIStage
  "io.7mind.izumi" %% "distage-extension-logstage" % "0.10.2-SNAPSHOT",
  // Router from LogStage to Slf4J
  "io.7mind.izumi" %% "logstage-sink-slf4j " % "0.10.2-SNAPSHOT",
)

Overview

The following snippet:

import logstage.IzLogger
import scala.util.Random

val logger = IzLogger()
// logger: logstage.package.IzLogger.Logger = izumi.logstage.api.IzLogger@790b6d10

val justAnArg = "example"
// justAnArg: String = "example"
val justAList = List[Any](10, "green", "bottles")
// justAList: List[Any] = List(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")
// ctxLogger: IzLogger = izumi.logstage.api.IzLogger@228b3146
val delta = Random.nextInt(1000)
// delta: Int = 354

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

Will look like this in string form:

logstage-sample-output-string

And like this in JSON:

logstage-sample-output-string

Note:

  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.

Syntax Reference

  1. Simple variable: scala logger.info(s"My message: $argument")

  2. Chain: scala logger.info(s"My message: ${call.method} ${access.value}")

  3. Named expression: scala logger.info(s"My message: ${Some.expression -> "argname"}")

  4. Invisible named expression: scala logger.info(s"My message: ${Some.expression -> "argname" -> null}")

5) De-camelcased name: scala logger.info(${camelCaseName-> ' '})

Basic setup

import logstage.{ConsoleSink, IzLogger, Trace}
import logstage.circe.LogstageCirceRenderingPolicy

val textSink = ConsoleSink.text(colored = true)
// textSink: ConsoleSink = izumi.logstage.sink.ConsoleSink@25ba093c
val jsonSink = ConsoleSink(LogstageCirceRenderingPolicy(prettyPrint = true))
// jsonSink: ConsoleSink = izumi.logstage.sink.ConsoleSink@1a8a61aa

val sinks = List(jsonSink, textSink)
// sinks: List[ConsoleSink] = List(
//   izumi.logstage.sink.ConsoleSink@1a8a61aa,
//   izumi.logstage.sink.ConsoleSink@25ba093c
// )

val logger: IzLogger = IzLogger(Trace, sinks)
// logger: IzLogger = izumi.logstage.api.IzLogger@1a087afd
val contextLogger: IzLogger = logger("key" -> "value")
// contextLogger: IzLogger = izumi.logstage.api.IzLogger@647ee2ce

logger.info("Hey")

contextLogger.info(s"Hey")

Log algebras

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

import logstage.{IzLogger, LogIO}
import cats.effect.IO

val logger = IzLogger()
// logger: logstage.package.IzLogger.Logger = izumi.logstage.api.IzLogger@6b3477ca

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

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 a LogBIO instance that logs the current ZIO FiberId in addition to the thread id:

Example:

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

val log = LogstageZIO.withFiberId(IzLogger())
// log: logstage.package.LogBIO[IO] = logstage.LogstageZIO$$anon$1@14cc0bdd

val rts = new DefaultRuntime {}
// rts: AnyRef with DefaultRuntime = repl.Session$App10$$anon$1@39f183a1
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 using their .apply method, same as IzLogger:

import com.example.Entity

def load(entity: Entity): cats.effect.IO[Unit] = cats.effect.IO.unit
import cats.effect.IO
import cats.implicits._
import logstage.LogIO
import io.circe.Printer
import io.circe.syntax._

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

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

SLF4J Router

When not configured, logstage-adapter-slf4j will log messages with level >= Info to stdout.

Due to the global mutable nature of slf4j, to configure slf4j logging you’ll have to mutate a global singleton StaticLogRouter. Replace its LogRouter with the same one you use elsewhere in your application to use the same configuration for Slf4j.

import logstage.IzLogger
import izumi.logstage.api.routing.StaticLogRouter

val myLogger = IzLogger()
// myLogger: logstage.package.IzLogger.Logger = izumi.logstage.api.IzLogger@71b33972

// configure SLF4j to use the same router that `myLogger` uses
StaticLogRouter.instance.setup(myLogger.router)

@@@ index

@@@