Skip to content

Commit

Permalink
refactor: use scribe for logging
Browse files Browse the repository at this point in the history
use scribe for logging (in http4sServer) - one
step to prepare for cross-compiling to scala native
  • Loading branch information
MartinHH committed Dec 27, 2024
1 parent 155a4c4 commit 3ea49a9
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 5 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ 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 {

Expand All @@ -44,7 +43,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]]]
Expand Down
Original file line number Diff line number Diff line change
@@ -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] {}
2 changes: 1 addition & 1 deletion project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ 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"
val laminextVersion = "0.17.1"
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"
}

0 comments on commit 3ea49a9

Please sign in to comment.