diff --git a/build.sbt b/build.sbt index 01d386c..899401c 100644 --- a/build.sbt +++ b/build.sbt @@ -107,7 +107,7 @@ lazy val http4sServer = "org.http4s" %% "http4s-ember-server" % Versions.http4sVersion, "org.http4s" %% "http4s-circe" % Versions.http4sVersion, "org.http4s" %% "http4s-dsl" % Versions.http4sVersion, - "org.typelevel" %% "log4cats-slf4j" % Versions.log4CatsVersion, - "ch.qos.logback" % "logback-classic" % Versions.logBackVersion + "com.outr" %% "scribe" % Versions.scribeVersion, + "com.outr" %% "scribe-cats" % Versions.scribeVersion ) } diff --git a/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/ServerMain.scala b/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/ServerMain.scala index 15504f9..9cbde43 100644 --- a/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/ServerMain.scala +++ b/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/ServerMain.scala @@ -18,7 +18,7 @@ import org.http4s.ember.server.EmberServerBuilder import org.http4s.implicits.* import org.http4s.server.websocket.WebSocketBuilder import org.typelevel.log4cats.LoggerFactory -import org.typelevel.log4cats.slf4j.Slf4jFactory + object ServerMain extends IOApp { @@ -44,7 +44,7 @@ object ServerMain extends IOApp { def run(args: List[String]): IO[ExitCode] = // TODO: configurable rules - make this configurable given rules: Rules = Rules(DeckRule.WithNines) - given LoggerFactory[IO] = Slf4jFactory.create + given LoggerFactory[IO] = utils.logging.ScribeLogger.factory for { queue <- Queue.unbounded[IO, IncomingAction[ClientId]] topic <- Topic[IO, Map[ClientId, Seq[MessageToClient]]] diff --git a/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/utils/logging/ScribeLogger.scala b/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/utils/logging/ScribeLogger.scala new file mode 100644 index 0000000..20faa28 --- /dev/null +++ b/http4s-server/src/main/scala/io/github/mahh/doko/http4sserver/utils/logging/ScribeLogger.scala @@ -0,0 +1,135 @@ +package io.github.mahh.doko.http4sserver.utils.logging + +import cats.syntax.flatMap.catsSyntaxIfM +import cats.effect.Sync +import org.typelevel.log4cats +import org.typelevel.log4cats.LoggerName +import scribe.Level +import scribe.Scribe +import scribe.cats.* +import scribe.format.* +import scribe.mdc.MDC +import scribe.mdc.MDCValue + +class ScribeLogger[F[_]: Sync](private val scribeLogger: scribe.Logger) + extends log4cats.SelfAwareStructuredLogger[F] { + + private val scribe: Scribe[F] = scribeLogger.f[F] + + def error(message: => String): F[Unit] = scribe.error(message) + + def warn(message: => String): F[Unit] = scribe.warn(message) + + def info(message: => String): F[Unit] = scribe.info(message) + + def debug(message: => String): F[Unit] = scribe.debug(message) + + def trace(message: => String): F[Unit] = scribe.trace(message) + + def error(t: Throwable)(message: => String): F[Unit] = scribe.error(message, t) + + def warn(t: Throwable)(message: => String): F[Unit] = scribe.warn(message, t) + + def info(t: Throwable)(message: => String): F[Unit] = scribe.info(message, t) + + def debug(t: Throwable)(message: => String): F[Unit] = scribe.debug(message, t) + + def trace(t: Throwable)(message: => String): F[Unit] = scribe.trace(message, t) + + // starting here: SelfAwareLogger implementation + // (not really used by http4s, but required by LoggerFactory type signature) + + private def isLevelEnabled(level: Level): F[Boolean] = + Sync[F].delay(scribeLogger.includes(level)) + + def isTraceEnabled: F[Boolean] = isLevelEnabled(Level.Trace) + + def isDebugEnabled: F[Boolean] = isLevelEnabled(Level.Debug) + + def isInfoEnabled: F[Boolean] = isLevelEnabled(Level.Info) + + def isWarnEnabled: F[Boolean] = isLevelEnabled(Level.Warn) + + def isErrorEnabled: F[Boolean] = isLevelEnabled(Level.Error) + + // starting here: StructuredLogger implementation (not really used by http4s) + // (not really used by http4s, but required by LoggerFactory type signature) + + private[this] def contextLog( + isEnabled: F[Boolean], + ctx: Map[String, String], + logging: () => Unit + ): F[Unit] = { + val ifEnabled = Sync[F].delay { + MDC.context(ctx.map { (k, v) => k -> MDCValue(() => v) }.toSeq*) { + logging() + } + } + + isEnabled.ifM( + ifEnabled, + Sync[F].unit + ) + } + + def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + contextLog(isTraceEnabled, ctx, () => scribeLogger.trace(msg)) + + def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + contextLog(isTraceEnabled, ctx, () => scribeLogger.trace(msg, t)) + + def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + contextLog(isDebugEnabled, ctx, () => scribeLogger.debug(msg)) + + def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + contextLog(isDebugEnabled, ctx, () => scribeLogger.debug(msg, t)) + + def info(ctx: Map[String, String])(msg: => String): F[Unit] = + contextLog(isInfoEnabled, ctx, () => scribeLogger.info(msg)) + + def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + contextLog(isInfoEnabled, ctx, () => scribeLogger.info(msg, t)) + + def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + contextLog(isWarnEnabled, ctx, () => scribeLogger.warn(msg)) + + def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + contextLog(isWarnEnabled, ctx, () => scribeLogger.warn(msg, t)) + + def error(ctx: Map[String, String])(msg: => String): F[Unit] = + contextLog(isErrorEnabled, ctx, () => scribeLogger.error(msg)) + + def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + contextLog(isErrorEnabled, ctx, () => scribeLogger.error(msg, t)) +} + +object ScribeLogger: + + // do not use default formatter because it tries to print source location which is useless + // because the ScribeLogger wrapper cannot forward the proper sourcecode implicits + private val formatter: Formatter = + formatter"[$date $threadName $level] $messages" + + // install the formatter globally (this is ugly, but so far, I did not find a better + // way + scribe.Logger.root + .clearHandlers() + .withHandler(formatter = formatter) + .replace() + + def getLogger[F[_]]( + implicit f: Sync[F], + name: LoggerName + ): log4cats.SelfAwareStructuredLogger[F] = + new ScribeLogger[F](scribe.Logger(name.value)) + + trait LoggerFactory[F[_]: Sync] extends log4cats.LoggerFactory[F]: + + def getLoggerFromName(name: String): LoggerType = + given LoggerName = LoggerName(name) + ScribeLogger.getLogger + + def fromName(name: String): F[LoggerType] = + Sync[F].delay(getLoggerFromName(name)) + + def factory[F[_]: Sync]: LoggerFactory[F] = new LoggerFactory[F] {} diff --git a/project/Versions.scala b/project/Versions.scala index 88d847b..2dee66e 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -6,7 +6,6 @@ object Versions { val pekkoHttpVersion = "1.1.0" val catsVersion = "2.12.0" val logBackVersion = "1.3.14" - val log4CatsVersion = "2.7.0" val circeVersion = "0.14.10" val scalaJsDomVersion = "2.8.0" val laminarVersion = "17.2.0" @@ -14,5 +13,6 @@ object Versions { val munitVersion = "1.0.3" val munitScalacheckVersion = "1.0.0" val scalacheckDerivedVersion = "0.5.0" + val scribeVersion = "3.15.2" val http4sVersion = "1.0.0-M44" }