Skip to content

Commit

Permalink
Merge pull request #719 from morgen-peschke/add-writerT-structured-lo…
Browse files Browse the repository at this point in the history
…gger

Add structured equivalents of Writer & WriterT loggers
  • Loading branch information
danicheg committed Apr 16, 2023
2 parents eee8df8 + 2aa5f85 commit 95e26a1
Show file tree
Hide file tree
Showing 9 changed files with 531 additions and 104 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ class LoggerUsingService[F[_]: LoggerFactory: Monad] {
new LoggerUsingService[IO].use("foo")
```

## Using log4cats in tests

See [here](testing/README.md) for details

## CVE-2021-44228 ("log4shell")

log4cats is not directly susceptible to CVS-2021-44228. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,27 @@ package org.typelevel.log4cats.extras

import cats._
import cats.syntax.all._
import org.typelevel.log4cats.Logger

final case class LogMessage(level: LogLevel, t: Option[Throwable], message: String)

object LogMessage {
implicit val logMessageShow: Show[LogMessage] =
Show.show[LogMessage](l => show"LogMessage(${l.level},${l.t.map(_.getMessage)},${l.message})")

def log[F[_]](sm: LogMessage, l: Logger[F]): F[Unit] = sm match {
case LogMessage(LogLevel.Trace, Some(t), m) => l.trace(t)(m)
case LogMessage(LogLevel.Trace, None, m) => l.trace(m)

case LogMessage(LogLevel.Debug, Some(t), m) => l.debug(t)(m)
case LogMessage(LogLevel.Debug, None, m) => l.debug(m)

case LogMessage(LogLevel.Info, Some(t), m) => l.info(t)(m)
case LogMessage(LogLevel.Info, None, m) => l.info(m)

case LogMessage(LogLevel.Warn, Some(t), m) => l.warn(t)(m)
case LogMessage(LogLevel.Warn, None, m) => l.warn(m)

case LogMessage(LogLevel.Error, Some(t), m) => l.error(t)(m)
case LogMessage(LogLevel.Error, None, m) => l.error(m)
}
}
48 changes: 48 additions & 0 deletions core/shared/src/main/scala/org/typelevel/log4cats/extras/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Important notes about `Writer*Logger`s
======================================

The loggers provided here backed by `Writer` and `WriterT` come with some important caveats that
you should be aware of before using.

General Notes
-------------

> **Note**
> These loggers tie their logs to the lifecycle of the return value, so they're generally only useful
when the logs have a similar lifecycle.

> **Warning**
> These loggers should not be used in situations where the logs would be needed if an error occurs (including timeouts).
Basically, they're a way to use `Writer` or `WriterT` with the `log4cats` API. No additional guarantees
are provided. Annotating the happy path is one example of a good use-case for these loggers.

Better alternatives are provided by the `testing` module:
- If a `SelfAwareLogger` is needed for test code, consider
`org.typelevel.log4cats.testing.TestingLogger` over `WriterLogger`

- If a `SelfAwareStructuredLogger` is needed for test code, consider
`org.typelevel.log4cats.testing.StructuredTestingLogger` over `WriterStructuredLogger`

`WriterLogger` / `WriterStructureLogger`
----------------------------------------

> **Warning**
> Expect to lose logs if an exception occurs
These are built using `Writer`, which does not directly interact with effects, so expect to do a
non-trivial amount of plumbing if you're planning on using them. Otherwise, if the logs don't matter
in the presence of errors in the context you're using them, they're fine.

`WriterTLogger` / `WriterTStructuredLogger`
-------------------------------------------

These are built using `WriterT`, and are much easier to use with effects. Running the `WriterT`
instance will yield a value of type `F[(G[LogMessage], A)]`.

> **Warning**
> Logged messages can be materialized if *and only if* `F succeeds`
Unfortunately, because of the way that cancellation (and thus timeouts) is handled by
`cats.effect.IO`, in practice `WriterT` isn't a great fit for anything which can timeout.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.Show
import cats.syntax.all._
import org.typelevel.log4cats.StructuredLogger

final case class StructuredLogMessage(
level: LogLevel,
context: Map[String, String],
throwableOpt: Option[Throwable],
message: String
)
object StructuredLogMessage {
def log[F[_]](sm: StructuredLogMessage, l: StructuredLogger[F]): F[Unit] = sm match {
case StructuredLogMessage(LogLevel.Trace, ctx, Some(t), m) => l.trace(ctx, t)(m)
case StructuredLogMessage(LogLevel.Trace, ctx, None, m) => l.trace(ctx)(m)

case StructuredLogMessage(LogLevel.Debug, ctx, Some(t), m) => l.debug(ctx, t)(m)
case StructuredLogMessage(LogLevel.Debug, ctx, None, m) => l.debug(ctx)(m)

case StructuredLogMessage(LogLevel.Info, ctx, Some(t), m) => l.info(ctx, t)(m)
case StructuredLogMessage(LogLevel.Info, ctx, None, m) => l.info(ctx)(m)

case StructuredLogMessage(LogLevel.Warn, ctx, Some(t), m) => l.warn(ctx, t)(m)
case StructuredLogMessage(LogLevel.Warn, ctx, None, m) => l.warn(ctx)(m)

case StructuredLogMessage(LogLevel.Error, ctx, Some(t), m) => l.error(ctx, t)(m)
case StructuredLogMessage(LogLevel.Error, ctx, None, m) => l.error(ctx)(m)
}

implicit val structuredLogMessageShow: Show[StructuredLogMessage] =
Show.show[StructuredLogMessage] { l =>
show"StructuredLogMessage(${l.level},${l.context},${l.throwableOpt.map(_.getMessage)},${l.message})"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import cats.data._
import cats.syntax.all._
import org.typelevel.log4cats._

/**
* A `SelfAwareLogger` implemented using `cats.data.Writer`.
*
* >>> 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! <<<
*
* If a `SelfAwareLogger` is needed for test code, the `testing` module provides a better option:
* `org.typelevel.log4cats.testing.TestingLogger`
*/
object WriterLogger {

def apply[G[_]: Alternative](
Expand All @@ -29,89 +39,14 @@ object WriterLogger {
infoEnabled: Boolean = true,
warnEnabled: Boolean = true,
errorEnabled: Boolean = true
): SelfAwareLogger[Writer[G[LogMessage], *]] = {
implicit val monoidGLogMessage: Monoid[G[LogMessage]] = Alternative[G].algebra[LogMessage]
new SelfAwareLogger[Writer[G[LogMessage], *]] {
def isTraceEnabled: Writer[G[LogMessage], Boolean] =
Writer.value[G[LogMessage], Boolean](traceEnabled)
def isDebugEnabled: Writer[G[LogMessage], Boolean] =
Writer.value[G[LogMessage], Boolean](debugEnabled)
def isInfoEnabled: Writer[G[LogMessage], Boolean] =
Writer.value[G[LogMessage], Boolean](infoEnabled)
def isWarnEnabled: Writer[G[LogMessage], Boolean] =
Writer.value[G[LogMessage], Boolean](warnEnabled)
def isErrorEnabled: Writer[G[LogMessage], Boolean] =
Writer.value[G[LogMessage], Boolean](errorEnabled)

def debug(t: Throwable)(message: => String): Writer[G[LogMessage], Unit] =
if (debugEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Debug, t.some, message)))
else Writer.value[G[LogMessage], Unit](())
def error(t: Throwable)(message: => String): Writer[G[LogMessage], Unit] =
if (errorEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Error, t.some, message)))
else Writer.value[G[LogMessage], Unit](())
def info(t: Throwable)(message: => String): Writer[G[LogMessage], Unit] =
if (infoEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Info, t.some, message)))
else Writer.value[G[LogMessage], Unit](())
def trace(t: Throwable)(message: => String): Writer[G[LogMessage], Unit] =
if (traceEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Trace, t.some, message)))
else Writer.value[G[LogMessage], Unit](())
def warn(t: Throwable)(message: => String): Writer[G[LogMessage], Unit] =
if (warnEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Warn, t.some, message)))
else Writer.value[G[LogMessage], Unit](())
def debug(message: => String): Writer[G[LogMessage], Unit] =
if (debugEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Debug, None, message)))
else Writer.value[G[LogMessage], Unit](())
def error(message: => String): Writer[G[LogMessage], Unit] =
if (errorEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Error, None, message)))
else Writer.value[G[LogMessage], Unit](())
def info(message: => String): Writer[G[LogMessage], Unit] =
if (infoEnabled) Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Info, None, message)))
else Writer.value[G[LogMessage], Unit](())
def trace(message: => String): Writer[G[LogMessage], Unit] =
if (traceEnabled)
Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Trace, None, message)))
else Writer.value[G[LogMessage], Unit](())
def warn(message: => String): Writer[G[LogMessage], Unit] =
if (warnEnabled) Writer.tell(Alternative[G].pure(LogMessage(LogLevel.Warn, None, message)))
else Writer.value[G[LogMessage], Unit](())
}
}
): SelfAwareLogger[Writer[G[LogMessage], *]] =
WriterTLogger[cats.Id, G](traceEnabled, debugEnabled, infoEnabled, warnEnabled, errorEnabled)

def run[F[_]: Applicative, G[_]: Foldable](l: Logger[F]): Writer[G[LogMessage], *] ~> F =
new ~>[Writer[G[LogMessage], *], F] {
def logMessage(logMessage: LogMessage): F[Unit] = logMessage match {
case LogMessage(LogLevel.Error, Some(t), m) =>
l.error(t)(m)
case LogMessage(LogLevel.Error, None, m) =>
l.error(m)
case LogMessage(LogLevel.Warn, Some(t), m) =>
l.warn(t)(m)
case LogMessage(LogLevel.Warn, None, m) =>
l.warn(m)
case LogMessage(LogLevel.Info, Some(t), m) =>
l.info(t)(m)
case LogMessage(LogLevel.Info, None, m) =>
l.info(m)
case LogMessage(LogLevel.Debug, Some(t), m) =>
l.debug(t)(m)
case LogMessage(LogLevel.Debug, None, m) =>
l.debug(m)
case LogMessage(LogLevel.Trace, Some(t), m) =>
l.trace(t)(m)
case LogMessage(LogLevel.Trace, None, m) =>
l.trace(m)
}

new (Writer[G[LogMessage], *] ~> F) {
def apply[A](fa: Writer[G[LogMessage], A]): F[A] = {
val (toLog, out) = fa.run
toLog.traverse_(logMessage).as(out)
toLog.traverse_(LogMessage.log(_, l)).as(out)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.Writer
import cats.syntax.all._
import cats.{~>, Alternative, Applicative, Foldable, Id}
import org.typelevel.log4cats.{SelfAwareStructuredLogger, StructuredLogger}

/**
* A `SelfAwareStructuredLogger` implemented using `cats.data.Writer`.
*
* >>> 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! <<<
*
* If a `SelfAwareStructuredLogger` is needed for test code, the `testing` module provides a better
* option: `org.typelevel.log4cats.testing.StructuredTestingLogger`
*/
object WriterStructuredLogger {
def apply[G[_]: Alternative](
traceEnabled: Boolean = true,
debugEnabled: Boolean = true,
infoEnabled: Boolean = true,
warnEnabled: Boolean = true,
errorEnabled: Boolean = true
): SelfAwareStructuredLogger[Writer[G[StructuredLogMessage], *]] =
WriterTStructuredLogger[Id, G](
traceEnabled,
debugEnabled,
infoEnabled,
warnEnabled,
errorEnabled
)

def run[F[_]: Applicative, G[_]: Foldable](
l: StructuredLogger[F]
): Writer[G[StructuredLogMessage], *] ~> F =
new (Writer[G[StructuredLogMessage], *] ~> F) {
def apply[A](fa: Writer[G[StructuredLogMessage], A]): F[A] = {
val (toLog, out) = fa.run
toLog.traverse_(StructuredLogMessage.log(_, l)).as(out)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ import cats.syntax.all._
import org.typelevel.log4cats._

/**
* >>> WARNING READ BEFORE USAGE! <<< This logger will NOT log anything if `F` fails!
* A `SelfAwareLogger` implemented using `cats.data.WriterT`.
*
* Running the `WriterT` instance will yield a value of type `F[(G[LogMessage], A)]`. As a result,
* the logged messages can be materialized if and only `F` succeeds.
* >>> 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! <<<
*
* If a `SelfAwareLogger` is needed for test code, the `testing` module provides a better option:
* `org.typelevel.log4cats.testing.TestingLogger`
*/
object WriterTLogger {
def apply[F[_]: Applicative, G[_]: Alternative](
Expand Down Expand Up @@ -85,28 +89,10 @@ object WriterTLogger {
}

def run[F[_]: Monad, G[_]: Foldable](l: Logger[F]): WriterT[F, G[LogMessage], *] ~> F =
new ~>[WriterT[F, G[LogMessage], *], F] {
override def apply[A](fa: WriterT[F, G[LogMessage], A]): F[A] = {
def logMessage(logMessage: LogMessage): F[Unit] = logMessage match {
case LogMessage(LogLevel.Trace, Some(t), m) => l.trace(t)(m)
case LogMessage(LogLevel.Trace, None, m) => l.trace(m)

case LogMessage(LogLevel.Debug, Some(t), m) => l.debug(t)(m)
case LogMessage(LogLevel.Debug, None, m) => l.debug(m)

case LogMessage(LogLevel.Info, Some(t), m) => l.info(t)(m)
case LogMessage(LogLevel.Info, None, m) => l.info(m)

case LogMessage(LogLevel.Warn, Some(t), m) => l.warn(t)(m)
case LogMessage(LogLevel.Warn, None, m) => l.warn(m)

case LogMessage(LogLevel.Error, Some(t), m) => l.error(t)(m)
case LogMessage(LogLevel.Error, None, m) => l.error(m)
}

new (WriterT[F, G[LogMessage], *] ~> F) {
override def apply[A](fa: WriterT[F, G[LogMessage], A]): F[A] =
fa.run.flatMap { case (toLog, out) =>
toLog.traverse_(logMessage).as(out)
toLog.traverse_(LogMessage.log(_, l)).as(out)
}
}
}
}
Loading

0 comments on commit 95e26a1

Please sign in to comment.