diff --git a/build.sbt b/build.sbt index 73ab01e9..14d537fe 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,34 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= { if (tlIsScala3.value) Seq.empty else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided) - } + }, + mimaBinaryIssueFilters ++= Seq( + // Removed implicit conversions for monad transformers from Logger - these were intentionally removed + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.Logger.kleisliLogger"), + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.Logger.eitherTLogger"), + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.Logger.optionTLogger"), + // Removed implicit conversions for monad transformers from LoggerFactory - these were intentionally removed + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.LoggerFactory.kleisliFactory"), + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.LoggerFactory.eitherTFactory"), + ProblemFilters + .exclude[DirectMissingMethodProblem]("org.typelevel.log4cats.LoggerFactory.optionTFactory"), + // Added new kernel methods - these are new API additions + ProblemFilters.exclude[ReversedMissingMethodProblem]("org.typelevel.log4cats.Logger.kernel"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "org.typelevel.log4cats.extras.DeferredLogger.kernel" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "org.typelevel.log4cats.extras.DeferredSelfAwareStructuredLogger.kernel" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "org.typelevel.log4cats.extras.DeferredStructuredLogger.kernel" + ) + ) ) .nativeSettings(commonNativeSettings) diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/ConsoleLoggerKernel.scala b/core/shared/src/main/scala/org/typelevel/log4cats/ConsoleLoggerKernel.scala new file mode 100644 index 00000000..43f3fdc9 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/ConsoleLoggerKernel.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.effect.kernel.Sync + +/** + * A simple console implementation of LoggerKernel for testing the SAM design. + */ +class ConsoleLoggerKernel[F[_], Ctx](implicit F: Sync[F]) extends LoggerKernel[F, Ctx] { + + def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = { + F.delay { + val logRecord = record(Log.mutableBuilder[Ctx]()).build() + + val timestamp = logRecord.timestamp.getOrElse(java.time.Instant.now()) + val timeStr = timestamp.toString + + val levelStr = logRecord.level.namePadded + val message = logRecord.message + val className = logRecord.className.map(c => s"[$c]").getOrElse("") + val fileName = + logRecord.fileName.map(f => s"($f:${logRecord.line.getOrElse(0)})").getOrElse("") + + val contextStr = if (logRecord.context.nonEmpty) { + val contextPairs = logRecord.context.map { case (k, v) => s"$k=$v" }.mkString(", ") + s" {$contextPairs}" + } else "" + + val throwableStr = logRecord.throwable.map(t => s"\n${t.toString}").getOrElse("") + + val logLine = s"$timeStr $levelStr $className$fileName$contextStr $message$throwableStr" + + println(logLine) + } + } +} + +object ConsoleLoggerKernel { + def apply[F[_], Ctx](implicit F: Sync[F]): ConsoleLoggerKernel[F, Ctx] = + new ConsoleLoggerKernel[F, Ctx] +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/Context.scala b/core/shared/src/main/scala/org/typelevel/log4cats/Context.scala new file mode 100644 index 00000000..e92839a0 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/Context.scala @@ -0,0 +1,57 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import scala.concurrent.duration.FiniteDuration + +import org.typelevel.log4cats.Context.Encoder + +/** + * A value that can be written into a json-like construct, provided a visitor. + */ +trait Context[C] { + def capture[A](a: A)(implicit E: Encoder[A, C]): C +} + +object Context { + trait Encoder[A, B] { + def encode(a: A): B + } + + object Encoder { + def apply[A, B](implicit ev: Encoder[A, B]): ev.type = ev + + // Identity encoder for when input and output types are the same + implicit def identityEncoder[A]: Encoder[A, A] = a => a + + implicit val stringToStringEncoder: Encoder[String, String] = a => a + + implicit val intToStringEncoder: Encoder[Int, String] = _.toString + + implicit val longToStringEncoder: Encoder[Long, String] = _.toString + + implicit val doubleToStringEncoder: Encoder[Double, String] = _.toString + + implicit val booleanToStringEncoder: Encoder[Boolean, String] = if (_) "true" else "false" + + // Removed Instant encoder for Scala Native compatibility + // implicit val instantToStringEncoder: Encoder[Instant, String] = + // DateTimeFormatter.ISO_INSTANT.format(_) + + implicit val finiteDurationToStringEncoder: Encoder[FiniteDuration, String] = _.toString + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala b/core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala new file mode 100644 index 00000000..cd715669 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.Order + +final case class KernelLogLevel(name: String, value: Int) { + def namePadded: String = name.padTo(5, ' ').mkString +} + +object KernelLogLevel { + implicit final val orderKernelLogLevel: Order[KernelLogLevel] = + Order.by[KernelLogLevel, Int](-_.value) + + val Trace: KernelLogLevel = KernelLogLevel("TRACE", 100) + val Debug: KernelLogLevel = KernelLogLevel("DEBUG", 200) + val Info: KernelLogLevel = KernelLogLevel("INFO", 300) + val Warn: KernelLogLevel = KernelLogLevel("WARN", 400) + val Error: KernelLogLevel = KernelLogLevel("ERROR", 500) + val Fatal: KernelLogLevel = KernelLogLevel("FATAL", 600) +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/Log.scala b/core/shared/src/main/scala/org/typelevel/log4cats/Log.scala new file mode 100644 index 00000000..68e755ae --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/Log.scala @@ -0,0 +1,193 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import scala.collection.mutable +import scala.concurrent.duration.FiniteDuration + +/** + * Low-level interface exposing methods to enrich a log record with relevant information. The + * methods are designed to capture elements that cannot be easily captured from a monadic context + * (or by running an effect). Elements such as timestamps should be provided by means of + * middlewares. + */ +trait Log[Ctx] { + def timestamp: Option[FiniteDuration] + def level: KernelLogLevel + def message: () => String + def throwable: Option[Throwable] + def context: Map[String, Ctx] + def fileName: Option[String] + def className: Option[String] + def methodName: Option[String] + def line: Option[Int] + def levelValue: Int +} + +object Log { + trait Builder[Ctx] { + def withTimestamp(value: FiniteDuration): Builder[Ctx] + def withLevel(level: KernelLogLevel): Builder[Ctx] + def withMessage(message: => String): Builder[Ctx] + def withThrowable(throwable: Throwable): Builder[Ctx] + def withContext[A](name: String)(ctx: A)(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] + def withFileName(name: String): Builder[Ctx] + def withClassName(name: String): Builder[Ctx] + def withMethodName(name: String): Builder[Ctx] + def withLine(line: Int): Builder[Ctx] + + final def withContextMap[A]( + contextMap: Map[String, A] + )(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] = + contextMap.foldLeft(this) { case (builder, (k, v)) => builder.withContext(k)(v) } + + def adaptTimestamp(f: FiniteDuration => FiniteDuration): Builder[Ctx] + def adaptLevel(f: KernelLogLevel => KernelLogLevel): Builder[Ctx] + def adaptMessage(f: String => String): Builder[Ctx] + def adaptThrowable(f: Throwable => Throwable): Builder[Ctx] + def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): Builder[Ctx] + def adaptFileName(f: String => String): Builder[Ctx] + def adaptClassName(f: String => String): Builder[Ctx] + def adaptMethodName(f: String => String): Builder[Ctx] + def adaptLine(f: Int => Int): Builder[Ctx] + + def build(): Log[Ctx] + } + + def mutableBuilder[Ctx](): Builder[Ctx] = new MutableBuilder[Ctx]() + + private class MutableBuilder[Ctx] extends Builder[Ctx] { + private var _timestamp: Option[FiniteDuration] = None + private var _level: KernelLogLevel = KernelLogLevel.Info + private var _message: () => String = () => "" + private var _throwable: Option[Throwable] = None + private var _context: mutable.Builder[(String, Ctx), Map[String, Ctx]] = + Map.newBuilder[String, Ctx] + private var _fileName: Option[String] = None + private var _className: Option[String] = None + private var _methodName: Option[String] = None + private var _line: Option[Int] = None + + def build(): Log[Ctx] = new Log[Ctx] { + override def timestamp: Option[FiniteDuration] = _timestamp + override def level: KernelLogLevel = _level + override def message: () => String = _message + override def throwable: Option[Throwable] = _throwable + override def context: Map[String, Ctx] = _context.result() + override def className: Option[String] = _className + override def fileName: Option[String] = _fileName + override def methodName: Option[String] = _methodName + override def line: Option[Int] = _line.filter(_ > 0) + override def levelValue: Int = _level.value + } + + override def withTimestamp(value: FiniteDuration): this.type = { + _timestamp = Some(value) + this + } + + override def withLevel(level: KernelLogLevel): this.type = { + _level = level + this + } + + override def withMessage(message: => String): this.type = { + _message = () => message + this + } + + override def adaptMessage(f: String => String): this.type = { + val originalMessage = _message + _message = () => f(originalMessage()) + this + } + + override def adaptTimestamp(f: FiniteDuration => FiniteDuration): this.type = { + _timestamp = _timestamp.map(f) + this + } + + override def adaptLevel(f: KernelLogLevel => KernelLogLevel): this.type = { + _level = f(_level) + this + } + + override def adaptThrowable(f: Throwable => Throwable): this.type = { + _throwable = _throwable.map(f) + this + } + + override def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): this.type = { + val currentContext = _context.result() + _context = Map.newBuilder[String, Ctx] + f(currentContext).foreach { case (k, v) => _context += (k -> v) } + this + } + + override def adaptFileName(f: String => String): this.type = { + _fileName = _fileName.map(f) + this + } + + override def adaptClassName(f: String => String): this.type = { + _className = _className.map(f) + this + } + + override def adaptMethodName(f: String => String): this.type = { + _methodName = _methodName.map(f) + this + } + + override def adaptLine(f: Int => Int): this.type = { + _line = _line.map(f) + this + } + + override def withThrowable(throwable: Throwable): this.type = { + _throwable = Some(throwable) + this + } + + override def withContext[A]( + name: String + )(ctx: A)(implicit E: Context.Encoder[A, Ctx]): this.type = { + _context += (name -> E.encode(ctx)) + this + } + + override def withFileName(name: String): this.type = { + _fileName = Some(name) + this + } + + override def withClassName(name: String): this.type = { + _className = Some(name) + this + } + + override def withMethodName(name: String): this.type = { + _methodName = Some(name) + this + } + + override def withLine(line: Int): this.type = { + _line = if (line > 0) Some(line) else None + this + } + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/Logger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/Logger.scala index 78a84258..fbe8ded9 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/Logger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/Logger.scala @@ -17,9 +17,45 @@ package org.typelevel.log4cats import cats.* -import cats.data.{EitherT, Kleisli, OptionT} trait Logger[F[_]] extends MessageLogger[F] with ErrorLogger[F] { + protected def kernel: LoggerKernel[F, String] + + /** Access to the underlying kernel for advanced use cases */ + def underlying: LoggerKernel[F, String] = kernel + + // MessageLogger methods + def error(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Error, _.withMessage(message)) + + def warn(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Warn, _.withMessage(message)) + + def info(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Info, _.withMessage(message)) + + def debug(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Debug, _.withMessage(message)) + + def trace(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Trace, _.withMessage(message)) + + // ErrorLogger methods + def error(t: Throwable)(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Error, _.withMessage(message).withThrowable(t)) + + def warn(t: Throwable)(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Warn, _.withMessage(message).withThrowable(t)) + + def info(t: Throwable)(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Info, _.withMessage(message).withThrowable(t)) + + def debug(t: Throwable)(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Debug, _.withMessage(message).withThrowable(t)) + + def trace(t: Throwable)(message: => String): F[Unit] = + kernel.log(KernelLogLevel.Trace, _.withMessage(message).withThrowable(t)) + def withModifiedString(f: String => String): Logger[F] = Logger.withModifiedString[F](this, f) override def mapK[G[_]](fk: F ~> G): Logger[G] = Logger.mapK(fk)(this) } @@ -27,51 +63,23 @@ trait Logger[F[_]] extends MessageLogger[F] with ErrorLogger[F] { object Logger { def apply[F[_]](implicit ev: Logger[F]) = ev - implicit def optionTLogger[F[_]: Logger: Functor]: Logger[OptionT[F, *]] = - Logger[F].mapK(OptionT.liftK[F]) - - implicit def eitherTLogger[F[_]: Logger: Functor, E]: Logger[EitherT[F, E, *]] = - Logger[F].mapK(EitherT.liftK[F, E]) - - implicit def kleisliLogger[F[_]: Logger, A]: Logger[Kleisli[F, A, *]] = - Logger[F].mapK(Kleisli.liftK[F, A]) - private def withModifiedString[F[_]](l: Logger[F], f: String => String): Logger[F] = new Logger[F] { - def error(message: => String): F[Unit] = l.error(f(message)) - def error(t: Throwable)(message: => String): F[Unit] = l.error(t)(f(message)) - def warn(message: => String): F[Unit] = l.warn(f(message)) - def warn(t: Throwable)(message: => String): F[Unit] = l.warn(t)(f(message)) - def info(message: => String): F[Unit] = l.info(f(message)) - def info(t: Throwable)(message: => String): F[Unit] = l.info(t)(f(message)) - def debug(message: => String): F[Unit] = l.debug(f(message)) - def debug(t: Throwable)(message: => String): F[Unit] = l.debug(t)(f(message)) - def trace(message: => String): F[Unit] = l.trace(f(message)) - def trace(t: Throwable)(message: => String): F[Unit] = l.trace(t)(f(message)) + protected def kernel: LoggerKernel[F, String] = l.underlying + override def error(message: => String): F[Unit] = l.error(f(message)) + override def error(t: Throwable)(message: => String): F[Unit] = l.error(t)(f(message)) + override def warn(message: => String): F[Unit] = l.warn(f(message)) + override def warn(t: Throwable)(message: => String): F[Unit] = l.warn(t)(f(message)) + override def info(message: => String): F[Unit] = l.info(f(message)) + override def info(t: Throwable)(message: => String): F[Unit] = l.info(t)(f(message)) + override def debug(message: => String): F[Unit] = l.debug(f(message)) + override def debug(t: Throwable)(message: => String): F[Unit] = l.debug(t)(f(message)) + override def trace(message: => String): F[Unit] = l.trace(f(message)) + override def trace(t: Throwable)(message: => String): F[Unit] = l.trace(t)(f(message)) } private def mapK[G[_], F[_]](f: G ~> F)(logger: Logger[G]): Logger[F] = new Logger[F] { - def error(t: Throwable)(message: => String): F[Unit] = - f(logger.error(t)(message)) - def warn(t: Throwable)(message: => String): F[Unit] = - f(logger.warn(t)(message)) - def info(t: Throwable)(message: => String): F[Unit] = - f(logger.info(t)(message)) - def debug(t: Throwable)(message: => String): F[Unit] = - f(logger.debug(t)(message)) - def trace(t: Throwable)(message: => String): F[Unit] = - f(logger.trace(t)(message)) - def error(message: => String): F[Unit] = - f(logger.error(message)) - def warn(message: => String): F[Unit] = - f(logger.warn(message)) - def info(message: => String): F[Unit] = - f(logger.info(message)) - def debug(message: => String): F[Unit] = - f(logger.debug(message)) - def trace(message: => String): F[Unit] = - f(logger.trace(message)) + protected def kernel: LoggerKernel[F, String] = logger.underlying.mapK(f) } - } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala index f8d3ad94..afe8390a 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala @@ -18,11 +18,8 @@ package org.typelevel.log4cats import cats.Functor import cats.Show.Shown -import cats.data.Kleisli import cats.syntax.functor.* import cats.~> -import cats.data.OptionT -import cats.data.EitherT import scala.annotation.implicitNotFound @@ -49,15 +46,6 @@ trait LoggerFactory[F[_]] extends LoggerFactoryGen[F] { object LoggerFactory extends LoggerFactoryGenCompanion { def apply[F[_]: LoggerFactory]: LoggerFactory[F] = implicitly - implicit def optionTFactory[F[_]: LoggerFactory: Functor]: LoggerFactory[OptionT[F, *]] = - LoggerFactory[F].mapK(OptionT.liftK[F]) - - implicit def eitherTFactory[F[_]: LoggerFactory: Functor, E]: LoggerFactory[EitherT[F, E, *]] = - LoggerFactory[F].mapK(EitherT.liftK[F, E]) - - implicit def kleisliFactory[F[_]: LoggerFactory: Functor, A]: LoggerFactory[Kleisli[F, A, *]] = - LoggerFactory[F].mapK(Kleisli.liftK[F, A]) - private def mapK[F[_]: Functor, G[_]](fk: F ~> G)(lf: LoggerFactory[F]): LoggerFactory[G] = new LoggerFactory[G] { diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LoggerKernel.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerKernel.scala new file mode 100644 index 00000000..286758ed --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerKernel.scala @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.~> + +/** + * This is the fundamental abstraction: a single-abstract-method interface that has the following + * properties: + * + * - Doesn't enforce a specific memory layout. More specific interfaces can back `LogRecord` with + * mutables/immutables values, and avoid storing things that are not important. + * - LogRecord allows to precisely capture a lot of information. In particular, it does not + * enforce a `Map[String, String]` representation of context values that is not sufficient to + * leverage all the power from logging backends query engines, and without pulling a third-party + * JSON library. + * - The SAM-like nature of the construct makes it inherently middleware friendly, as a single + * method call needs to be intercepted/proxied in order to amend the behaviour of the logger. + * + * This also means that different libraries can use wrappers on top of this kernel interface to use + * whatever UX is preferred without necessarily imposing constraints on the underlying + * implementation. + */ +trait LoggerKernel[F[_], Ctx] { + def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] + + final def logTrace(record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + log(KernelLogLevel.Trace, record) + final def logDebug(record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + log(KernelLogLevel.Debug, record) + final def logInfo(record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + log(KernelLogLevel.Info, record) + final def logWarn(record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + log(KernelLogLevel.Warn, record) + final def logError(record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + log(KernelLogLevel.Error, record) + + /** + * Transform the effect type using a natural transformation. This allows converting between + * different effect types (e.g., IO to Task, Task to IO). + */ + def mapK[G[_]](f: F ~> G): LoggerKernel[G, Ctx] = new LoggerKernel[G, Ctx] { + def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): G[Unit] = + f(LoggerKernel.this.log(level, record)) + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/PagingSelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/PagingSelfAwareStructuredLogger.scala index 432514d2..0464cb1b 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/PagingSelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/PagingSelfAwareStructuredLogger.scala @@ -76,6 +76,8 @@ object PagingSelfAwareStructuredLogger { s"pageSizeK(=$pageSizeK) and maxPageNeeded(=$maxPageNeeded) must be positive numbers." ) + protected def kernel: LoggerKernel[F, String] = sl.underlying + @deprecated("Use constructor with randomUUID", "2.5.0") def this(pageSizeK: Int, maxPageNeeded: Int)( sl: SelfAwareStructuredLogger[F] diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SamLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SamLogger.scala new file mode 100644 index 00000000..5c4d6024 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SamLogger.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.* + +/** + * A SAM-based Logger that provides a user-friendly interface. This is the new design that will + * eventually replace the current Logger trait. + */ +trait SamLogger[F[_], Ctx] { + protected def kernel: LoggerKernel[F, Ctx] + + /** Access to the underlying kernel for advanced use cases */ + def underlying: LoggerKernel[F, Ctx] = kernel + + final def info(message: => String): F[Unit] = log_(KernelLogLevel.Info, message) + + final def warn(message: => String): F[Unit] = log_(KernelLogLevel.Warn, message) + + final def error(message: => String): F[Unit] = log_(KernelLogLevel.Error, message) + + final def trace(message: => String): F[Unit] = log_(KernelLogLevel.Trace, message) + + final def debug(message: => String): F[Unit] = log_(KernelLogLevel.Debug, message) + + private final def log_( + level: KernelLogLevel, + message: => String + ): F[Unit] = { + kernel.log( + level, + (record: Log.Builder[Ctx]) => + record + .withLevel(level) + .withMessage(message) + ) + } + + def withModifiedString(f: String => String): SamLogger[F, Ctx] = + SamLogger.withModifiedString[F, Ctx](this, f) + def mapK[G[_]](fk: F ~> G): SamLogger[G, Ctx] = SamLogger.mapK(fk)(this) +} + +object SamLogger { + def apply[F[_], Ctx](implicit ev: SamLogger[F, Ctx]) = ev + + def wrap[F[_], Ctx](k: LoggerKernel[F, Ctx]): SamLogger[F, Ctx] = new SamLogger[F, Ctx] { + protected def kernel: LoggerKernel[F, Ctx] = k + } + + private def withModifiedString[F[_], Ctx]( + l: SamLogger[F, Ctx], + f: String => String + ): SamLogger[F, Ctx] = + new SamLogger[F, Ctx] { + protected def kernel: LoggerKernel[F, Ctx] = new LoggerKernel[F, Ctx] { + def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + l.underlying.log(level, record(_).adaptMessage(f)) + } + } + + private def mapK[G[_], F[_], Ctx](f: G ~> F)(logger: SamLogger[G, Ctx]): SamLogger[F, Ctx] = + new SamLogger[F, Ctx] { + protected def kernel: LoggerKernel[F, Ctx] = new LoggerKernel[F, Ctx] { + def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + f(logger.underlying.log(level, record)) + } + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SamStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SamStructuredLogger.scala new file mode 100644 index 00000000..804e2071 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SamStructuredLogger.scala @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.* +import cats.Show.Shown + +/** + * A SAM-based StructuredLogger that extends Logger and provides structured logging capabilities. + * This implementation uses the new SAM LoggerKernel design for better performance and middleware + * compatibility. + */ +trait SamStructuredLogger[F[_]] extends Logger[F] { + protected def kernel: LoggerKernel[F, String] + + final def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.logTrace(_.withMessage(msg).withContextMap(ctx)) + + final def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.logTrace(_.withMessage(msg).withThrowable(t).withContextMap(ctx)) + + final def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.logDebug(_.withMessage(msg).withContextMap(ctx)) + + final def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.logDebug(_.withMessage(msg).withThrowable(t).withContextMap(ctx)) + + final def info(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.logInfo(_.withMessage(msg).withContextMap(ctx)) + + final def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.logInfo(_.withMessage(msg).withThrowable(t).withContextMap(ctx)) + + final def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.logWarn(_.withMessage(msg).withContextMap(ctx)) + + final def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.logWarn(_.withMessage(msg).withThrowable(t).withContextMap(ctx)) + + final def error(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.logError(_.withMessage(msg).withContextMap(ctx)) + + final def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.logError(_.withMessage(msg).withThrowable(t).withContextMap(ctx)) + + final override def trace(message: => String): F[Unit] = + kernel.logTrace(_.withMessage(message)) + + final override def trace(t: Throwable)(message: => String): F[Unit] = + kernel.logTrace(_.withMessage(message).withThrowable(t)) + + final override def debug(message: => String): F[Unit] = + kernel.logDebug(_.withMessage(message)) + + final override def debug(t: Throwable)(message: => String): F[Unit] = + kernel.logDebug(_.withMessage(message).withThrowable(t)) + + final override def info(message: => String): F[Unit] = + kernel.logInfo(_.withMessage(message)) + + final override def info(t: Throwable)(message: => String): F[Unit] = + kernel.logInfo(_.withMessage(message).withThrowable(t)) + + final override def warn(message: => String): F[Unit] = + kernel.logWarn(_.withMessage(message)) + + final override def warn(t: Throwable)(message: => String): F[Unit] = + kernel.logWarn(_.withMessage(message).withThrowable(t)) + + final override def error(message: => String): F[Unit] = + kernel.logError(_.withMessage(message)) + + final override def error(t: Throwable)(message: => String): F[Unit] = + kernel.logError(_.withMessage(message).withThrowable(t)) + + def addContext(ctx: Map[String, String]): SamStructuredLogger[F] = + SamStructuredLogger.withContext(this)(ctx) + + def addContext(pairs: (String, Shown)*): SamStructuredLogger[F] = + SamStructuredLogger.withContext(this)( + pairs.map { case (k, v) => (k, v.toString) }.toMap + ) + + override def withModifiedString(f: String => String): SamStructuredLogger[F] = + SamStructuredLogger.withModifiedString[F](this, f) + + override def mapK[G[_]](fk: F ~> G): SamStructuredLogger[G] = + SamStructuredLogger.mapK(fk)(this) +} + +object SamStructuredLogger { + def apply[F[_]](implicit ev: SamStructuredLogger[F]): SamStructuredLogger[F] = ev + + def fromKernel[F[_]](kernelImpl: LoggerKernel[F, String]): SamStructuredLogger[F] = + new SamStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = kernelImpl + } + + def withContext[F[_]](sl: SamStructuredLogger[F])( + ctx: Map[String, String] + ): SamStructuredLogger[F] = withModifiedContext[F](sl)(ctx ++ _) + + def withModifiedContext[F[_]]( + sl: SamStructuredLogger[F] + )(modifyCtx: Map[String, String] => Map[String, String]): SamStructuredLogger[F] = + new SamStructuredLogger[F] { + protected val kernel: LoggerKernel[F, String] = + (level, record) => sl.kernel.log(level, record(_).adaptContext(modifyCtx)) + } + + private def withModifiedString[F[_]]( + l: SamStructuredLogger[F], + f: String => String + ): SamStructuredLogger[F] = + new SamStructuredLogger[F] { + protected val kernel: LoggerKernel[F, String] = + (level, record) => l.kernel.log(level, record(_).adaptMessage(f)) + } + + private def mapK[G[_], F[_]](f: G ~> F)(logger: SamStructuredLogger[G]): SamStructuredLogger[F] = + new SamStructuredLogger[F] { + protected val kernel: LoggerKernel[F, String] = logger.kernel.mapK(f) + } + +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareLogger.scala index e609e1eb..5462b666 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareLogger.scala @@ -34,37 +34,13 @@ object SelfAwareLogger { private def mapK[G[_], F[_]](f: G ~> F)(logger: SelfAwareLogger[G]): SelfAwareLogger[F] = new SelfAwareLogger[F] { - def isTraceEnabled: F[Boolean] = - f(logger.isTraceEnabled) - def isDebugEnabled: F[Boolean] = - f(logger.isDebugEnabled) - def isInfoEnabled: F[Boolean] = - f(logger.isInfoEnabled) - def isWarnEnabled: F[Boolean] = - f(logger.isWarnEnabled) - def isErrorEnabled: F[Boolean] = - f(logger.isErrorEnabled) + protected def kernel: LoggerKernel[F, String] = logger.underlying.mapK(f) - def error(t: Throwable)(message: => String): F[Unit] = - f(logger.error(t)(message)) - def warn(t: Throwable)(message: => String): F[Unit] = - f(logger.warn(t)(message)) - def info(t: Throwable)(message: => String): F[Unit] = - f(logger.info(t)(message)) - def debug(t: Throwable)(message: => String): F[Unit] = - f(logger.debug(t)(message)) - def trace(t: Throwable)(message: => String): F[Unit] = - f(logger.trace(t)(message)) - def error(message: => String): F[Unit] = - f(logger.error(message)) - def warn(message: => String): F[Unit] = - f(logger.warn(message)) - def info(message: => String): F[Unit] = - f(logger.info(message)) - def debug(message: => String): F[Unit] = - f(logger.debug(message)) - def trace(message: => String): F[Unit] = - f(logger.trace(message)) + def isTraceEnabled: F[Boolean] = f(logger.isTraceEnabled) + def isDebugEnabled: F[Boolean] = f(logger.isDebugEnabled) + def isInfoEnabled: F[Boolean] = f(logger.isInfoEnabled) + def isWarnEnabled: F[Boolean] = f(logger.isWarnEnabled) + def isErrorEnabled: F[Boolean] = f(logger.isErrorEnabled) } private def withModifiedString[F[_]]( @@ -72,6 +48,8 @@ object SelfAwareLogger { f: String => String ): SelfAwareLogger[F] = new SelfAwareLogger[F] { + protected def kernel: LoggerKernel[F, String] = l.underlying + override def isTraceEnabled: F[Boolean] = l.isTraceEnabled override def isDebugEnabled: F[Boolean] = l.isDebugEnabled override def isInfoEnabled: F[Boolean] = l.isInfoEnabled diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala index 40907bcf..f4e4b4d3 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala @@ -48,49 +48,52 @@ object SelfAwareStructuredLogger { private class ModifiedContextSelfAwareStructuredLogger[F[_]](sl: SelfAwareStructuredLogger[F])( modify: Map[String, String] => Map[String, String] ) extends SelfAwareStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = sl.underlying private lazy val defaultCtx: Map[String, String] = modify(Map.empty) - def error(message: => String): F[Unit] = sl.error(defaultCtx)(message) - def warn(message: => String): F[Unit] = sl.warn(defaultCtx)(message) - def info(message: => String): F[Unit] = sl.info(defaultCtx)(message) - def debug(message: => String): F[Unit] = sl.debug(defaultCtx)(message) - def trace(message: => String): F[Unit] = sl.trace(defaultCtx)(message) - def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + + override def error(message: => String): F[Unit] = sl.error(defaultCtx)(message) + override def warn(message: => String): F[Unit] = sl.warn(defaultCtx)(message) + override def info(message: => String): F[Unit] = sl.info(defaultCtx)(message) + override def debug(message: => String): F[Unit] = sl.debug(defaultCtx)(message) + override def trace(message: => String): F[Unit] = sl.trace(defaultCtx)(message) + + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = sl.trace(modify(ctx))(msg) - def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = sl.debug(modify(ctx))(msg) - def info(ctx: Map[String, String])(msg: => String): F[Unit] = + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = sl.info(modify(ctx))(msg) - def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = sl.warn(modify(ctx))(msg) - def error(ctx: Map[String, String])(msg: => String): F[Unit] = + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = sl.error(modify(ctx))(msg) - def isTraceEnabled: F[Boolean] = sl.isTraceEnabled - def isDebugEnabled: F[Boolean] = sl.isDebugEnabled - def isInfoEnabled: F[Boolean] = sl.isInfoEnabled - def isWarnEnabled: F[Boolean] = sl.isWarnEnabled - def isErrorEnabled: F[Boolean] = sl.isErrorEnabled + override def isTraceEnabled: F[Boolean] = sl.isTraceEnabled + override def isDebugEnabled: F[Boolean] = sl.isDebugEnabled + override def isInfoEnabled: F[Boolean] = sl.isInfoEnabled + override def isWarnEnabled: F[Boolean] = sl.isWarnEnabled + override def isErrorEnabled: F[Boolean] = sl.isErrorEnabled - def error(t: Throwable)(message: => String): F[Unit] = + override def error(t: Throwable)(message: => String): F[Unit] = sl.error(defaultCtx, t)(message) - def warn(t: Throwable)(message: => String): F[Unit] = + override def warn(t: Throwable)(message: => String): F[Unit] = sl.warn(defaultCtx, t)(message) - def info(t: Throwable)(message: => String): F[Unit] = + override def info(t: Throwable)(message: => String): F[Unit] = sl.info(defaultCtx, t)(message) - def debug(t: Throwable)(message: => String): F[Unit] = + override def debug(t: Throwable)(message: => String): F[Unit] = sl.debug(defaultCtx, t)(message) - def trace(t: Throwable)(message: => String): F[Unit] = + override def trace(t: Throwable)(message: => String): F[Unit] = sl.trace(defaultCtx, t)(message) - def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.error(modify(ctx), t)(message) - def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.warn(modify(ctx), t)(message) - def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.info(modify(ctx), t)(message) - def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.debug(modify(ctx), t)(message) - def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.trace(modify(ctx), t)(message) } @@ -99,6 +102,8 @@ object SelfAwareStructuredLogger { f: String => String ): SelfAwareStructuredLogger[F] = new SelfAwareStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = l.underlying + def isTraceEnabled: F[Boolean] = l.isTraceEnabled def isDebugEnabled: F[Boolean] = l.isDebugEnabled def isInfoEnabled: F[Boolean] = l.isInfoEnabled @@ -136,58 +141,12 @@ object SelfAwareStructuredLogger { f: G ~> F )(logger: SelfAwareStructuredLogger[G]): SelfAwareStructuredLogger[F] = new SelfAwareStructuredLogger[F] { - def isTraceEnabled: F[Boolean] = - f(logger.isTraceEnabled) - def isDebugEnabled: F[Boolean] = - f(logger.isDebugEnabled) - def isInfoEnabled: F[Boolean] = - f(logger.isInfoEnabled) - def isWarnEnabled: F[Boolean] = - f(logger.isWarnEnabled) - def isErrorEnabled: F[Boolean] = - f(logger.isErrorEnabled) - - def trace(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.trace(ctx)(msg)) - def debug(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.debug(ctx)(msg)) - def info(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.info(ctx)(msg)) - def warn(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.warn(ctx)(msg)) - def error(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.error(ctx)(msg)) - - def error(t: Throwable)(message: => String): F[Unit] = - f(logger.error(t)(message)) - def warn(t: Throwable)(message: => String): F[Unit] = - f(logger.warn(t)(message)) - def info(t: Throwable)(message: => String): F[Unit] = - f(logger.info(t)(message)) - def debug(t: Throwable)(message: => String): F[Unit] = - f(logger.debug(t)(message)) - def trace(t: Throwable)(message: => String): F[Unit] = - f(logger.trace(t)(message)) - def error(message: => String): F[Unit] = - f(logger.error(message)) - def warn(message: => String): F[Unit] = - f(logger.warn(message)) - def info(message: => String): F[Unit] = - f(logger.info(message)) - def debug(message: => String): F[Unit] = - f(logger.debug(message)) - def trace(message: => String): F[Unit] = - f(logger.trace(message)) - - def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.trace(ctx, t)(msg)) - def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.debug(ctx, t)(msg)) - def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.info(ctx, t)(msg)) - def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.warn(ctx, t)(msg)) - def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.error(ctx, t)(msg)) + protected def kernel: LoggerKernel[F, String] = logger.underlying.mapK(f) + + def isTraceEnabled: F[Boolean] = f(logger.isTraceEnabled) + def isDebugEnabled: F[Boolean] = f(logger.isDebugEnabled) + def isInfoEnabled: F[Boolean] = f(logger.isInfoEnabled) + def isWarnEnabled: F[Boolean] = f(logger.isWarnEnabled) + def isErrorEnabled: F[Boolean] = f(logger.isErrorEnabled) } } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala index ee50e011..8e233a6f 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala @@ -20,16 +20,37 @@ import cats.* import cats.Show.Shown trait StructuredLogger[F[_]] extends Logger[F] { - def trace(ctx: Map[String, String])(msg: => String): F[Unit] - def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] - def debug(ctx: Map[String, String])(msg: => String): F[Unit] - def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] - def info(ctx: Map[String, String])(msg: => String): F[Unit] - def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] - def warn(ctx: Map[String, String])(msg: => String): F[Unit] - def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] - def error(ctx: Map[String, String])(msg: => String): F[Unit] - def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + // Structured logging methods + def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Trace, _.withMessage(msg).withContextMap(ctx)) + + def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Trace, _.withMessage(msg).withContextMap(ctx).withThrowable(t)) + + def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Debug, _.withMessage(msg).withContextMap(ctx)) + + def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Debug, _.withMessage(msg).withContextMap(ctx).withThrowable(t)) + + def info(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Info, _.withMessage(msg).withContextMap(ctx)) + + def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Info, _.withMessage(msg).withContextMap(ctx).withThrowable(t)) + + def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Warn, _.withMessage(msg).withContextMap(ctx)) + + def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Warn, _.withMessage(msg).withContextMap(ctx).withThrowable(t)) + + def error(ctx: Map[String, String])(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Error, _.withMessage(msg).withContextMap(ctx)) + + def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + kernel.log(KernelLogLevel.Error, _.withMessage(msg).withContextMap(ctx).withThrowable(t)) + override def mapK[G[_]](fk: F ~> G): StructuredLogger[G] = StructuredLogger.mapK(fk)(this) @@ -61,43 +82,46 @@ object StructuredLogger { private class ModifiedContextStructuredLogger[F[_]](sl: StructuredLogger[F])( modify: Map[String, String] => Map[String, String] ) extends StructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = sl.underlying private lazy val defaultCtx: Map[String, String] = modify(Map.empty) - def error(message: => String): F[Unit] = sl.error(defaultCtx)(message) - def warn(message: => String): F[Unit] = sl.warn(defaultCtx)(message) - def info(message: => String): F[Unit] = sl.info(defaultCtx)(message) - def debug(message: => String): F[Unit] = sl.debug(defaultCtx)(message) - def trace(message: => String): F[Unit] = sl.trace(defaultCtx)(message) - def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + + override def error(message: => String): F[Unit] = sl.error(defaultCtx)(message) + override def warn(message: => String): F[Unit] = sl.warn(defaultCtx)(message) + override def info(message: => String): F[Unit] = sl.info(defaultCtx)(message) + override def debug(message: => String): F[Unit] = sl.debug(defaultCtx)(message) + override def trace(message: => String): F[Unit] = sl.trace(defaultCtx)(message) + + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = sl.trace(modify(ctx))(msg) - def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = sl.debug(modify(ctx))(msg) - def info(ctx: Map[String, String])(msg: => String): F[Unit] = + override def info(ctx: Map[String, String])(msg: => String): F[Unit] = sl.info(modify(ctx))(msg) - def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = sl.warn(modify(ctx))(msg) - def error(ctx: Map[String, String])(msg: => String): F[Unit] = + override def error(ctx: Map[String, String])(msg: => String): F[Unit] = sl.error(modify(ctx))(msg) - def error(t: Throwable)(message: => String): F[Unit] = + override def error(t: Throwable)(message: => String): F[Unit] = sl.error(defaultCtx, t)(message) - def warn(t: Throwable)(message: => String): F[Unit] = + override def warn(t: Throwable)(message: => String): F[Unit] = sl.warn(defaultCtx, t)(message) - def info(t: Throwable)(message: => String): F[Unit] = + override def info(t: Throwable)(message: => String): F[Unit] = sl.info(defaultCtx, t)(message) - def debug(t: Throwable)(message: => String): F[Unit] = + override def debug(t: Throwable)(message: => String): F[Unit] = sl.debug(defaultCtx, t)(message) - def trace(t: Throwable)(message: => String): F[Unit] = + override def trace(t: Throwable)(message: => String): F[Unit] = sl.trace(defaultCtx, t)(message) - def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.error(modify(ctx), t)(message) - def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.warn(modify(ctx), t)(message) - def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.info(modify(ctx), t)(message) - def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.debug(modify(ctx), t)(message) - def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = sl.trace(modify(ctx), t)(message) } @@ -106,6 +130,7 @@ object StructuredLogger { f: String => String ): StructuredLogger[F] = new StructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = l.underlying override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = l.trace(ctx)(f(msg)) override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = l.trace(ctx, t)(f(msg)) @@ -135,47 +160,6 @@ object StructuredLogger { private def mapK[G[_], F[_]](f: G ~> F)(logger: StructuredLogger[G]): StructuredLogger[F] = new StructuredLogger[F] { - def trace(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.trace(ctx)(msg)) - def debug(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.debug(ctx)(msg)) - def info(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.info(ctx)(msg)) - def warn(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.warn(ctx)(msg)) - def error(ctx: Map[String, String])(msg: => String): F[Unit] = - f(logger.error(ctx)(msg)) - - def error(t: Throwable)(message: => String): F[Unit] = - f(logger.error(t)(message)) - def warn(t: Throwable)(message: => String): F[Unit] = - f(logger.warn(t)(message)) - def info(t: Throwable)(message: => String): F[Unit] = - f(logger.info(t)(message)) - def debug(t: Throwable)(message: => String): F[Unit] = - f(logger.debug(t)(message)) - def trace(t: Throwable)(message: => String): F[Unit] = - f(logger.trace(t)(message)) - def error(message: => String): F[Unit] = - f(logger.error(message)) - def warn(message: => String): F[Unit] = - f(logger.warn(message)) - def info(message: => String): F[Unit] = - f(logger.info(message)) - def debug(message: => String): F[Unit] = - f(logger.debug(message)) - def trace(message: => String): F[Unit] = - f(logger.trace(message)) - - def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.trace(ctx, t)(msg)) - def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.debug(ctx, t)(msg)) - def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.info(ctx, t)(msg)) - def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.warn(ctx, t)(msg)) - def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - f(logger.error(ctx, t)(msg)) + protected def kernel: LoggerKernel[F, String] = logger.underlying.mapK(f) } } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogger.scala index d8ac9c60..2d6773aa 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogger.scala @@ -21,7 +21,8 @@ import cats.effect.kernel.Resource.ExitCase import cats.effect.kernel.{Concurrent, Ref, Resource} import cats.syntax.all.* import cats.~> -import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.{KernelLogLevel, Logger, LoggerKernel} +import org.typelevel.log4cats.Log /** * `Logger` that does not immediately log. @@ -49,6 +50,8 @@ import org.typelevel.log4cats.Logger * >>> WARNING: READ BEFORE USAGE! <<< */ trait DeferredLogger[F[_]] extends Logger[F] with DeferredLogging[F] { + protected def kernel: LoggerKernel[F, String] + override def withModifiedString(f: String => String): DeferredLogger[F] = DeferredLogger.withModifiedString(this, f) override def mapK[G[_]](fk: F ~> G): DeferredLogger[G] = DeferredLogger.mapK(this, fk) @@ -66,6 +69,31 @@ object DeferredLogger { new DeferredLogger[F] { private def save(lm: DeferredLogMessage): F[Unit] = ref.update(_.append(lm)) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val logLevel = level match { + case KernelLogLevel.Trace => LogLevel.Trace + case KernelLogLevel.Debug => LogLevel.Debug + case KernelLogLevel.Info => LogLevel.Info + case KernelLogLevel.Warn => LogLevel.Warn + case KernelLogLevel.Error => LogLevel.Error + case KernelLogLevel.Fatal => LogLevel.Error + case _ => LogLevel.Error // Handle any other KernelLogLevel values + } + val deferredMsg = DeferredLogMessage( + logLevel, + log.context, + log.throwable, + log.message + ) + save(deferredMsg) + } + } + override def trace(t: Throwable)(msg: => String): F[Unit] = save(DeferredLogMessage.trace(Map.empty, t.some, () => msg)) override def debug(t: Throwable)(msg: => String): F[Unit] = @@ -99,6 +127,8 @@ object DeferredLogger { fk: F ~> G ): DeferredLogger[G] = new DeferredLogger[G] { + protected def kernel: LoggerKernel[G, String] = logger.kernel.mapK(fk) + override def inspect: G[Chain[DeferredLogMessage]] = fk(logger.inspect) override def log: G[Unit] = fk(logger.log) @@ -124,6 +154,8 @@ object DeferredLogger { f: String => String ): DeferredLogger[F] = new DeferredLogger[F] { + protected def kernel: LoggerKernel[F, String] = logger.kernel + override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect override def log: F[Unit] = logger.log diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala index 778305db..06d3d056 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala @@ -21,7 +21,8 @@ import cats.effect.kernel.Resource.ExitCase import cats.effect.kernel.{Concurrent, Ref, Resource} import cats.syntax.all.* import cats.{~>, Show} -import org.typelevel.log4cats.SelfAwareStructuredLogger +import org.typelevel.log4cats.{KernelLogLevel, LoggerKernel, SelfAwareStructuredLogger} +import org.typelevel.log4cats.Log /** * Similar to `DeferredStructuredLogger`, for `SelfAwareStructuredLogger` @@ -33,6 +34,8 @@ import org.typelevel.log4cats.SelfAwareStructuredLogger trait DeferredSelfAwareStructuredLogger[F[_]] extends SelfAwareStructuredLogger[F] with DeferredLogging[F] { + protected def kernel: LoggerKernel[F, String] + override def mapK[G[_]](fk: F ~> G): DeferredSelfAwareStructuredLogger[G] = DeferredSelfAwareStructuredLogger.mapK(this, fk) @@ -61,6 +64,31 @@ object DeferredSelfAwareStructuredLogger { private def save(lm: DeferredLogMessage): F[Unit] = stash.update(_.append(lm -> logger)) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val logLevel = level match { + case KernelLogLevel.Trace => LogLevel.Trace + case KernelLogLevel.Debug => LogLevel.Debug + case KernelLogLevel.Info => LogLevel.Info + case KernelLogLevel.Warn => LogLevel.Warn + case KernelLogLevel.Error => LogLevel.Error + case KernelLogLevel.Fatal => LogLevel.Error + case _ => LogLevel.Error // Handle any other KernelLogLevel values + } + val deferredMsg = DeferredLogMessage( + logLevel, + log.context, + log.throwable, + log.message + ) + save(deferredMsg) + } + } + override def isTraceEnabled: F[Boolean] = logger.isTraceEnabled override def isDebugEnabled: F[Boolean] = logger.isDebugEnabled override def isInfoEnabled: F[Boolean] = logger.isInfoEnabled @@ -156,6 +184,8 @@ object DeferredSelfAwareStructuredLogger { fk: F ~> G ): DeferredSelfAwareStructuredLogger[G] = new DeferredSelfAwareStructuredLogger[G] { + protected def kernel: LoggerKernel[G, String] = logger.kernel.mapK(fk) + override def inspect: G[Chain[DeferredLogMessage]] = fk( logger.inspect ) @@ -217,6 +247,8 @@ object DeferredSelfAwareStructuredLogger { new DeferredSelfAwareStructuredLogger[F] { private def addCtx(ctx: Map[String, String]): Map[String, String] = baseCtx ++ ctx + protected def kernel: LoggerKernel[F, String] = logger.kernel + override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect override def log: F[Unit] = logger.log override def isTraceEnabled: F[Boolean] = logger.isTraceEnabled @@ -270,6 +302,8 @@ object DeferredSelfAwareStructuredLogger { f: String => String ): DeferredSelfAwareStructuredLogger[F] = new DeferredSelfAwareStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = logger.kernel + override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect override def log: F[Unit] = logger.log override def isTraceEnabled: F[Boolean] = logger.isTraceEnabled diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala index 1c0855bc..87f5f4de 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala @@ -22,7 +22,8 @@ import cats.effect.kernel.Resource.ExitCase import cats.effect.kernel.{Concurrent, Ref, Resource} import cats.syntax.all.* import cats.~> -import org.typelevel.log4cats.StructuredLogger +import org.typelevel.log4cats.{KernelLogLevel, LoggerKernel, StructuredLogger} +import org.typelevel.log4cats.Log /** * `StructuredLogger` that does not immediately log. @@ -50,6 +51,8 @@ import org.typelevel.log4cats.StructuredLogger * >>> WARNING: READ BEFORE USAGE! <<< */ trait DeferredStructuredLogger[F[_]] extends StructuredLogger[F] with DeferredLogging[F] { + protected def kernel: LoggerKernel[F, String] + override def mapK[G[_]](fk: F ~> G): DeferredStructuredLogger[G] = DeferredStructuredLogger.mapK(this, fk) @@ -79,6 +82,31 @@ object DeferredStructuredLogger { new DeferredStructuredLogger[F] { private def save(lm: DeferredLogMessage): F[Unit] = ref.update(_.append(lm)) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val logLevel = level match { + case KernelLogLevel.Trace => LogLevel.Trace + case KernelLogLevel.Debug => LogLevel.Debug + case KernelLogLevel.Info => LogLevel.Info + case KernelLogLevel.Warn => LogLevel.Warn + case KernelLogLevel.Error => LogLevel.Error + case KernelLogLevel.Fatal => LogLevel.Error + case _ => LogLevel.Error // Handle any other KernelLogLevel values + } + val deferredMsg = DeferredLogMessage( + logLevel, + log.context, + log.throwable, + log.message + ) + save(deferredMsg) + } + } + override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = save(DeferredLogMessage.trace(ctx, none, () => msg)) override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = @@ -135,6 +163,8 @@ object DeferredStructuredLogger { fk: F ~> G ): DeferredStructuredLogger[G] = new DeferredStructuredLogger[G] { + protected def kernel: LoggerKernel[G, String] = logger.kernel.mapK(fk) + override def inspect: G[Chain[DeferredLogMessage]] = fk(logger.inspect) override def log: G[Unit] = fk(logger.log) @@ -178,6 +208,8 @@ object DeferredStructuredLogger { baseCtx: Map[String, String] ): DeferredStructuredLogger[F] = new DeferredStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = logger.kernel + private def addCtx(ctx: Map[String, String]): Map[String, String] = baseCtx ++ ctx override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect @@ -228,6 +260,8 @@ object DeferredStructuredLogger { f: String => String ): DeferredStructuredLogger[F] = new DeferredStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = logger.kernel + override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect override def log: F[Unit] = logger.log diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogLevel.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogLevel.scala index 2a4c6754..7c9d7001 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogLevel.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/LogLevel.scala @@ -56,7 +56,8 @@ object LogLevel { case Trace => 1 } - implicit final val logLevelOrder: Order[LogLevel] = Order.by[LogLevel, Int](toIndex) + implicit final val logLevelOrder: Order[LogLevel] = + Order.by[LogLevel, Int](toIndex) implicit final val logLevelHash: Hash[LogLevel] = Hash.by(toIndex) } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTLogger.scala index 70ecd01a..2375e864 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTLogger.scala @@ -40,6 +40,28 @@ object WriterTLogger { errorEnabled: Boolean = true ): SelfAwareLogger[WriterT[F, G[LogMessage], *]] = new SelfAwareLogger[WriterT[F, G[LogMessage], *]] { + protected def kernel: LoggerKernel[WriterT[F, G[LogMessage], *], String] = + new LoggerKernel[WriterT[F, G[LogMessage], *], String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): WriterT[F, G[LogMessage], Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val logLevel = level match { + case KernelLogLevel.Trace => LogLevel.Trace + case KernelLogLevel.Debug => LogLevel.Debug + case KernelLogLevel.Info => LogLevel.Info + case KernelLogLevel.Warn => LogLevel.Warn + case KernelLogLevel.Error => LogLevel.Error + case KernelLogLevel.Fatal => LogLevel.Error + case _ => LogLevel.Error // Handle any other KernelLogLevel values + } + WriterT.tell[F, G[LogMessage]]( + Applicative[G].pure(LogMessage(logLevel, log.throwable, log.message())) + ) + } + } + override def isTraceEnabled: WriterT[F, G[LogMessage], Boolean] = isEnabled(traceEnabled) override def isDebugEnabled: WriterT[F, G[LogMessage], Boolean] = isEnabled(debugEnabled) override def isInfoEnabled: WriterT[F, G[LogMessage], Boolean] = isEnabled(infoEnabled) diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTStructuredLogger.scala index b8b218de..6f77c6a6 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/WriterTStructuredLogger.scala @@ -20,7 +20,13 @@ import cats.data.WriterT import cats.kernel.Monoid import cats.syntax.all.* import cats.{~>, Alternative, Applicative, Foldable, Monad} -import org.typelevel.log4cats.{SelfAwareStructuredLogger, StructuredLogger} +import org.typelevel.log4cats.{ + KernelLogLevel, + LoggerKernel, + SelfAwareStructuredLogger, + StructuredLogger +} +import org.typelevel.log4cats.Log /** * A `SelfAwareStructuredLogger` implemented using `cats.data.WriterT`. @@ -43,6 +49,30 @@ object WriterTStructuredLogger { new SelfAwareStructuredLogger[WriterT[F, G[StructuredLogMessage], *]] { type LoggerF[A] = WriterT[F, G[StructuredLogMessage], A] + protected def kernel: LoggerKernel[WriterT[F, G[StructuredLogMessage], *], String] = + new LoggerKernel[WriterT[F, G[StructuredLogMessage], *], String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): WriterT[F, G[StructuredLogMessage], Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val logLevel = level match { + case KernelLogLevel.Trace => LogLevel.Trace + case KernelLogLevel.Debug => LogLevel.Debug + case KernelLogLevel.Info => LogLevel.Info + case KernelLogLevel.Warn => LogLevel.Warn + case KernelLogLevel.Error => LogLevel.Error + case KernelLogLevel.Fatal => LogLevel.Error + case _ => LogLevel.Error // Handle any other KernelLogLevel values + } + WriterT.tell[F, G[StructuredLogMessage]]( + Applicative[G].pure( + StructuredLogMessage(logLevel, log.context, log.throwable, log.message()) + ) + ) + } + } + override def isTraceEnabled: LoggerF[Boolean] = isEnabled(traceEnabled) override def isDebugEnabled: LoggerF[Boolean] = isEnabled(debugEnabled) diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLogger.scala index 95762949..7813f994 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLogger.scala @@ -17,7 +17,7 @@ package org.typelevel.log4cats.noop import cats.Applicative -import org.typelevel.log4cats.SelfAwareStructuredLogger +import org.typelevel.log4cats.{KernelLogLevel, Log, LoggerKernel, SelfAwareStructuredLogger} object NoOpLogger { def apply[F[_]: Applicative]: SelfAwareStructuredLogger[F] = impl[F] @@ -31,80 +31,29 @@ object NoOpLogger { if (evaluateArgs) strictImpl else lazyImpl private def lazyImpl[F[_]: Applicative] = new SelfAwareStructuredLogger[F] { + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = + Applicative[F].pure(()) + } - val no: F[Boolean] = Applicative[F].pure(false) - val unit: F[Unit] = Applicative[F].pure(()) - - @inline override def isTraceEnabled: F[Boolean] = no - @inline override def isDebugEnabled: F[Boolean] = no - @inline override def isInfoEnabled: F[Boolean] = no - @inline override def isWarnEnabled: F[Boolean] = no - @inline override def isErrorEnabled: F[Boolean] = no - @inline override def trace(t: Throwable)(msg: => String): F[Unit] = unit - @inline override def trace(msg: => String): F[Unit] = unit - @inline override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = unit - @inline override def debug(t: Throwable)(msg: => String): F[Unit] = unit - @inline override def debug(msg: => String): F[Unit] = unit - @inline override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = unit - @inline override def info(t: Throwable)(msg: => String): F[Unit] = unit - @inline override def info(msg: => String): F[Unit] = unit - @inline override def info(ctx: Map[String, String])(msg: => String): F[Unit] = unit - @inline override def warn(t: Throwable)(msg: => String): F[Unit] = unit - @inline override def warn(msg: => String): F[Unit] = unit - @inline override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = unit - @inline override def error(t: Throwable)(msg: => String): F[Unit] = unit - @inline override def error(msg: => String): F[Unit] = unit - @inline override def error(ctx: Map[String, String])(msg: => String): F[Unit] = unit - @inline override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - unit - @inline override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - unit - @inline override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - unit - @inline override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - unit - @inline override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - unit + @inline override def isTraceEnabled: F[Boolean] = Applicative[F].pure(false) + @inline override def isDebugEnabled: F[Boolean] = Applicative[F].pure(false) + @inline override def isInfoEnabled: F[Boolean] = Applicative[F].pure(false) + @inline override def isWarnEnabled: F[Boolean] = Applicative[F].pure(false) + @inline override def isErrorEnabled: F[Boolean] = Applicative[F].pure(false) } private def strictImpl[F[_]: Applicative] = new SelfAwareStructuredLogger[F] { - - val yes: F[Boolean] = Applicative[F].pure(true) - def void(arg: => Any): F[Unit] = Applicative[F].pure { - val _ = arg - () + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = + Applicative[F].pure(()) } - @inline override def isTraceEnabled: F[Boolean] = yes - @inline override def isDebugEnabled: F[Boolean] = yes - @inline override def isInfoEnabled: F[Boolean] = yes - @inline override def isWarnEnabled: F[Boolean] = yes - @inline override def isErrorEnabled: F[Boolean] = yes - @inline override def trace(t: Throwable)(msg: => String): F[Unit] = void(t) - @inline override def trace(msg: => String): F[Unit] = void(msg) - @inline override def trace(ctx: Map[String, String])(msg: => String): F[Unit] = void(msg) - @inline override def debug(t: Throwable)(msg: => String): F[Unit] = void(msg) - @inline override def debug(msg: => String): F[Unit] = void(msg) - @inline override def debug(ctx: Map[String, String])(msg: => String): F[Unit] = void(msg) - @inline override def info(t: Throwable)(msg: => String): F[Unit] = void(msg) - @inline override def info(msg: => String): F[Unit] = void(msg) - @inline override def info(ctx: Map[String, String])(msg: => String): F[Unit] = void(msg) - @inline override def warn(t: Throwable)(msg: => String): F[Unit] = void(msg) - @inline override def warn(msg: => String): F[Unit] = void(msg) - @inline override def warn(ctx: Map[String, String])(msg: => String): F[Unit] = void(msg) - @inline override def error(t: Throwable)(msg: => String): F[Unit] = void(msg) - @inline override def error(msg: => String): F[Unit] = void(msg) - @inline override def error(ctx: Map[String, String])(msg: => String): F[Unit] = void(msg) - @inline override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - void(msg) - @inline override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - void(msg) - @inline override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - void(msg) - @inline override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - void(msg) - @inline override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = - void(msg) + @inline override def isTraceEnabled: F[Boolean] = Applicative[F].pure(true) + @inline override def isDebugEnabled: F[Boolean] = Applicative[F].pure(true) + @inline override def isInfoEnabled: F[Boolean] = Applicative[F].pure(true) + @inline override def isWarnEnabled: F[Boolean] = Applicative[F].pure(true) + @inline override def isErrorEnabled: F[Boolean] = Applicative[F].pure(true) } } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLoggerKernel.scala b/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLoggerKernel.scala new file mode 100644 index 00000000..b56ea115 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/noop/NoOpLoggerKernel.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats.noop + +import cats.Applicative +import org.typelevel.log4cats.{KernelLogLevel, Log, LoggerKernel} + +object NoOpLoggerKernel { + def apply[F[_]: Applicative, Ctx]: LoggerKernel[F, Ctx] = new LoggerKernel[F, Ctx] { + override def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = + Applicative[F].unit + } +} diff --git a/core/shared/src/test/scala/org/typelevel/log4cats/NoOpLoggerKernelTest.scala b/core/shared/src/test/scala/org/typelevel/log4cats/NoOpLoggerKernelTest.scala new file mode 100644 index 00000000..e0327e1c --- /dev/null +++ b/core/shared/src/test/scala/org/typelevel/log4cats/NoOpLoggerKernelTest.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats.noop + +import cats.effect.IO +import munit.CatsEffectSuite +import org.typelevel.log4cats._ + +class NoOpLoggerKernelTest extends CatsEffectSuite { + private def boom()(implicit loc: munit.Location): String = fail( + "This code should not have executed" + ) + + test("NoOpLoggerKernel should do nothing and not fail") { + val kernel = NoOpLoggerKernel[IO, String] + val logger = SamLogger.wrap(kernel) + // All of these should be no-ops and not throw + logger.info("This should not appear") *> + logger.error(boom()) *> + logger.warn(boom()) *> + logger.debug(boom()) *> + logger.trace(boom()) + } + + test("NoOpLoggerKernel should work with SamLogger") { + val kernel = NoOpLoggerKernel[IO, String] + val logger = SamLogger.wrap(kernel) + logger.info("SamLogger test") *> + logger.error("SamLogger test") + } +} diff --git a/core/shared/src/test/scala/org/typelevel/log4cats/SamLoggerTest.scala b/core/shared/src/test/scala/org/typelevel/log4cats/SamLoggerTest.scala new file mode 100644 index 00000000..3db1da9a --- /dev/null +++ b/core/shared/src/test/scala/org/typelevel/log4cats/SamLoggerTest.scala @@ -0,0 +1,131 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.effect.IO +import cats.effect.Ref +import munit.CatsEffectSuite + +class SamLoggerTest extends CatsEffectSuite { + + // Test kernel that captures log calls for verification + def testKernel[F[_]]( + ref: Ref[F, List[Log[String]]] + )(implicit F: cats.effect.Sync[F]): LoggerKernel[F, String] = { + new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + record: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val logRecord = record(Log.mutableBuilder[String]()).build() + F.flatMap(ref.update(_ :+ logRecord))(_ => F.unit) + } + } + } + + test("SamLogger should execute logging operations and capture them") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + + _ <- logger.info("Hello, SAM Logger!") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + assertEquals(logs.head.message(), "Hello, SAM Logger!") + } + } + + test("SamLogger should support simple message logging") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + + _ <- logger.info("User action") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "User action") + } + } + + test("SamLogger should support error logging") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + + _ <- logger.error("Something went wrong") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "Something went wrong") + } + } + + test("SamLogger should support multiple log calls") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + + _ <- logger.info("First log entry") + _ <- logger.info("Second log entry") + logs <- ref.get + } yield { + assertEquals(logs.length, 2) + assertEquals(logs(0).message(), "First log entry") + assertEquals(logs(1).message(), "Second log entry") + } + } + + test("SamLogger withModifiedString should modify messages correctly") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + modifiedLogger = logger.withModifiedString(msg => s"[MODIFIED] $msg") + + _ <- modifiedLogger.info("Test message") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + assertEquals(logs.head.message(), "[MODIFIED] Test message") + } + } + + test("SamLogger withModifiedString should preserve other fields") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamLogger.wrap(kernel) + modifiedLogger = logger.withModifiedString(msg => s"[MODIFIED] $msg") + + _ <- modifiedLogger.info("Test message") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "[MODIFIED] Test message") + } + } + +} diff --git a/core/shared/src/test/scala/org/typelevel/log4cats/SamStructuredLoggerTest.scala b/core/shared/src/test/scala/org/typelevel/log4cats/SamStructuredLoggerTest.scala new file mode 100644 index 00000000..9d825f82 --- /dev/null +++ b/core/shared/src/test/scala/org/typelevel/log4cats/SamStructuredLoggerTest.scala @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.effect.IO +import cats.effect.Ref +import munit.CatsEffectSuite + +class SamStructuredLoggerTest extends CatsEffectSuite { + + // Test kernel that captures log calls for verification + def testKernel[F[_]]( + ref: Ref[F, List[Log[String]]] + )(implicit F: cats.effect.Sync[F]): LoggerKernel[F, String] = { + new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + record: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val logRecord = record(Log.mutableBuilder[String]()).build() + F.flatMap(ref.update(_ :+ logRecord))(_ => F.unit) + } + } + } + + test("SamStructuredLogger should handle context with multiple entries") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamStructuredLogger.fromKernel(kernel) + + _ <- logger.info(Map("base" -> "value", "extra" -> "data"))("Test message") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "Test message") + assertEquals(log.context.size, 2) + assertEquals(log.context("base"), "value") + assertEquals(log.context("extra"), "data") + } + } + + test("SamStructuredLogger should support all log levels") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamStructuredLogger.fromKernel(kernel) + + _ <- logger.trace(Map("level" -> "trace"))("Trace message") + _ <- logger.debug(Map("level" -> "debug"))("Debug message") + _ <- logger.info(Map("level" -> "info"))("Info message") + _ <- logger.warn(Map("level" -> "warn"))("Warn message") + _ <- logger.error(Map("level" -> "error"))("Error message") + logs <- ref.get + } yield { + assertEquals(logs.length, 5) + assertEquals(logs(0).context("level"), "trace") + assertEquals(logs(1).context("level"), "debug") + assertEquals(logs(2).context("level"), "info") + assertEquals(logs(3).context("level"), "warn") + assertEquals(logs(4).context("level"), "error") + } + } + + test("SamStructuredLogger should support error logging with throwables") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamStructuredLogger.fromKernel(kernel) + + exception = new RuntimeException("Test exception") + _ <- logger.error(Map("error_type" -> "runtime"), exception)("Error with context") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "Error with context") + assertEquals(log.context("error_type"), "runtime") + assertEquals(log.throwable, Some(exception)) + } + } + + test("SamStructuredLogger should support addContext functionality") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamStructuredLogger.fromKernel(kernel) + enrichedLogger = logger.addContext(Map("service" -> "test-service")) + + _ <- enrichedLogger.info(Map("request_id" -> "123"))("Request processed") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "Request processed") + assertEquals(log.context("service"), "test-service") + assertEquals(log.context("request_id"), "123") + } + } + + test("SamStructuredLogger should support addContext with key-value pairs") { + for { + ref <- Ref.of[IO, List[Log[String]]](List.empty) + kernel = testKernel[IO](ref) + logger = SamStructuredLogger.fromKernel(kernel) + enrichedLogger = logger.addContext("service" -> "test-service", "version" -> "1.0.0") + + _ <- enrichedLogger.info("Service started") + logs <- ref.get + } yield { + assertEquals(logs.length, 1) + val log = logs.head + assertEquals(log.message(), "Service started") + assertEquals(log.context("service"), "test-service") + assertEquals(log.context("version"), "1.0.0") + } + } +} diff --git a/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerFactorySyntaxCompilation.scala b/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerFactorySyntaxCompilation.scala index e440320c..0f87b598 100644 --- a/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerFactorySyntaxCompilation.scala +++ b/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerFactorySyntaxCompilation.scala @@ -34,12 +34,12 @@ object LoggerFactorySyntaxCompilation { lf.mapK(Kleisli.liftK[F, A]) def eitherTLoggerFactory[F[_]: Functor: LoggerFactory, E]: LoggerFactory[EitherT[F, E, *]] = - LoggerFactory[EitherT[F, E, *]] + LoggerFactory[F].mapK(EitherT.liftK[F, E]) def optionTLoggerFactory[F[_]: Functor: LoggerFactory]: LoggerFactory[OptionT[F, *]] = - LoggerFactory[OptionT[F, *]] + LoggerFactory[F].mapK(OptionT.liftK[F]) def kleisliLoggerFactory[F[_]: Functor: LoggerFactory, A]: LoggerFactory[Kleisli[F, A, *]] = - LoggerFactory[Kleisli[F, A, *]] + LoggerFactory[F].mapK(Kleisli.liftK[F, A]) } diff --git a/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerSyntaxCompilation.scala b/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerSyntaxCompilation.scala index 27ec4558..cf6d9815 100644 --- a/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerSyntaxCompilation.scala +++ b/core/shared/src/test/scala/org/typelevel/log4cats/extras/syntax/LoggerSyntaxCompilation.scala @@ -54,12 +54,12 @@ object LoggerSyntaxCompilation { l.withModifiedString(identity) def eitherTLogger[F[_]: Functor: Logger, A]: EitherT[F, A, Unit] = - Logger[EitherT[F, A, *]].info("foo") + Logger[F].mapK(EitherT.liftK[F, A]).info("foo") def kleisliLogger[F[_]: Logger, A]: Kleisli[F, A, Unit] = - Logger[Kleisli[F, A, *]].info("bar") + Logger[F].mapK(Kleisli.liftK[F, A]).info("bar") def optionTLogger[F[_]: Functor: Logger]: OptionT[F, Unit] = - Logger[OptionT[F, *]].info("baz") + Logger[F].mapK(OptionT.liftK[F]).info("baz") } diff --git a/js-console/src/main/scala/org/typelevel/log4cats/console/ConsoleLogger.scala b/js-console/src/main/scala/org/typelevel/log4cats/console/ConsoleLogger.scala index 6f79e754..dac951da 100644 --- a/js-console/src/main/scala/org/typelevel/log4cats/console/ConsoleLogger.scala +++ b/js-console/src/main/scala/org/typelevel/log4cats/console/ConsoleLogger.scala @@ -25,25 +25,32 @@ import org.typelevel.log4cats.extras.LogLevel.* class ConsoleLogger[F[_]: Sync](logLevel: Option[LogLevel] = Option(Trace)) extends SelfAwareStructuredLogger[F] { private val ConsoleF: ConsoleF[F] = implicitly - override def trace(t: Throwable)(message: => String): F[Unit] = ConsoleF.debug(message, t) - override def trace(message: => String): F[Unit] = ConsoleF.debug(message) - override def isTraceEnabled: F[Boolean] = logLevel.exists(_ <= Trace).pure[F] - override def debug(t: Throwable)(message: => String): F[Unit] = ConsoleF.debug(message, t) - override def debug(message: => String): F[Unit] = ConsoleF.debug(message) - override def isDebugEnabled: F[Boolean] = logLevel.exists(_ <= Debug).pure[F] + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = { + val logRecord = record(Log.mutableBuilder[String]()) + val log = logRecord.build() + val message = log.message() + val throwable = log.throwable - override def info(t: Throwable)(message: => String): F[Unit] = ConsoleF.info(message, t) - override def info(message: => String): F[Unit] = ConsoleF.info(message) - override def isInfoEnabled: F[Boolean] = logLevel.exists(_ <= Info).pure[F] + level match { + case KernelLogLevel.Trace => ConsoleF.debug(message, throwable.orNull) + case KernelLogLevel.Debug => ConsoleF.debug(message, throwable.orNull) + case KernelLogLevel.Info => ConsoleF.info(message, throwable.orNull) + case KernelLogLevel.Warn => ConsoleF.warn(message, throwable.orNull) + case KernelLogLevel.Error => ConsoleF.error(message, throwable.orNull) + case KernelLogLevel.Fatal => ConsoleF.error(message, throwable.orNull) + case _ => + ConsoleF.error(message, throwable.orNull) // Handle any other KernelLogLevel values + } + } + } - override def warn(t: Throwable)(message: => String): F[Unit] = ConsoleF.warn(message, t) - override def warn(message: => String): F[Unit] = ConsoleF.warn(message) - override def isWarnEnabled: F[Boolean] = logLevel.exists(_ <= Warn).pure[F] - - override def error(t: Throwable)(message: => String): F[Unit] = ConsoleF.error(message, t) - override def error(message: => String): F[Unit] = ConsoleF.error(message) - override def isErrorEnabled: F[Boolean] = logLevel.exists(_ <= Error).pure[F] + def isTraceEnabled: F[Boolean] = logLevel.exists(_ <= Trace).pure[F] + def isDebugEnabled: F[Boolean] = logLevel.exists(_ <= Debug).pure[F] + def isInfoEnabled: F[Boolean] = logLevel.exists(_ <= Info).pure[F] + def isWarnEnabled: F[Boolean] = logLevel.exists(_ <= Warn).pure[F] + def isErrorEnabled: F[Boolean] = logLevel.exists(_ <= Error).pure[F] /* * ConsoleLogger should probably not extend from StructuredLogger, because there's not diff --git a/slf4j/src/main/scala/org/typelevel/log4cats/slf4j/internal/Slf4jLoggerInternal.scala b/slf4j/src/main/scala/org/typelevel/log4cats/slf4j/internal/Slf4jLoggerInternal.scala index fc0f771c..34b5c223 100644 --- a/slf4j/src/main/scala/org/typelevel/log4cats/slf4j/internal/Slf4jLoggerInternal.scala +++ b/slf4j/src/main/scala/org/typelevel/log4cats/slf4j/internal/Slf4jLoggerInternal.scala @@ -49,6 +49,90 @@ private[slf4j] object Slf4jLoggerInternal { ) = this(logger, Sync.Type.Delay)(F) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + // Check if the log level is enabled before building the Log object + val isEnabled = level match { + case KernelLogLevel.Trace => F.delay(logger.isTraceEnabled) + case KernelLogLevel.Debug => F.delay(logger.isDebugEnabled) + case KernelLogLevel.Info => F.delay(logger.isInfoEnabled) + case KernelLogLevel.Warn => F.delay(logger.isWarnEnabled) + case KernelLogLevel.Error => F.delay(logger.isErrorEnabled) + case _ => F.pure(false) // Handle any other KernelLogLevel values + } + + Sync[F].flatMap(isEnabled) { enabled => + if (enabled) { + // Only build the Log object if the level is enabled + val log = logBuilder(Log.mutableBuilder[String]()).build() + val context = log.context + + // Set MDC context + val setMdc = F.delay { + context.foreach { case (k, v) => MDC.put(k, v) } + } + + val clearMdc = F.delay { + context.keys.foreach(MDC.remove) + } + + val logMessage = level match { + case KernelLogLevel.Trace => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.trace(message, throwable.get) + else logger.trace(message) + } + case KernelLogLevel.Debug => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.debug(message, throwable.get) + else logger.debug(message) + } + case KernelLogLevel.Info => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.info(message, throwable.get) + else logger.info(message) + } + case KernelLogLevel.Warn => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.warn(message, throwable.get) + else logger.warn(message) + } + case KernelLogLevel.Error => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.error(message, throwable.get) + else logger.error(message) + } + case _ => + F.delay { + val message = log.message() + val throwable = log.throwable + if (throwable.isDefined) logger.error(message, throwable.get) + else logger.error(message) + } + } + + Sync[F].flatMap(setMdc)(_ => Sync[F].flatMap(logMessage)(_ => clearMdc)) + } else { + // If level is disabled, do nothing + F.unit + } + } + } + } + private val isTraceEnabledUnsafe = () => logger.isTraceEnabled private val isDebugEnabledUnsafe = () => logger.isDebugEnabled private val isInfoEnabledUnsafe = () => logger.isInfoEnabled diff --git a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/StructuredTestingLogger.scala b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/StructuredTestingLogger.scala index 640a67f5..d9fe88f7 100644 --- a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/StructuredTestingLogger.scala +++ b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/StructuredTestingLogger.scala @@ -17,7 +17,8 @@ package org.typelevel.log4cats.testing import cats.data.Chain -import org.typelevel.log4cats.SelfAwareStructuredLogger +import org.typelevel.log4cats.{KernelLogLevel, LoggerKernel, SelfAwareStructuredLogger} +import org.typelevel.log4cats.Log import cats.effect.{Ref, Sync} import cats.syntax.all.* @@ -145,51 +146,82 @@ object StructuredTestingLogger { def isWarnEnabled: F[Boolean] = Sync[F].pure(warnEnabled) def isErrorEnabled: F[Boolean] = Sync[F].pure(errorEnabled) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val message = log.message() + val throwable = log.throwable + val context = log.context + + level match { + case KernelLogLevel.Trace => + if (traceEnabled) appendLogMessage(TRACE(message, throwable, context)) + else Sync[F].unit + case KernelLogLevel.Debug => + if (debugEnabled) appendLogMessage(DEBUG(message, throwable, context)) + else Sync[F].unit + case KernelLogLevel.Info => + if (infoEnabled) appendLogMessage(INFO(message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Warn => + if (warnEnabled) appendLogMessage(WARN(message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Error => + if (errorEnabled) appendLogMessage(ERROR(message, throwable, context)) + else Sync[F].unit + case _ => + if (errorEnabled) appendLogMessage(ERROR(message, throwable, context)) + else Sync[F].unit + } + } + } + private val noop = Sync[F].unit - def error(message: => String): F[Unit] = + override def error(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, None)) else noop - def error(t: Throwable)(message: => String): F[Unit] = + override def error(t: Throwable)(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, t.some)) else noop - def error(ctx: Map[String, String])(message: => String): F[Unit] = + override def error(ctx: Map[String, String])(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, None, ctx)) else noop - def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, t.some, ctx)) else noop - def warn(message: => String): F[Unit] = + override def warn(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, None)) else noop - def warn(t: Throwable)(message: => String): F[Unit] = + override def warn(t: Throwable)(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, t.some)) else noop - def warn(ctx: Map[String, String])(message: => String): F[Unit] = + override def warn(ctx: Map[String, String])(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, None, ctx)) else noop - def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, t.some, ctx)) else noop - def info(message: => String): F[Unit] = + override def info(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, None)) else noop - def info(t: Throwable)(message: => String): F[Unit] = + override def info(t: Throwable)(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, t.some)) else noop - def info(ctx: Map[String, String])(message: => String): F[Unit] = + override def info(ctx: Map[String, String])(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, None, ctx)) else noop - def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, t.some, ctx)) else noop - def debug(message: => String): F[Unit] = + override def debug(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, None)) else noop - def debug(t: Throwable)(message: => String): F[Unit] = + override def debug(t: Throwable)(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, t.some)) else noop - def debug(ctx: Map[String, String])(message: => String): F[Unit] = + override def debug(ctx: Map[String, String])(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, None, ctx)) else noop - def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, t.some, ctx)) else noop - def trace(message: => String): F[Unit] = + override def trace(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, None)) else noop - def trace(t: Throwable)(message: => String): F[Unit] = + override def trace(t: Throwable)(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, t.some)) else noop - def trace(ctx: Map[String, String])(message: => String): F[Unit] = + override def trace(ctx: Map[String, String])(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, None, ctx)) else noop - def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + override def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, t.some, ctx)) else noop } } diff --git a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLogger.scala b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLogger.scala index 80bc2700..9309c600 100644 --- a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLogger.scala +++ b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLogger.scala @@ -17,7 +17,8 @@ package org.typelevel.log4cats.testing import cats.data.Chain -import org.typelevel.log4cats.SelfAwareLogger +import org.typelevel.log4cats.{KernelLogLevel, LoggerKernel, SelfAwareLogger} +import org.typelevel.log4cats.Log import cats.effect.{Ref, Sync} import cats.syntax.all.* @@ -124,31 +125,57 @@ object TestingLogger { def isWarnEnabled: F[Boolean] = Sync[F].pure(warnEnabled) def isErrorEnabled: F[Boolean] = Sync[F].pure(errorEnabled) + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val message = log.message() + val throwable = log.throwable + + level match { + case KernelLogLevel.Trace => + if (traceEnabled) appendLogMessage(TRACE(message, throwable)) else Sync[F].unit + case KernelLogLevel.Debug => + if (debugEnabled) appendLogMessage(DEBUG(message, throwable)) else Sync[F].unit + case KernelLogLevel.Info => + if (infoEnabled) appendLogMessage(INFO(message, throwable)) else Sync[F].unit + case KernelLogLevel.Warn => + if (warnEnabled) appendLogMessage(WARN(message, throwable)) else Sync[F].unit + case KernelLogLevel.Error => + if (errorEnabled) appendLogMessage(ERROR(message, throwable)) else Sync[F].unit + case _ => + if (errorEnabled) appendLogMessage(ERROR(message, throwable)) else Sync[F].unit + } + } + } + private val noop = Sync[F].unit - def error(message: => String): F[Unit] = + override def error(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, None)) else noop - def error(t: Throwable)(message: => String): F[Unit] = + override def error(t: Throwable)(message: => String): F[Unit] = if (errorEnabled) appendLogMessage(ERROR(message, t.some)) else noop - def warn(message: => String): F[Unit] = + override def warn(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, None)) else noop - def warn(t: Throwable)(message: => String): F[Unit] = + override def warn(t: Throwable)(message: => String): F[Unit] = if (warnEnabled) appendLogMessage(WARN(message, t.some)) else noop - def info(message: => String): F[Unit] = + override def info(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, None)) else noop - def info(t: Throwable)(message: => String): F[Unit] = + override def info(t: Throwable)(message: => String): F[Unit] = if (infoEnabled) appendLogMessage(INFO(message, t.some)) else noop - def debug(message: => String): F[Unit] = + override def debug(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, None)) else noop - def debug(t: Throwable)(message: => String): F[Unit] = + override def debug(t: Throwable)(message: => String): F[Unit] = if (debugEnabled) appendLogMessage(DEBUG(message, t.some)) else noop - def trace(message: => String): F[Unit] = + override def trace(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, None)) else noop - def trace(t: Throwable)(message: => String): F[Unit] = + override def trace(t: Throwable)(message: => String): F[Unit] = if (traceEnabled) appendLogMessage(TRACE(message, t.some)) else noop } diff --git a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLoggerFactory.scala b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLoggerFactory.scala index c05c95e9..96079218 100644 --- a/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLoggerFactory.scala +++ b/testing/shared/src/main/scala/org/typelevel/log4cats/testing/TestingLoggerFactory.scala @@ -22,7 +22,13 @@ import cats.effect.{Ref, Sync} import cats.syntax.all.* import org.typelevel.log4cats.extras.LogLevel import org.typelevel.log4cats.testing.TestingLoggerFactory.LogMessage -import org.typelevel.log4cats.{LoggerFactory, SelfAwareStructuredLogger} +import org.typelevel.log4cats.{ + KernelLogLevel, + LoggerFactory, + LoggerKernel, + SelfAwareStructuredLogger +} +import org.typelevel.log4cats.Log import java.io.{PrintWriter, StringWriter} import java.util.concurrent.atomic.AtomicReference @@ -179,6 +185,33 @@ object TestingLoggerFactory { override val isWarnEnabled: F[Boolean] = warnEnabled.pure[F] override val isErrorEnabled: F[Boolean] = errorEnabled.pure[F] + protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { + def log( + level: KernelLogLevel, + logBuilder: Log.Builder[String] => Log.Builder[String] + ): F[Unit] = { + val log = logBuilder(Log.mutableBuilder[String]()).build() + val message = log.message() + val throwable = log.throwable + val context = log.context + + level match { + case KernelLogLevel.Trace => + if (traceEnabled) save(Trace(name, message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Debug => + if (debugEnabled) save(Debug(name, message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Info => + if (infoEnabled) save(Info(name, message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Warn => + if (warnEnabled) save(Warn(name, message, throwable, context)) else Sync[F].unit + case KernelLogLevel.Error => + if (errorEnabled) save(Error(name, message, throwable, context)) else Sync[F].unit + case _ => + if (errorEnabled) save(Error(name, message, throwable, context)) else Sync[F].unit + } + } + } + private val noop = Sync[F].unit override def trace(ctx: Map[String, String])(msg: => String): F[Unit] =