-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #835 from morgen-peschke/deferrable-loggers
Initial attempt at a logger that allows deferred conditional logging
- Loading branch information
Showing
14 changed files
with
2,315 additions
and
37 deletions.
There are no files selected for viewing
148 changes: 148 additions & 0 deletions
148
core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogMessage.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* | ||
* 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.extras | ||
|
||
import cats.syntax.show.* | ||
import cats.Show | ||
import cats.kernel.Hash | ||
import org.typelevel.log4cats.{Logger, StructuredLogger} | ||
import org.typelevel.log4cats.extras.DeferredLogMessage.{ | ||
deferredStructuredLogMessageHash, | ||
deferredStructuredLogMessageShow | ||
} | ||
|
||
/** | ||
* `StructuredLogMessage` has a bug that can't be fixed without breaking bincompat (because it's a | ||
* `case class`), but it's only used in the `Writer*Logger`s, so it's not a huge deal. | ||
* | ||
* The issue is that the API of the `*Logger` classes has a by-name parameter for the message, and | ||
* `StructuredLogMessage` (and by extension, the `Writer*Logger`) don't lazily compute the message. | ||
* | ||
* At some point, this should be renamed to `StructuredLogMessage` and replace the old class. | ||
*/ | ||
sealed trait DeferredLogMessage { | ||
def level: LogLevel | ||
def context: Map[String, String] | ||
def throwableOpt: Option[Throwable] | ||
def message: () => String | ||
|
||
def log[F[_]](logger: Logger[F]): F[Unit] = { | ||
level match { | ||
case LogLevel.Error => | ||
throwableOpt match { | ||
case Some(e) => logger.error(e)(message()) | ||
case None => logger.error(message()) | ||
} | ||
case LogLevel.Warn => | ||
throwableOpt match { | ||
case Some(e) => logger.warn(e)(message()) | ||
case None => logger.warn(message()) | ||
} | ||
case LogLevel.Info => | ||
throwableOpt match { | ||
case Some(e) => logger.info(e)(message()) | ||
case None => logger.info(message()) | ||
} | ||
case LogLevel.Debug => | ||
throwableOpt match { | ||
case Some(e) => logger.debug(e)(message()) | ||
case None => logger.debug(message()) | ||
} | ||
case LogLevel.Trace => | ||
throwableOpt match { | ||
case Some(e) => logger.trace(e)(message()) | ||
case None => logger.trace(message()) | ||
} | ||
} | ||
} | ||
|
||
def logStructured[F[_]](logger: StructuredLogger[F]): F[Unit] = { | ||
level match { | ||
case LogLevel.Error => | ||
throwableOpt match { | ||
case Some(e) => logger.error(context, e)(message()) | ||
case None => logger.error(context)(message()) | ||
} | ||
case LogLevel.Warn => | ||
throwableOpt match { | ||
case Some(e) => logger.warn(context, e)(message()) | ||
case None => logger.warn(context)(message()) | ||
} | ||
case LogLevel.Info => | ||
throwableOpt match { | ||
case Some(e) => logger.info(context, e)(message()) | ||
case None => logger.info(context)(message()) | ||
} | ||
case LogLevel.Debug => | ||
throwableOpt match { | ||
case Some(e) => logger.debug(context, e)(message()) | ||
case None => logger.debug(context)(message()) | ||
} | ||
case LogLevel.Trace => | ||
throwableOpt match { | ||
case Some(e) => logger.trace(context, e)(message()) | ||
case None => logger.trace(context)(message()) | ||
} | ||
} | ||
} | ||
|
||
override def equals(obj: Any): Boolean = obj match { | ||
case other: DeferredLogMessage => deferredStructuredLogMessageHash.eqv(this, other) | ||
case _ => false | ||
} | ||
|
||
override def hashCode(): Int = deferredStructuredLogMessageHash.hash(this) | ||
|
||
override def toString: String = deferredStructuredLogMessageShow.show(this) | ||
} | ||
object DeferredLogMessage { | ||
def apply( | ||
l: LogLevel, | ||
c: Map[String, String], | ||
t: Option[Throwable], | ||
m: () => String | ||
): DeferredLogMessage = | ||
new DeferredLogMessage { | ||
override val level: LogLevel = l | ||
override val context: Map[String, String] = c | ||
override val throwableOpt: Option[Throwable] = t | ||
override val message: () => String = m | ||
} | ||
|
||
def trace(c: Map[String, String], t: Option[Throwable], m: () => String): DeferredLogMessage = | ||
apply(LogLevel.Trace, c, t, m) | ||
|
||
def debug(c: Map[String, String], t: Option[Throwable], m: () => String): DeferredLogMessage = | ||
apply(LogLevel.Debug, c, t, m) | ||
|
||
def info(c: Map[String, String], t: Option[Throwable], m: () => String): DeferredLogMessage = | ||
apply(LogLevel.Info, c, t, m) | ||
|
||
def warn(c: Map[String, String], t: Option[Throwable], m: () => String): DeferredLogMessage = | ||
apply(LogLevel.Warn, c, t, m) | ||
|
||
def error(c: Map[String, String], t: Option[Throwable], m: () => String): DeferredLogMessage = | ||
apply(LogLevel.Error, c, t, m) | ||
|
||
implicit val deferredStructuredLogMessageHash: Hash[DeferredLogMessage] = Hash.by { l => | ||
(l.level, l.context, l.throwableOpt.map(_.getMessage), l.message()) | ||
} | ||
|
||
implicit val deferredStructuredLogMessageShow: Show[DeferredLogMessage] = Show.show { l => | ||
show"DeferredStructuredLogMessage(${l.level},${l.context},${l.throwableOpt.map(_.getMessage)},${l.message()})" | ||
} | ||
} |
142 changes: 142 additions & 0 deletions
142
core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredLogger.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/* | ||
* 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.extras | ||
|
||
import cats.data.Chain | ||
import cats.effect.kernel.Resource.ExitCase | ||
import cats.effect.kernel.{Concurrent, Ref, Resource} | ||
import cats.syntax.all._ | ||
import cats.~> | ||
import org.typelevel.log4cats.Logger | ||
|
||
/** | ||
* `Logger` that does not immediately log. | ||
* | ||
* Similar in idea to `WriterLogger`, but a bit safer. This will not lose logs when the effect | ||
* fails, instead logging when the resource is cancelled or fails. | ||
* | ||
* This can be used to implement failure-only logging. | ||
* {{{ | ||
* def handleRequest[F[_](request: Request[F], logger: StructuredLogger[F]): OptionT[F, Response[F]] = ??? | ||
* | ||
* HttpRoutes[F] { req => | ||
* DeferredLogger[F](logger) | ||
* .mapK(OptionT.liftK[F]) | ||
* .use { logger => | ||
* handleRequest(request, deferredLogger).flatTap { response => | ||
* deferredLogger.log.unlessA(response.status.isSuccess) | ||
* } | ||
* } | ||
* } | ||
* }}} | ||
* | ||
* >>> WARNING: READ BEFORE USAGE! <<< | ||
* https://github.com/typelevel/log4cats/blob/main/core/shared/src/main/scala/org/typelevel/log4cats/extras/README.md | ||
* >>> WARNING: READ BEFORE USAGE! <<< | ||
*/ | ||
trait DeferredLogger[F[_]] extends Logger[F] with DeferredLogging[F] { | ||
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) | ||
} | ||
object DeferredLogger { | ||
def apply[F[_]](logger: Logger[F])(implicit F: Concurrent[F]): Resource[F, DeferredLogger[F]] = | ||
Resource | ||
.makeCase(Ref.empty[F, Chain[DeferredLogMessage]]) { (ref, exitCase) => | ||
exitCase match { | ||
case ExitCase.Succeeded => F.unit | ||
case _ => ref.get.flatMap(_.traverse_(_.log(logger))) | ||
} | ||
} | ||
.map { ref => | ||
new DeferredLogger[F] { | ||
private def save(lm: DeferredLogMessage): F[Unit] = ref.update(_.append(lm)) | ||
|
||
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] = | ||
save(DeferredLogMessage.debug(Map.empty, t.some, () => msg)) | ||
override def info(t: Throwable)(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.info(Map.empty, t.some, () => msg)) | ||
override def warn(t: Throwable)(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.warn(Map.empty, t.some, () => msg)) | ||
override def error(t: Throwable)(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.error(Map.empty, t.some, () => msg)) | ||
|
||
override def trace(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.trace(Map.empty, none, () => msg)) | ||
override def debug(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.debug(Map.empty, none, () => msg)) | ||
override def info(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.info(Map.empty, none, () => msg)) | ||
override def warn(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.warn(Map.empty, none, () => msg)) | ||
override def error(msg: => String): F[Unit] = | ||
save(DeferredLogMessage.error(Map.empty, none, () => msg)) | ||
|
||
override def inspect: F[Chain[DeferredLogMessage]] = ref.get | ||
|
||
override def log: F[Unit] = ref.getAndSet(Chain.empty).flatMap(_.traverse_(_.log(logger))) | ||
} | ||
} | ||
|
||
private def mapK[F[_], G[_]]( | ||
logger: DeferredLogger[F], | ||
fk: F ~> G | ||
): DeferredLogger[G] = | ||
new DeferredLogger[G] { | ||
override def inspect: G[Chain[DeferredLogMessage]] = fk(logger.inspect) | ||
override def log: G[Unit] = fk(logger.log) | ||
|
||
override def trace(t: Throwable)(message: => String): G[Unit] = fk(logger.trace(t)(message)) | ||
override def debug(t: Throwable)(message: => String): G[Unit] = fk(logger.debug(t)(message)) | ||
override def info(t: Throwable)(message: => String): G[Unit] = fk(logger.info(t)(message)) | ||
override def warn(t: Throwable)(message: => String): G[Unit] = fk(logger.warn(t)(message)) | ||
override def error(t: Throwable)(message: => String): G[Unit] = fk(logger.error(t)(message)) | ||
|
||
override def trace(message: => String): G[Unit] = fk(logger.trace(message)) | ||
override def debug(message: => String): G[Unit] = fk(logger.debug(message)) | ||
override def info(message: => String): G[Unit] = fk(logger.info(message)) | ||
override def warn(message: => String): G[Unit] = fk(logger.warn(message)) | ||
override def error(message: => String): G[Unit] = fk(logger.error(message)) | ||
|
||
override def withModifiedString(f: String => String): DeferredLogger[G] = | ||
DeferredLogger.withModifiedString(this, f) | ||
override def mapK[H[_]](fk: G ~> H): DeferredLogger[H] = DeferredLogger.mapK(this, fk) | ||
} | ||
|
||
def withModifiedString[F[_]]( | ||
logger: DeferredLogger[F], | ||
f: String => String | ||
): DeferredLogger[F] = | ||
new DeferredLogger[F] { | ||
override def inspect: F[Chain[DeferredLogMessage]] = logger.inspect | ||
override def log: F[Unit] = logger.log | ||
|
||
override def trace(t: Throwable)(message: => String): F[Unit] = logger.trace(t)(f(message)) | ||
override def debug(t: Throwable)(message: => String): F[Unit] = logger.debug(t)(f(message)) | ||
override def info(t: Throwable)(message: => String): F[Unit] = logger.info(t)(f(message)) | ||
override def warn(t: Throwable)(message: => String): F[Unit] = logger.warn(t)(f(message)) | ||
override def error(t: Throwable)(message: => String): F[Unit] = logger.error(t)(f(message)) | ||
|
||
override def trace(message: => String): F[Unit] = logger.trace(f(message)) | ||
override def debug(message: => String): F[Unit] = logger.debug(f(message)) | ||
override def info(message: => String): F[Unit] = logger.info(f(message)) | ||
override def warn(message: => String): F[Unit] = logger.warn(f(message)) | ||
override def error(message: => String): F[Unit] = logger.error(f(message)) | ||
} | ||
} |
Oops, something went wrong.