diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85bebeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.* +*.swp +*~ +*.log +target diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..4e743b9 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,7 @@ +version = 2.2.2 +maxColumn = 120 +align = most +align.openParenDefnSite = true +align.openParenCallSite = true +includeCurlyBraceInSelectChains = false +rewrite.rules = [SortImports, SortModifiers, RedundantBraces] diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..d693853 --- /dev/null +++ b/build.sbt @@ -0,0 +1,65 @@ +import sbt._ +import Keys._ +import xerial.sbt.Sonatype._ + +inThisBuild( + Seq( + organization := "com.github.mvv.zilog", + version := "0.1-SNAPSHOT", + homepage := Some(url("https://github.com/mvv/zilog")), + scmInfo := Some(ScmInfo(url("https://github.com/mvv/zilog"), "scm:git@github.com:mvv/zilog.git")), + licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), + developers := List( + Developer(id = "mvv", + name = "Mikhail Vorozhtsov", + email = "mikhail.vorozhtsov@gmail.com", + url = url("https://github.com/mvv")) + ), + sonatypeProjectHosting := Some(GitHubHosting("mvv", "zilog", "mikhail.vorozhtsov@gmail.com")) + ) +) + +ThisBuild / publishTo := sonatypePublishToBundle.value +ThisBuild / publishMavenStyle := true + +inThisBuild( + Seq( + crossScalaVersions := Seq("2.13.1", "2.12.10", "2.11.12"), + scalaVersion := crossScalaVersions.value.head, + scalacOptions ++= Seq("-feature", "-deprecation", "-unchecked", "-Xfatal-warnings") + ) +) + +def isPriorTo2_13(version: String): Boolean = + CrossVersion.partialVersion(version) match { + case Some((2, minor)) => minor < 13 + case _ => false + } + +lazy val zilog = (project in file(".")) + .settings( + name := "zilog", + sonatypeProfileName := "com.github.mvv", + sonatypeSessionName := s"Zilog_${version.value}", + scalacOptions ++= { + if (isPriorTo2_13(scalaVersion.value)) { + Nil + } else { + Seq("-Ymacro-annotations") + } + }, + libraryDependencies ++= + Seq( + "dev.zio" %% "zio" % "1.0.0-RC16" % Provided, + "org.slf4j" % "slf4j-api" % "1.7.29", + "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.specs2" %% "specs2-core" % "4.8.1" % Test + ), + libraryDependencies ++= { + if (isPriorTo2_13(scalaVersion.value)) { + Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)) + } else { + Nil + } + } + ) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..6adcdc7 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.3 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..4eac55e --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.2.1") +addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8") diff --git a/src/main/scala/com/github/mvv/zilog/ImplicitArgsLogger.scala b/src/main/scala/com/github/mvv/zilog/ImplicitArgsLogger.scala new file mode 100644 index 0000000..cb02bf3 --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/ImplicitArgsLogger.scala @@ -0,0 +1,106 @@ +package com.github.mvv.zilog + +import org.slf4j.MDC +import zio.{FiberRef, UIO, ZIO} + +trait ImplicitArgsLogger extends Logger { + override def logger: ImplicitArgsLogger.Service[Any] +} + +object ImplicitArgsLogger { + trait Service[-R] extends Logger.Service[R] { + def withImplicitLogArgs[R1 <: R, E, A](args: (String, Any)*)(zio: ZIO[R1, E, A]): ZIO[R1, E, A] + } + + sealed abstract private class FiberRefService[-R](fiberRef: FiberRef[Map[String, Any]]) extends Service[R] { + final override def withImplicitLogArgs[R1 <: R, E, A](args: (String, Any)*)(zio: ZIO[R1, E, A]): ZIO[R1, E, A] = + fiberRef.get.flatMap { current => + fiberRef.locally(current ++ args)(zio) + } + def log(level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any])( + implicit ctx: LoggerContext + ): ZIO[R, Nothing, Unit] + final override def log(level: Level, format: String, args: Array[Any])( + implicit ctx: LoggerContext + ): ZIO[R, Nothing, Unit] = + fiberRef.get.flatMap(log(level, format, args, _)) + } + + object Service { + def apply[R]( + f: (LoggerContext, Level, String, Array[Any], Map[String, Any]) => ZIO[R, Nothing, Unit] + ): UIO[Service[R]] = + FiberRef.make(Map.empty[String, Any], (first: Map[String, Any], _: Map[String, Any]) => first).map { fiberRef => + new FiberRefService[R](fiberRef) { + override def log(level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any])( + implicit ctx: LoggerContext + ): ZIO[R, Nothing, Unit] = + f(ctx, level, format, explicitArgs, implicitArgs) + } + } + } + + val mdc: UIO[Service[Any]] = Service { + (ctx: LoggerContext, level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any]) => + ZIO.effectTotal { + val oldValues = implicitArgs.iterator.map { + case (key, value) => + val oldValue = Option(MDC.get(key)) + MDC.put(key, value.toString) + key -> oldValue + }.toSeq + ctx.log(level, format, explicitArgs) + oldValues.foreach { + case (key, oldValue) => + oldValue match { + case Some(value) => + MDC.put(key, value) + case None => + MDC.remove(key) + } + } + } + } + + private def countPlaceholdersIn(format: String): Int = { + var i = format.indexOf("{}") + var counter = 0 + if (i == 0) { + counter += 1 + i = format.indexOf("{}", 2) + } + while (i > 0) { + if (format.charAt(i - 1) != '\\') { + counter += 1 + } + i = format.indexOf("{}", i + 2) + } + counter + } + + val appendToMessage: UIO[Service[Any]] = Service { + (ctx: LoggerContext, level: Level, format: String, explicitArgs: Array[Any], implicitArgs: Map[String, Any]) => + ZIO.effectTotal { + val (fullFormat, fullArgs) = if (implicitArgs.isEmpty) { + (format, explicitArgs) + } else { + val formatSuffix = implicitArgs.keysIterator.map { key => + s"$key = {}" + }.mkString("; ") + val numFormatArgs = countPlaceholdersIn(format) + val numExplicitArgs = explicitArgs.length + val fullArgs = numFormatArgs.compare(numExplicitArgs) match { + case -1 => + val (formatArgs, extraArgs) = explicitArgs.splitAt(numFormatArgs) + formatArgs ++ implicitArgs.values ++ extraArgs + case 1 => + explicitArgs ++ Seq.fill(numFormatArgs - numExplicitArgs)(null) ++ implicitArgs.values + case _ => + explicitArgs ++ implicitArgs.values + } + (s"$format; $formatSuffix", fullArgs) + } + ctx.log(level, fullFormat, fullArgs) + } + } +} diff --git a/src/main/scala/com/github/mvv/zilog/Level.scala b/src/main/scala/com/github/mvv/zilog/Level.scala new file mode 100644 index 0000000..e1ebf82 --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/Level.scala @@ -0,0 +1,27 @@ +package com.github.mvv.zilog + +sealed trait Level { + protected val code: Int +} + +object Level { + case object Error extends Level { + override protected val code = 4 + } + case object Warn extends Level { + override protected val code = 3 + } + case object Info extends Level { + override protected val code = 2 + } + case object Debug extends Level { + override protected val code = 1 + } + case object Trace extends Level { + override protected val code = 0 + } + + implicit val levelOrdering: Ordering[Level] = new Ordering[Level] { + override def compare(x: Level, y: Level): Int = x.code.compare(y.code) + } +} diff --git a/src/main/scala/com/github/mvv/zilog/Logger.scala b/src/main/scala/com/github/mvv/zilog/Logger.scala new file mode 100644 index 0000000..67e3c1e --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/Logger.scala @@ -0,0 +1,34 @@ +package com.github.mvv.zilog + +import zio.ZIO + +import scala.language.experimental.macros + +trait Logger { + def logger: Logger.Service[Any] +} + +object Logger { + trait Service[-R] { + def log(level: Level, format: String, args: Array[Any])(implicit ctx: LoggerContext): ZIO[R, Nothing, Unit] + final def error(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logError + final def error(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logErrorWithThrowable + final def warn(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logWarn + final def warn(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logWarnWithThrowable + final def info(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logInfo + final def info(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logInfoWithThrowable + final def debug(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logDebug + final def debug(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logDebugWithThrowable + final def trace(message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logTrace + final def trace(e: Throwable, message: String): ZIO[R, Nothing, Unit] = macro LoggerMacro.logTraceWithThrowable + } + + object Default extends Logger { + override val logger: Service[Any] = new Service[Any] { + override def log(level: Level, format: String, args: Array[Any])( + implicit ctx: LoggerContext + ): ZIO[Any, Nothing, Unit] = + ZIO.effectTotal(ctx.log(level, format, args)) + } + } +} diff --git a/src/main/scala/com/github/mvv/zilog/LoggerContext.scala b/src/main/scala/com/github/mvv/zilog/LoggerContext.scala new file mode 100644 index 0000000..6cbed55 --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/LoggerContext.scala @@ -0,0 +1,29 @@ +package com.github.mvv.zilog + +import org.slf4j.LoggerFactory + +import scala.reflect.{classTag, ClassTag} + +final class LoggerContext(val underlying: org.slf4j.Logger) extends AnyVal { + def isLevelEnabled(level: Level): Boolean = + level match { + case Level.Error => underlying.isErrorEnabled + case Level.Warn => underlying.isWarnEnabled + case Level.Info => underlying.isInfoEnabled + case Level.Debug => underlying.isDebugEnabled + case Level.Trace => underlying.isTraceEnabled + } + def log(level: Level, format: String, args: Array[Any]): Unit = + level match { + case Level.Error => underlying.error(format, args.asInstanceOf[Array[AnyRef]]: _*) + case Level.Warn => underlying.warn(format, args.asInstanceOf[Array[AnyRef]]: _*) + case Level.Info => underlying.info(format, args.asInstanceOf[Array[AnyRef]]: _*) + case Level.Debug => underlying.debug(format, args.asInstanceOf[Array[AnyRef]]: _*) + case Level.Trace => underlying.trace(format, args.asInstanceOf[Array[AnyRef]]: _*) + } +} + +object LoggerContext { + def apply(name: String): LoggerContext = new LoggerContext(LoggerFactory.getLogger(name)) + def apply[A: ClassTag]: LoggerContext = new LoggerContext(LoggerFactory.getLogger(classTag[A].runtimeClass)) +} diff --git a/src/main/scala/com/github/mvv/zilog/LoggerMacro.scala b/src/main/scala/com/github/mvv/zilog/LoggerMacro.scala new file mode 100644 index 0000000..c611ad2 --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/LoggerMacro.scala @@ -0,0 +1,100 @@ +package com.github.mvv.zilog + +import scala.annotation.tailrec +import scala.reflect.macros.blackbox + +class LoggerMacro(val c: blackbox.Context) { + import c.universe._ + + private val Error = c.Expr(q"_root_.com.github.mvv.zilog.Level.Error") + private val Warn = c.Expr(q"_root_.com.github.mvv.zilog.Level.Warn") + private val Info = c.Expr(q"_root_.com.github.mvv.zilog.Level.Info") + private val Debug = c.Expr(q"_root_.com.github.mvv.zilog.Level.Debug") + private val Trace = c.Expr(q"_root_.com.github.mvv.zilog.Level.Trace") + + def logError(message: c.Expr[String]): Tree = + log(Error, None, message) + def logErrorWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree = + log(Error, Some(e), message) + def logWarn(message: c.Expr[String]): Tree = + log(Warn, None, message) + def logWarnWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree = + log(Warn, Some(e), message) + def logInfo(message: c.Expr[String]): Tree = + log(Info, None, message) + def logInfoWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree = + log(Info, Some(e), message) + def logDebug(message: c.Expr[String]): Tree = + log(Debug, None, message) + def logDebugWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree = + log(Debug, Some(e), message) + def logTrace(message: c.Expr[String]): Tree = + log(Trace, None, message) + def logTraceWithThrowable(e: c.Expr[Throwable], message: c.Expr[String]): Tree = + log(Trace, Some(e), message) + + private object ListOfStrings { + def unapply(list: List[Tree]): Option[List[String]] = { + @tailrec + def loop(acc: List[String], left: List[Tree]): Option[List[String]] = + left.headOption match { + case Some(Literal(Constant(s: String))) => + loop(s :: acc, left.tail) + case Some(_) => + None + case None => + Some(acc.reverse) + } + loop(Nil, list) + } + } + + def log(level: c.Expr[Level], error: Option[c.Expr[Throwable]], message: c.Expr[String]): Tree = { + val (format, args) = message.tree match { + // 2.11 and 2.12 + case Apply(Select(Apply(Select(prefix, TermName("apply")), ListOfStrings(formatPieces)), TermName("s")), args) + if prefix.tpe.typeSymbol == typeOf[StringContext.type].typeSymbol && formatPieces.size == args.size + 1 => + (Literal(Constant(formatPieces.mkString("{}"))), args) + // 2.13+ + case Typed(expr, _) => + @tailrec + def linearize(acc: List[Tree], lhs: Tree): List[Tree] = + lhs match { + case Apply(Select(next, TermName("$plus")), List(arg)) + if next.tpe.typeSymbol == typeOf[String].typeSymbol => + linearize(arg :: acc, next) + case other => + other :: acc + } + @tailrec + def loop(format: String, args: Seq[Tree], rhs: Seq[Tree], isArg: Boolean): (Tree, Seq[Tree]) = + rhs.headOption match { + case Some(next) if isArg => + loop(s"$format{}", args :+ next, rhs.tail, false) + case Some(Literal(Constant(s: String))) => + loop(s"$format$s", args, rhs.tail, true) + case Some(next) => + loop(s"$format{}", args :+ next, rhs.tail, false) + case None => + (Literal(Constant(format)), args) + } + loop("", Vector.empty, linearize(Nil, expr), false) + case _ => + (message.tree, Seq.empty) + } + val argsWithError = error.fold(args)(e => args :+ e.tree) + val ctx = TermName(c.freshName("ctx")) + q"""{ + val $ctx = implicitly[_root_.com.github.mvv.zilog.LoggerContext] + if ($ctx.isLevelEnabled($level)) { + ${c.prefix}.log($level, $format, ${if (argsWithError.isEmpty) { + q"_root_.scala.collection.immutable.Nil" + } else { + q"_root_.scala.Array[_root_.scala.Any](..$argsWithError)" + }}) + } else { + _root_.zio.ZIO.unit + } + }""" + } +} diff --git a/src/main/scala/com/github/mvv/zilog/package.scala b/src/main/scala/com/github/mvv/zilog/package.scala new file mode 100644 index 0000000..79dde44 --- /dev/null +++ b/src/main/scala/com/github/mvv/zilog/package.scala @@ -0,0 +1,14 @@ +package com.github.mvv + +import zio.ZIO + +package object zilog extends Logger.Service[Logger] with ImplicitArgsLogger.Service[ImplicitArgsLogger] { + override def log(level: Level, format: String, args: Array[Any])( + implicit ctx: LoggerContext + ): ZIO[Logger, Nothing, Unit] = + ZIO.accessM[Logger](_.logger.log(level, format, args)) + override def withImplicitLogArgs[R1 <: ImplicitArgsLogger, E, A]( + args: (String, Any)* + )(zio: ZIO[R1, E, A]): ZIO[R1, E, A] = + ZIO.accessM[R1](_.logger.withImplicitLogArgs(args: _*)(zio)) +} diff --git a/src/test/scala/com/github/mvv/zilog/test/LoggerSpec.scala b/src/test/scala/com/github/mvv/zilog/test/LoggerSpec.scala new file mode 100644 index 0000000..3922d04 --- /dev/null +++ b/src/test/scala/com/github/mvv/zilog/test/LoggerSpec.scala @@ -0,0 +1,43 @@ +package com.github.mvv.zilog.test + +import com.github.mvv.zilog +import com.github.mvv.zilog.{Level, Logger, LoggerContext} +import org.specs2.mutable.Specification +import zio.{DefaultRuntime, ZIO} + +class LoggerSpec extends Specification with DefaultRuntime { + "Logger" >> { + "should log" >> { + val logger = new TestLogger(Level.Debug) + unsafeRunSync { + implicit val ctx = new LoggerContext(logger) + ZIO.provide(Logger.Default) { + zilog.info(s"Start ${1 + 2}${8} and ${"bar"} end") + } + } + logger.entries must containTheSameElementsAs( + Seq( + (Level.Info, "Start {}{} and {} end", List(3, 8, "bar")) + )) + } + + "should log errors" >> { + val e = new RuntimeException + val logger = new TestLogger(Level.Debug) + unsafeRunSync { + implicit val ctx = new LoggerContext(logger) + ZIO.provide(Logger.Default) { + zilog.error(e, s"${1} plus ${2}") + } + } + logger.entries must containTheSameElementsAs( + Seq( + (Level.Error, "{} plus {}", List(1, 2, e)) + )) + } + } +} + +object LoggerSpec { + final val Const: String = "const" +} diff --git a/src/test/scala/com/github/mvv/zilog/test/TestLogger.scala b/src/test/scala/com/github/mvv/zilog/test/TestLogger.scala new file mode 100644 index 0000000..a1c4fd0 --- /dev/null +++ b/src/test/scala/com/github/mvv/zilog/test/TestLogger.scala @@ -0,0 +1,81 @@ +package com.github.mvv.zilog.test + +import com.github.mvv.zilog.Level +import org.slf4j.Marker + +class TestLogger(minLevel: Level) extends org.slf4j.Logger { + import Ordering.Implicits._ + + private var entriesVar = Vector.empty[(Level, String, List[Any])] + + override def getName: String = ??? + + override def isErrorEnabled: Boolean = minLevel <= Level.Error + override def isErrorEnabled(marker: Marker): Boolean = ??? + override def error(format: String, arg1: Any, arg2: Any): Unit = log(Level.Error, format, arg1, arg2) + override def error(format: String, arg: Any): Unit = log(Level.Error, format, arg) + override def error(format: String, arguments: AnyRef*): Unit = log(Level.Error, format, arguments: _*) + override def error(msg: String): Unit = log(Level.Error, msg) + override def error(msg: String, t: Throwable): Unit = log(Level.Error, msg, t) + override def error(marker: Marker, format: String, arg1: Any, arg2: Any): Unit = ??? + override def error(marker: Marker, format: String, arg: Any): Unit = ??? + override def error(marker: Marker, format: String, arguments: AnyRef*): Unit = ??? + override def error(marker: Marker, msg: String): Unit = ??? + override def error(marker: Marker, msg: String, t: Throwable): Unit = ??? + + override def isWarnEnabled: Boolean = minLevel <= Level.Warn + override def isWarnEnabled(marker: Marker): Boolean = ??? + override def warn(format: String, arg1: Any, arg2: Any): Unit = log(Level.Warn, format, arg1, arg2) + override def warn(format: String, arg: Any): Unit = log(Level.Warn, format, arg) + override def warn(format: String, arguments: AnyRef*): Unit = log(Level.Warn, format, arguments: _*) + override def warn(msg: String): Unit = log(Level.Warn, msg) + override def warn(msg: String, t: Throwable): Unit = log(Level.Warn, msg, t) + override def warn(marker: Marker, format: String, arg1: Any, arg2: Any): Unit = ??? + override def warn(marker: Marker, format: String, arg: Any): Unit = ??? + override def warn(marker: Marker, format: String, arguments: AnyRef*): Unit = ??? + override def warn(marker: Marker, msg: String): Unit = ??? + override def warn(marker: Marker, msg: String, t: Throwable): Unit = ??? + + override def isInfoEnabled: Boolean = minLevel <= Level.Info + override def isInfoEnabled(marker: Marker): Boolean = ??? + override def info(format: String, arg1: Any, arg2: Any): Unit = log(Level.Info, format, arg1, arg2) + override def info(format: String, arg: Any): Unit = log(Level.Info, format, arg) + override def info(format: String, arguments: AnyRef*): Unit = log(Level.Info, format, arguments: _*) + override def info(msg: String): Unit = log(Level.Info, msg) + override def info(msg: String, t: Throwable): Unit = log(Level.Info, msg, t) + override def info(marker: Marker, format: String, arg1: Any, arg2: Any): Unit = ??? + override def info(marker: Marker, format: String, arg: Any): Unit = ??? + override def info(marker: Marker, format: String, arguments: AnyRef*): Unit = ??? + override def info(marker: Marker, msg: String): Unit = ??? + override def info(marker: Marker, msg: String, t: Throwable): Unit = ??? + + override def isDebugEnabled: Boolean = minLevel <= Level.Debug + override def isDebugEnabled(marker: Marker): Boolean = ??? + override def debug(format: String, arg1: Any, arg2: Any): Unit = log(Level.Debug, format, arg1, arg2) + override def debug(format: String, arg: Any): Unit = log(Level.Debug, format, arg) + override def debug(format: String, arguments: AnyRef*): Unit = log(Level.Debug, format, arguments: _*) + override def debug(msg: String): Unit = log(Level.Debug, msg) + override def debug(msg: String, t: Throwable): Unit = log(Level.Debug, msg, t) + override def debug(marker: Marker, format: String, arg1: Any, arg2: Any): Unit = ??? + override def debug(marker: Marker, format: String, arg: Any): Unit = ??? + override def debug(marker: Marker, format: String, arguments: AnyRef*): Unit = ??? + override def debug(marker: Marker, msg: String): Unit = ??? + override def debug(marker: Marker, msg: String, t: Throwable): Unit = ??? + + override def isTraceEnabled: Boolean = minLevel <= Level.Trace + override def isTraceEnabled(marker: Marker): Boolean = ??? + override def trace(format: String, arg1: Any, arg2: Any): Unit = log(Level.Trace, format, arg1, arg2) + override def trace(format: String, arg: Any): Unit = log(Level.Trace, format, arg) + override def trace(format: String, arguments: AnyRef*): Unit = log(Level.Trace, format, arguments: _*) + override def trace(msg: String): Unit = log(Level.Trace, msg) + override def trace(msg: String, t: Throwable): Unit = log(Level.Trace, msg, t) + override def trace(marker: Marker, format: String, arg1: Any, arg2: Any): Unit = ??? + override def trace(marker: Marker, format: String, arg: Any): Unit = ??? + override def trace(marker: Marker, format: String, arguments: AnyRef*): Unit = ??? + override def trace(marker: Marker, msg: String): Unit = ??? + override def trace(marker: Marker, msg: String, t: Throwable): Unit = ??? + + private def log(level: Level, format: String, args: Any*): Unit = + entriesVar :+= (level, format, args.toList) + def entries: Seq[(Level, String, List[Any])] = entriesVar +}