Skip to content

Commit 542a508

Browse files
committed
Add logger with Local semantics
1 parent 642b6a6 commit 542a508

File tree

4 files changed

+435
-1
lines changed

4 files changed

+435
-1
lines changed

build.sbt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1")
2828

2929
val catsV = "2.13.0"
3030
val catsEffectV = "3.7.0-RC1"
31+
val catsMtlV = "1.6.0"
3132
val slf4jV = "1.7.36"
3233
val munitCatsEffectV = "2.2.0-RC1"
3334
val logbackClassicV = "1.2.13"
@@ -47,7 +48,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
4748
name := "log4cats-core",
4849
libraryDependencies ++= Seq(
4950
"org.typelevel" %%% "cats-core" % catsV,
50-
"org.typelevel" %%% "cats-effect-std" % catsEffectV
51+
"org.typelevel" %%% "cats-effect-std" % catsEffectV,
52+
"org.typelevel" %%% "cats-mtl" % catsMtlV
5153
),
5254
libraryDependencies ++= {
5355
if (tlIsScala3.value) Seq.empty
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import cats.mtl.{Ask, LiftKind, Local}
20+
import cats.syntax.functor.*
21+
import cats.syntax.traverse.*
22+
import cats.{Applicative, Show}
23+
24+
import scala.collection.immutable.ArraySeq
25+
26+
/**
27+
* Log context stored in a [[cats.mtl.Local `Local`]], as well as potentially additional log context
28+
* provided by [[cats.mtl.Ask `Ask`s]].
29+
*/
30+
sealed trait LocalLogContext[F[_]] {
31+
32+
/**
33+
* @return
34+
* the current log context stored [[cats.mtl.Local locally]], as well as the context from any
35+
* provided [[cats.mtl.Ask `Ask`]]s
36+
*/
37+
private[log4cats] def currentLogContext: F[Map[String, String]]
38+
39+
/**
40+
* @return
41+
* the given effect modified to have the provided context stored [[cats.mtl.Local locally]]
42+
*/
43+
private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A]
44+
45+
/**
46+
* @return
47+
* the given effect modified to have the provided context stored [[cats.mtl.Local locally]]
48+
*/
49+
private[log4cats] final def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] =
50+
withAddedContext {
51+
ctx.view.map { case (k, v) => k -> v.toString }.toMap
52+
}(fa)
53+
54+
/**
55+
* Modifies this [[cats.mtl.Local local]] log context to include the context provided by the given
56+
* [[cats.mtl.Ask `Ask`]] with higher priority than all of its current context; that is, if both
57+
* the `Ask` and this local log context provide values for some key, the value from the `Ask` will
58+
* be used. The context is asked for at every logging operation.
59+
*/
60+
def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F]
61+
62+
/**
63+
* Modifies this [[cats.mtl.Local local]] log context to include the context provided by the given
64+
* [[cats.mtl.Ask `Ask`]] with lower priority than all of its current context; that is, if both
65+
* the `Ask` and this local log context provide values for some key, the value from this local log
66+
* context will be used. The context is asked for at every logging operation.
67+
*/
68+
def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F]
69+
70+
/** Lifts this [[cats.mtl.Local local]] log context from `F` to `G`. */
71+
def liftTo[G[_]](implicit lift: LiftKind[F, G]): LocalLogContext[G]
72+
}
73+
74+
object LocalLogContext {
75+
private[this] type AskContext[F[_]] = Ask[F, Map[String, String]]
76+
77+
private[this] final class MultiAskContext[F[_]] private[MultiAskContext] (
78+
asks: Seq[AskContext[F]] /* never empty */
79+
) extends AskContext[F] {
80+
implicit def applicative: Applicative[F] = asks.head.applicative
81+
def ask[E2 >: Map[String, String]]: F[E2] =
82+
asks
83+
.traverse(_.ask[Map[String, String]])
84+
.map(_.reduceLeft(_ ++ _))
85+
def prependLowPriority(ask: AskContext[F]): MultiAskContext[F] =
86+
new MultiAskContext(ask +: asks)
87+
def appendHighPriority(ask: AskContext[F]): MultiAskContext[F] =
88+
new MultiAskContext(asks :+ ask)
89+
}
90+
91+
private[this] object MultiAskContext {
92+
def apply[F[_]](ask: AskContext[F]): MultiAskContext[F] =
93+
ask match {
94+
case multi: MultiAskContext[F] => multi
95+
case other => new MultiAskContext(ArraySeq(other))
96+
}
97+
}
98+
99+
private[this] final class Impl[F[_]](
100+
localCtx: Local[F, Map[String, String]],
101+
askCtx: AskContext[F]
102+
) extends LocalLogContext[F] {
103+
private[log4cats] def currentLogContext: F[Map[String, String]] =
104+
askCtx.ask[Map[String, String]]
105+
private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] =
106+
localCtx.local(fa)(_ ++ ctx)
107+
108+
def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] =
109+
new Impl(
110+
localCtx,
111+
MultiAskContext(askCtx).appendHighPriority(ask)
112+
)
113+
114+
def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] =
115+
new Impl(
116+
localCtx,
117+
MultiAskContext(askCtx).prependLowPriority(ask)
118+
)
119+
120+
def liftTo[G[_]](implicit lift: LiftKind[F, G]): LocalLogContext[G] = {
121+
val localF = localCtx
122+
val askF = askCtx
123+
val localG = localF.liftTo[G]
124+
val askG =
125+
if (askF eq localF) localG
126+
else askF.liftTo[G]
127+
new Impl(localG, askG)
128+
}
129+
}
130+
131+
/** @return a `LocalLogContext` backed by the given implicit [[cats.mtl.Local `Local`]] */
132+
def fromLocal[F[_]](implicit localCtx: Local[F, Map[String, String]]): LocalLogContext[F] =
133+
new Impl(localCtx, localCtx)
134+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import cats.mtl.{LiftKind, Local}
20+
import cats.syntax.functor.*
21+
import cats.{~>, Monad, Show}
22+
23+
sealed trait LocalLogger[F[_]] extends SelfAwareLogger[F] {
24+
def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A]
25+
26+
def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A]
27+
28+
def error(ctx: Map[String, String])(msg: => String): F[Unit]
29+
def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit]
30+
def warn(ctx: Map[String, String])(msg: => String): F[Unit]
31+
def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit]
32+
def info(ctx: Map[String, String])(msg: => String): F[Unit]
33+
def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit]
34+
def debug(ctx: Map[String, String])(msg: => String): F[Unit]
35+
def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit]
36+
def trace(ctx: Map[String, String])(msg: => String): F[Unit]
37+
def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit]
38+
39+
@deprecated(
40+
"use the overload that takes a `LiftKind` and `Monad` instead",
41+
since = "2.8.0"
42+
)
43+
override def mapK[G[_]](fk: F ~> G): SelfAwareLogger[G] = super.mapK(fk)
44+
45+
def liftTo[G[_]](implicit lift: LiftKind[F, G], G: Monad[G]): LocalLogger[G]
46+
47+
override def withModifiedString(f: String => String): LocalLogger[F]
48+
49+
@deprecated(
50+
"`StructuredLogger` is cumbersome and lacks `cats.mtl.Local` semantics",
51+
since = "2.8.0"
52+
)
53+
def asStructuredLogger: SelfAwareStructuredLogger[F]
54+
}
55+
56+
object LocalLogger {
57+
private[this] final class Impl[F[_]](
58+
localLogContext: LocalLogContext[F],
59+
underlying: SelfAwareStructuredLogger[F]
60+
)(implicit F: Monad[F])
61+
extends LocalLogger[F]
62+
with SelfAwareStructuredLogger[F] {
63+
def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] =
64+
localLogContext.withAddedContext(ctx)(fa)
65+
def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] =
66+
localLogContext.withAddedContext(ctx*)(fa)
67+
68+
override def addContext(ctx: Map[String, String]): Impl[F] =
69+
new Impl(localLogContext, underlying.addContext(ctx))
70+
override def addContext(pairs: (String, Show.Shown)*): Impl[F] =
71+
new Impl(localLogContext, underlying.addContext(pairs*))
72+
73+
def isErrorEnabled: F[Boolean] = underlying.isErrorEnabled
74+
def isWarnEnabled: F[Boolean] = underlying.isWarnEnabled
75+
def isInfoEnabled: F[Boolean] = underlying.isInfoEnabled
76+
def isDebugEnabled: F[Boolean] = underlying.isDebugEnabled
77+
def isTraceEnabled: F[Boolean] = underlying.isTraceEnabled
78+
79+
@deprecated(
80+
"use the overload that takes a `LiftKind` and `Monad` instead",
81+
since = "2.8.0"
82+
)
83+
override def mapK[G[_]](fk: F ~> G): SelfAwareStructuredLogger[G] =
84+
super.mapK(fk)
85+
def liftTo[G[_]](implicit lift: LiftKind[F, G], G: Monad[G]): LocalLogger[G] =
86+
new Impl(localLogContext.liftTo[G], underlying.mapK(lift))
87+
override def withModifiedString(f: String => String): Impl[F] =
88+
new Impl(localLogContext, underlying.withModifiedString(f))
89+
90+
@deprecated(
91+
"`StructuredLogger` is cumbersome and lacks `cats.mtl.Local` semantics",
92+
since = "2.8.0"
93+
)
94+
def asStructuredLogger: SelfAwareStructuredLogger[F] = this
95+
96+
def error(message: => String): F[Unit] =
97+
F.ifM(underlying.isErrorEnabled)(
98+
for (localCtx <- localLogContext.currentLogContext)
99+
yield underlying.error(localCtx)(message),
100+
F.unit
101+
)
102+
def error(t: Throwable)(message: => String): F[Unit] =
103+
F.ifM(underlying.isErrorEnabled)(
104+
for (localCtx <- localLogContext.currentLogContext)
105+
yield underlying.error(localCtx, t)(message),
106+
F.unit
107+
)
108+
def error(ctx: Map[String, String])(msg: => String): F[Unit] =
109+
F.ifM(underlying.isErrorEnabled)(
110+
for (localCtx <- localLogContext.currentLogContext)
111+
yield underlying.error(localCtx ++ ctx)(msg),
112+
F.unit
113+
)
114+
def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
115+
F.ifM(underlying.isErrorEnabled)(
116+
for (localCtx <- localLogContext.currentLogContext)
117+
yield underlying.error(localCtx ++ ctx, t)(msg),
118+
F.unit
119+
)
120+
121+
def warn(message: => String): F[Unit] =
122+
F.ifM(underlying.isWarnEnabled)(
123+
for (localCtx <- localLogContext.currentLogContext)
124+
yield underlying.warn(localCtx)(message),
125+
F.unit
126+
)
127+
def warn(t: Throwable)(message: => String): F[Unit] =
128+
F.ifM(underlying.isWarnEnabled)(
129+
for (localCtx <- localLogContext.currentLogContext)
130+
yield underlying.warn(localCtx, t)(message),
131+
F.unit
132+
)
133+
def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
134+
F.ifM(underlying.isWarnEnabled)(
135+
for (localCtx <- localLogContext.currentLogContext)
136+
yield underlying.warn(localCtx ++ ctx)(msg),
137+
F.unit
138+
)
139+
def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
140+
F.ifM(underlying.isWarnEnabled)(
141+
for (localCtx <- localLogContext.currentLogContext)
142+
yield underlying.warn(localCtx ++ ctx, t)(msg),
143+
F.unit
144+
)
145+
146+
def info(message: => String): F[Unit] =
147+
F.ifM(underlying.isInfoEnabled)(
148+
for (localCtx <- localLogContext.currentLogContext)
149+
yield underlying.info(localCtx)(message),
150+
F.unit
151+
)
152+
def info(t: Throwable)(message: => String): F[Unit] =
153+
F.ifM(underlying.isInfoEnabled)(
154+
for (localCtx <- localLogContext.currentLogContext)
155+
yield underlying.info(localCtx, t)(message),
156+
F.unit
157+
)
158+
def info(ctx: Map[String, String])(msg: => String): F[Unit] =
159+
F.ifM(underlying.isInfoEnabled)(
160+
for (localCtx <- localLogContext.currentLogContext)
161+
yield underlying.info(localCtx ++ ctx)(msg),
162+
F.unit
163+
)
164+
def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
165+
F.ifM(underlying.isInfoEnabled)(
166+
for (localCtx <- localLogContext.currentLogContext)
167+
yield underlying.info(localCtx ++ ctx, t)(msg),
168+
F.unit
169+
)
170+
171+
def debug(message: => String): F[Unit] =
172+
F.ifM(underlying.isDebugEnabled)(
173+
for (localCtx <- localLogContext.currentLogContext)
174+
yield underlying.debug(localCtx)(message),
175+
F.unit
176+
)
177+
def debug(t: Throwable)(message: => String): F[Unit] =
178+
F.ifM(underlying.isDebugEnabled)(
179+
for (localCtx <- localLogContext.currentLogContext)
180+
yield underlying.debug(localCtx, t)(message),
181+
F.unit
182+
)
183+
def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
184+
F.ifM(underlying.isDebugEnabled)(
185+
for (localCtx <- localLogContext.currentLogContext)
186+
yield underlying.debug(localCtx ++ ctx)(msg),
187+
F.unit
188+
)
189+
def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
190+
F.ifM(underlying.isDebugEnabled)(
191+
for (localCtx <- localLogContext.currentLogContext)
192+
yield underlying.debug(localCtx ++ ctx, t)(msg),
193+
F.unit
194+
)
195+
196+
def trace(message: => String): F[Unit] =
197+
F.ifM(underlying.isTraceEnabled)(
198+
for (localCtx <- localLogContext.currentLogContext)
199+
yield underlying.trace(localCtx)(message),
200+
F.unit
201+
)
202+
def trace(t: Throwable)(message: => String): F[Unit] =
203+
F.ifM(underlying.isTraceEnabled)(
204+
for (localCtx <- localLogContext.currentLogContext)
205+
yield underlying.trace(localCtx, t)(message),
206+
F.unit
207+
)
208+
def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
209+
F.ifM(underlying.isTraceEnabled)(
210+
for (localCtx <- localLogContext.currentLogContext)
211+
yield underlying.trace(localCtx ++ ctx)(msg),
212+
F.unit
213+
)
214+
def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
215+
F.ifM(underlying.isTraceEnabled)(
216+
for (localCtx <- localLogContext.currentLogContext)
217+
yield underlying.trace(localCtx ++ ctx, t)(msg),
218+
F.unit
219+
)
220+
}
221+
222+
def apply[F[_]: Monad](
223+
localLogContext: LocalLogContext[F],
224+
underlying: SelfAwareStructuredLogger[F]
225+
): LocalLogger[F] =
226+
new Impl(localLogContext, underlying)
227+
228+
def fromLocal[F[_]: Monad](
229+
underlying: SelfAwareStructuredLogger[F]
230+
)(implicit localCtx: Local[F, Map[String, String]]): LocalLogger[F] =
231+
apply(LocalLogContext.fromLocal, underlying)
232+
}

0 commit comments

Comments
 (0)