Skip to content

Adds a StructuredLogger based on cats.mtl.Ask #900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1")

val catsV = "2.11.0"
val catsEffectV = "3.5.7"
val catsMtlV = "1.4.0"
val slf4jV = "1.7.36"
val munitCatsEffectV = "2.0.0"
val logbackClassicV = "1.2.13"
Expand All @@ -47,7 +48,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
name := "log4cats-core",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV,
"org.typelevel" %%% "cats-effect-std" % catsEffectV
"org.typelevel" %%% "cats-effect-std" % catsEffectV,
"org.typelevel" %%% "cats-mtl" % catsMtlV
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dependency will be part of cats-effect-3.6, so I'm not feeling guilty for introducing it to core. Otherwise, this could be a log4cats-mtl module.

),
libraryDependencies ++= {
if (tlIsScala3.value) Seq.empty
Expand Down
109 changes: 109 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
inputs = {
typelevel-nix.url = "github:typelevel/typelevel-nix";
nixpkgs.follows = "typelevel-nix/nixpkgs";
flake-utils.follows = "typelevel-nix/flake-utils";
};

outputs = { self, nixpkgs, flake-utils, typelevel-nix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ typelevel-nix.overlays.default ];
};
in {
devShell = pkgs.devshell.mkShell {
imports = [ typelevel-nix.typelevelShell ];
name = "log4cats";
typelevelShell = {
jdk.package = pkgs.jdk8;
native.enable = true;
nodejs.enable = true;
};
};
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.typelevel.log4cats
package slf4j

import cats.Applicative
import cats.effect.Sync
import org.typelevel.log4cats.slf4j.internal.Slf4jLoggerInternal
import org.slf4j.Logger as JLogger
Expand All @@ -33,10 +34,10 @@ object Slf4jLogger extends Slf4jLoggerCompat {
getLoggerFromSlf4j[F](org.slf4j.LoggerFactory.getLogger(clazz))

def getLoggerFromSlf4j[F[_]: Sync](logger: JLogger): SelfAwareStructuredLogger[F] =
new Slf4jLoggerInternal.Slf4jLogger(logger, Sync.Type.Delay)
new Slf4jLoggerInternal.Slf4jLogger(logger, Sync.Type.Delay, Applicative[F].pure(Map.empty))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and line 40) wire nothing in yet. We'd need a parallel set of Ask or Local based constructors here.


def getLoggerFromBlockingSlf4j[F[_]: Sync](logger: JLogger): SelfAwareStructuredLogger[F] =
new Slf4jLoggerInternal.Slf4jLogger(logger, Sync.Type.Blocking)
new Slf4jLoggerInternal.Slf4jLogger(logger, Sync.Type.Blocking, Applicative[F].pure(Map.empty))

def create[F[_]: Sync](implicit name: LoggerName): F[SelfAwareStructuredLogger[F]] =
Sync[F].delay(getLoggerFromName(name.value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,35 @@ private[slf4j] object Slf4jLoggerInternal {
def apply(t: Throwable)(msg: => String): F[Unit]
}

// Need this to make sure MDC is correctly cleared before logging
private[this] def noContextLog[F[_]](isEnabled: F[Boolean], logging: () => Unit)(implicit
F: Sync[F]
): F[Unit] =
contextLog[F](isEnabled, Map.empty, logging)

private[this] def contextLog[F[_]](
isEnabled: F[Boolean],
ctx: Map[String, String],
ctxF: F[Map[String, String]],
logging: () => Unit
)(implicit F: Sync[F]): F[Unit] = {

val ifEnabled = F.delay {
val backup =
try MDC.getCopyOfContextMap()
catch {
case e: IllegalStateException =>
// MDCAdapter is missing, no point in doing anything with
// the MDC, so just hope the logging backend can salvage
// something.
logging()
throw e
}

try {
// Once 2.12 is no longer supported, change this to MDC.setContextMap(ctx.asJava)
MDC.clear()
ctx.foreach { case (k, v) => MDC.put(k, v) }
logging()
} finally
if (backup eq null) MDC.clear()
else MDC.setContextMap(backup)
}
val ifEnabled = ctxF.flatMap(ctx =>
F.delay {
val backup =
try MDC.getCopyOfContextMap()
catch {
case e: IllegalStateException =>
// MDCAdapter is missing, no point in doing anything with
// the MDC, so just hope the logging backend can salvage
// something.
logging()
throw e
}

try {
// Once 2.12 is no longer supported, change this to MDC.setContextMap(ctx.asJava)
MDC.clear()
ctx.foreach { case (k, v) => MDC.put(k, v) }
logging()
} finally
if (backup eq null) MDC.clear()
else MDC.setContextMap(backup)
}
)

isEnabled.ifM(
ifEnabled,
Expand All @@ -77,14 +73,19 @@ private[slf4j] object Slf4jLoggerInternal {
}

@nowarn("msg=used")
final class Slf4jLogger[F[_]](val logger: JLogger, sync: Sync.Type = Sync.Type.Delay)(implicit
F: Sync[F]
) extends SelfAwareStructuredLogger[F] {
final class Slf4jLogger[F[_]](
val logger: JLogger,
sync: Sync.Type = Sync.Type.Delay,
defaultCtx: F[Map[String, String]]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By getting the default context into the logger, as an effect, we are now able to avoid the asking and concatenation of the context map when the log is disabled. But this is a bit less conducive to the middleware approach: we don't want addContext to return a ModifiedContextStructuredLogger anymore.

)(implicit F: Sync[F])
extends SelfAwareStructuredLogger[F] {

@deprecated("Use constructor with sync", "2.6.0")
def this(logger: JLogger, sync: Sync.Type)(F: Sync[F]) =
this(logger, Sync.Type.Delay, F.pure(Map.empty))(F)

@deprecated("Use constructor with sync", "2.6.0")
def this(logger: JLogger)(
F: Sync[F]
) =
def this(logger: JLogger)(F: Sync[F]) =
this(logger, Sync.Type.Delay)(F)

override def isTraceEnabled: F[Boolean] = F.delay(logger.isTraceEnabled)
Expand All @@ -94,48 +95,48 @@ private[slf4j] object Slf4jLoggerInternal {
override def isErrorEnabled: F[Boolean] = F.delay(logger.isErrorEnabled)

override def trace(t: Throwable)(msg: => String): F[Unit] =
noContextLog(isTraceEnabled, () => logger.trace(msg, t))
contextLog(isTraceEnabled, defaultCtx, () => logger.trace(msg, t))
override def trace(msg: => String): F[Unit] =
noContextLog(isTraceEnabled, () => logger.trace(msg))
contextLog(isTraceEnabled, defaultCtx, () => logger.trace(msg))
override def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
contextLog(isTraceEnabled, ctx, () => logger.trace(msg))
contextLog(isTraceEnabled, defaultCtx.map(_ ++ ctx), () => logger.trace(msg))
override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
contextLog(isTraceEnabled, ctx, () => logger.trace(msg, t))
contextLog(isTraceEnabled, defaultCtx.map(_ ++ ctx), () => logger.trace(msg, t))

override def debug(t: Throwable)(msg: => String): F[Unit] =
noContextLog(isDebugEnabled, () => logger.debug(msg, t))
contextLog(isDebugEnabled, defaultCtx, () => logger.debug(msg, t))
override def debug(msg: => String): F[Unit] =
noContextLog(isDebugEnabled, () => logger.debug(msg))
contextLog(isDebugEnabled, defaultCtx, () => logger.debug(msg))
override def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
contextLog(isDebugEnabled, ctx, () => logger.debug(msg))
contextLog(isDebugEnabled, defaultCtx.map(_ ++ ctx), () => logger.debug(msg))
override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
contextLog(isDebugEnabled, ctx, () => logger.debug(msg, t))
contextLog(isDebugEnabled, defaultCtx.map(_ ++ ctx), () => logger.debug(msg, t))

override def info(t: Throwable)(msg: => String): F[Unit] =
noContextLog(isInfoEnabled, () => logger.info(msg, t))
contextLog(isInfoEnabled, defaultCtx, () => logger.info(msg, t))
override def info(msg: => String): F[Unit] =
noContextLog(isInfoEnabled, () => logger.info(msg))
contextLog(isInfoEnabled, defaultCtx, () => logger.info(msg))
override def info(ctx: Map[String, String])(msg: => String): F[Unit] =
contextLog(isInfoEnabled, ctx, () => logger.info(msg))
contextLog(isInfoEnabled, defaultCtx.map(_ ++ ctx), () => logger.info(msg))
override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
contextLog(isInfoEnabled, ctx, () => logger.info(msg, t))
contextLog(isInfoEnabled, defaultCtx.map(_ ++ ctx), () => logger.info(msg, t))

override def warn(t: Throwable)(msg: => String): F[Unit] =
noContextLog(isWarnEnabled, () => logger.warn(msg, t))
contextLog(isWarnEnabled, defaultCtx, () => logger.warn(msg, t))
override def warn(msg: => String): F[Unit] =
noContextLog(isWarnEnabled, () => logger.warn(msg))
contextLog(isWarnEnabled, defaultCtx, () => logger.warn(msg))
override def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
contextLog(isWarnEnabled, ctx, () => logger.warn(msg))
contextLog(isWarnEnabled, defaultCtx.map(_ ++ ctx), () => logger.warn(msg))
override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
contextLog(isWarnEnabled, ctx, () => logger.warn(msg, t))
contextLog(isWarnEnabled, defaultCtx.map(_ ++ ctx), () => logger.warn(msg, t))

override def error(t: Throwable)(msg: => String): F[Unit] =
noContextLog(isErrorEnabled, () => logger.error(msg, t))
contextLog(isErrorEnabled, defaultCtx, () => logger.error(msg, t))
override def error(msg: => String): F[Unit] =
noContextLog(isErrorEnabled, () => logger.error(msg))
contextLog(isErrorEnabled, defaultCtx, () => logger.error(msg))
override def error(ctx: Map[String, String])(msg: => String): F[Unit] =
contextLog(isErrorEnabled, ctx, () => logger.error(msg))
contextLog(isErrorEnabled, defaultCtx.map(_ ++ ctx), () => logger.error(msg))
override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
contextLog(isErrorEnabled, ctx, () => logger.error(msg, t))
contextLog(isErrorEnabled, defaultCtx.map(_ ++ ctx), () => logger.error(msg, t))
}
}