Skip to content

Commit cb2bb58

Browse files
committed
Add logger with Local semantics
Add `LocalLogContext` as the core abstraction for storing context locally, as well as for querying context from arbitrary `cats.mtl.Ask` instances (for example, to automatically include the current trace and span IDs in a log event's context). Add `LocalLogger` and `LocalLoggerFactory`.
1 parent 642b6a6 commit cb2bb58

File tree

7 files changed

+955
-1
lines changed

7 files changed

+955
-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+
}

0 commit comments

Comments
 (0)