Skip to content

Commit d0bf3e7

Browse files
committed
Allow adding effect's contextual environment to the log context
1 parent 77bd29d commit d0bf3e7

File tree

8 files changed

+608
-2
lines changed

8 files changed

+608
-2
lines changed

build.sbt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1")
2828

2929
val catsV = "2.10.0"
3030
val catsEffectV = "3.5.2"
31+
val catsMtlV = "1.4.0"
3132
val slf4jV = "1.7.36"
3233
val munitCatsEffectV = "2.0.0-M3"
3334
val logbackClassicV = "1.2.12"
3435

3536
Global / onChangedBuildSource := ReloadOnSourceChanges
3637

37-
lazy val root = tlCrossRootProject.aggregate(core, testing, noop, slf4j, docs, `js-console`)
38+
lazy val root = tlCrossRootProject.aggregate(core, mtl, testing, noop, slf4j, docs, `js-console`)
3839

3940
lazy val docs = project
4041
.in(file("site"))
@@ -56,9 +57,18 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
5657
)
5758
.nativeSettings(commonNativeSettings)
5859

59-
lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
60+
lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform)
6061
.settings(commonSettings)
6162
.dependsOn(core)
63+
.settings(
64+
name := "log4cats-mtl",
65+
libraryDependencies += "org.typelevel" %%% "cats-mtl" % catsMtlV
66+
)
67+
.nativeSettings(commonNativeSettings)
68+
69+
lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
70+
.settings(commonSettings)
71+
.dependsOn(core, mtl % Test)
6272
.settings(
6373
name := "log4cats-testing",
6474
libraryDependencies ++= Seq(
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.mtl
18+
19+
import cats.Applicative
20+
import cats.mtl.Ask
21+
22+
/**
23+
* Represents the ability to access a contextual environment as a map of key-value pairs.
24+
*
25+
* @example
26+
* {{{
27+
* case class LogContext(logId: String)
28+
*
29+
* // how to transform the contextual environment into the log context
30+
* implicit val toLogContext: ToContext[LogContext] =
31+
* ctx => Map("log_id" -> ctx.logId)
32+
*
33+
* // available out of the box for Kleisli, Reader, etc
34+
* implicit val askLogContext: Ask[F, LogContext] = ???
35+
*
36+
* implicit val contextual: Contextual[F] = Contextual.fromAsk
37+
* }}}
38+
*
39+
* @tparam F
40+
* the higher-kinded type of a polymorphic effect
41+
*/
42+
@scala.annotation.implicitNotFound("""
43+
Couldn't find `Contextual` for type `${F}`. Make sure you have the following implicit instances:
44+
1) `org.typelevel.log4cats.mtl.ToContext[Ctx]`
45+
2) `cats.mtl.Ask[${F}, Ctx]`
46+
""")
47+
trait Contextual[F[_]] {
48+
49+
/**
50+
* Retrieves the current contextual environment as a map of key-value pairs.
51+
*/
52+
def current: F[Map[String, String]]
53+
}
54+
55+
object Contextual {
56+
57+
def apply[F[_]](implicit ev: Contextual[F]): Contextual[F] = ev
58+
59+
/**
60+
* Creates a [[Contextual]] instance that always returns the given `ctx`.
61+
*/
62+
def const[F[_]: Applicative](ctx: Map[String, String]): Contextual[F] =
63+
new Contextual[F] {
64+
val current: F[Map[String, String]] = Applicative[F].pure(ctx)
65+
}
66+
67+
implicit def fromAsk[F[_], A: ToContext](implicit A: Ask[F, A]): Contextual[F] =
68+
new Contextual[F] {
69+
def current: F[Map[String, String]] = A.reader(ToContext[A].extract)
70+
}
71+
72+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.typelevel.log4cats
2+
package mtl
3+
4+
import cats.FlatMap
5+
import cats.syntax.functor._
6+
import org.typelevel.log4cats.mtl.syntax._
7+
8+
object ContextualLoggerFactory {
9+
10+
/**
11+
* Creates a new [[LoggerFactory]] that returns [[SelfAwareStructuredLogger]] that adds
12+
* information captured by `Contextual` to the context.
13+
*
14+
* @example
15+
* {{{
16+
* case class LogContext(logId: String)
17+
*
18+
* implicit val toLogContext: ToContext[LogContext] =
19+
* ctx => Map("log_id" -> ctx.logId)
20+
*
21+
* implicit val askLogContext: Ask[F, LogContext] = ???
22+
*
23+
* val loggerFactory: LoggerFactory[F] = ??? // the general factory, e.g. Slf4jFactory
24+
* val contextual: LoggerFactory[F] = ContextualLoggerFactory(loggerFactory)
25+
* }}}
26+
*/
27+
def apply[F[_]: Contextual: FlatMap](factory: LoggerFactory[F]): LoggerFactory[F] =
28+
new LoggerFactory[F] {
29+
def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] =
30+
factory.getLoggerFromName(name).contextual
31+
32+
def fromName(name: String): F[SelfAwareStructuredLogger[F]] =
33+
factory.fromName(name).map(logger => logger.contextual)
34+
}
35+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
package mtl
19+
20+
import cats.FlatMap
21+
import cats.syntax.flatMap._
22+
import cats.syntax.functor._
23+
24+
private[mtl] class ContextualSelfAwareStructuredLogger[F[_]: Contextual: FlatMap](
25+
sl: SelfAwareStructuredLogger[F]
26+
) extends SelfAwareStructuredLogger[F] {
27+
28+
private val defaultCtx: F[Map[String, String]] = Contextual[F].current
29+
30+
private def modify(ctx: Map[String, String]): F[Map[String, String]] =
31+
defaultCtx.map(c => c ++ ctx)
32+
33+
def isTraceEnabled: F[Boolean] = sl.isTraceEnabled
34+
def isDebugEnabled: F[Boolean] = sl.isDebugEnabled
35+
def isInfoEnabled: F[Boolean] = sl.isInfoEnabled
36+
def isWarnEnabled: F[Boolean] = sl.isWarnEnabled
37+
def isErrorEnabled: F[Boolean] = sl.isErrorEnabled
38+
39+
def error(message: => String): F[Unit] =
40+
defaultCtx.flatMap(sl.error(_)(message))
41+
42+
def warn(message: => String): F[Unit] =
43+
defaultCtx.flatMap(sl.warn(_)(message))
44+
45+
def info(message: => String): F[Unit] =
46+
defaultCtx.flatMap(sl.info(_)(message))
47+
48+
def debug(message: => String): F[Unit] =
49+
defaultCtx.flatMap(sl.debug(_)(message))
50+
51+
def trace(message: => String): F[Unit] =
52+
defaultCtx.flatMap(sl.trace(_)(message))
53+
54+
def error(t: Throwable)(message: => String): F[Unit] =
55+
defaultCtx.flatMap(sl.error(_, t)(message))
56+
57+
def warn(t: Throwable)(message: => String): F[Unit] =
58+
defaultCtx.flatMap(sl.warn(_, t)(message))
59+
60+
def info(t: Throwable)(message: => String): F[Unit] =
61+
defaultCtx.flatMap(sl.info(_, t)(message))
62+
63+
def debug(t: Throwable)(message: => String): F[Unit] =
64+
defaultCtx.flatMap(sl.debug(_, t)(message))
65+
66+
def trace(t: Throwable)(message: => String): F[Unit] =
67+
defaultCtx.flatMap(sl.trace(_, t)(message))
68+
69+
def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
70+
modify(ctx).flatMap(sl.trace(_)(msg))
71+
72+
def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
73+
modify(ctx).flatMap(sl.debug(_)(msg))
74+
75+
def info(ctx: Map[String, String])(msg: => String): F[Unit] =
76+
modify(ctx).flatMap(sl.info(_)(msg))
77+
78+
def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
79+
modify(ctx).flatMap(sl.warn(_)(msg))
80+
81+
def error(ctx: Map[String, String])(msg: => String): F[Unit] =
82+
modify(ctx).flatMap(sl.error(_)(msg))
83+
84+
def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
85+
modify(ctx).flatMap(sl.error(_, t)(message))
86+
87+
def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
88+
modify(ctx).flatMap(sl.warn(_, t)(message))
89+
90+
def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
91+
modify(ctx).flatMap(sl.info(_, t)(message))
92+
93+
def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
94+
modify(ctx).flatMap(sl.debug(_, t)(message))
95+
96+
def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
97+
modify(ctx).flatMap(sl.trace(_, t)(message))
98+
}
99+
100+
object ContextualSelfAwareStructuredLogger {
101+
102+
/**
103+
* Creates a new [[SelfAwareStructuredLogger]] that adds information captured by `Contextual` to
104+
* the context.
105+
*
106+
* @example
107+
* {{{
108+
* case class LogContext(logId: String)
109+
*
110+
* implicit val toLogContext: ToContext[LogContext] =
111+
* ctx => Map("log_id" -> ctx.logId)
112+
*
113+
* implicit val askLogContext: Ask[F, LogContext] = ???
114+
*
115+
* val logger: SelfAwareStructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger
116+
* val contextual: SelfAwareStructuredLogger[F] = ContextualSelfAwareStructuredLogger(logger)
117+
* }}}
118+
*/
119+
def apply[F[_]: Contextual: FlatMap](
120+
logger: SelfAwareStructuredLogger[F]
121+
): SelfAwareStructuredLogger[F] =
122+
new ContextualSelfAwareStructuredLogger[F](logger)
123+
124+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
package mtl
19+
20+
import cats.FlatMap
21+
import cats.syntax.flatMap._
22+
import cats.syntax.functor._
23+
24+
private[mtl] class ContextualStructuredLogger[F[_]: Contextual: FlatMap](
25+
sl: StructuredLogger[F]
26+
) extends StructuredLogger[F] {
27+
28+
private val defaultCtx: F[Map[String, String]] = Contextual[F].current
29+
30+
private def modify(ctx: Map[String, String]): F[Map[String, String]] =
31+
defaultCtx.map(c => c ++ ctx)
32+
33+
def error(message: => String): F[Unit] =
34+
defaultCtx.flatMap(sl.error(_)(message))
35+
36+
def warn(message: => String): F[Unit] =
37+
defaultCtx.flatMap(sl.warn(_)(message))
38+
39+
def info(message: => String): F[Unit] =
40+
defaultCtx.flatMap(sl.info(_)(message))
41+
42+
def debug(message: => String): F[Unit] =
43+
defaultCtx.flatMap(sl.debug(_)(message))
44+
45+
def trace(message: => String): F[Unit] =
46+
defaultCtx.flatMap(sl.trace(_)(message))
47+
48+
def error(t: Throwable)(message: => String): F[Unit] =
49+
defaultCtx.flatMap(sl.error(_, t)(message))
50+
51+
def warn(t: Throwable)(message: => String): F[Unit] =
52+
defaultCtx.flatMap(sl.warn(_, t)(message))
53+
54+
def info(t: Throwable)(message: => String): F[Unit] =
55+
defaultCtx.flatMap(sl.info(_, t)(message))
56+
57+
def debug(t: Throwable)(message: => String): F[Unit] =
58+
defaultCtx.flatMap(sl.debug(_, t)(message))
59+
60+
def trace(t: Throwable)(message: => String): F[Unit] =
61+
defaultCtx.flatMap(sl.trace(_, t)(message))
62+
63+
def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
64+
modify(ctx).flatMap(sl.trace(_)(msg))
65+
66+
def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
67+
modify(ctx).flatMap(sl.debug(_)(msg))
68+
69+
def info(ctx: Map[String, String])(msg: => String): F[Unit] =
70+
modify(ctx).flatMap(sl.info(_)(msg))
71+
72+
def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
73+
modify(ctx).flatMap(sl.warn(_)(msg))
74+
75+
def error(ctx: Map[String, String])(msg: => String): F[Unit] =
76+
modify(ctx).flatMap(sl.error(_)(msg))
77+
78+
def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
79+
modify(ctx).flatMap(sl.error(_, t)(message))
80+
81+
def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
82+
modify(ctx).flatMap(sl.warn(_, t)(message))
83+
84+
def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
85+
modify(ctx).flatMap(sl.info(_, t)(message))
86+
87+
def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
88+
modify(ctx).flatMap(sl.debug(_, t)(message))
89+
90+
def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
91+
modify(ctx).flatMap(sl.trace(_, t)(message))
92+
}
93+
94+
object ContextualStructuredLogger {
95+
96+
/**
97+
* Creates a new [[StructuredLogger]] that adds information captured by `Contextual` to the
98+
* context.
99+
*
100+
* @example
101+
* {{{
102+
* case class LogContext(logId: String)
103+
*
104+
* implicit val toLogContext: ToContext[LogContext] =
105+
* ctx => Map("log_id" -> ctx.logId)
106+
*
107+
* implicit val askLogContext: Ask[F, LogContext] = ???
108+
*
109+
* val logger: StructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger
110+
* val contextual: StructuredLogger[F] = ContextualStructureLogger(logger)
111+
* }}}
112+
*/
113+
def apply[F[_]: Contextual: FlatMap](logger: StructuredLogger[F]): StructuredLogger[F] =
114+
new ContextualStructuredLogger[F](logger)
115+
116+
}

0 commit comments

Comments
 (0)