Skip to content

Add logger with Local semantics #909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1")

val catsV = "2.11.0"
val catsEffectV = "3.6.0"
val catsMtlV = "1.4.0"
val slf4jV = "1.7.36"
val munitCatsEffectV = "2.1.0"
val logbackClassicV = "1.2.13"

val tempOtel4sV = "0.11.0"

Global / onChangedBuildSource := ReloadOnSourceChanges

lazy val root = tlCrossRootProject.aggregate(core, testing, noop, slf4j, docs, `js-console`)
Expand All @@ -46,8 +49,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(
name := "log4cats-core",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV,
"org.typelevel" %%% "cats-effect-std" % catsEffectV
"org.typelevel" %%% "cats-core" % catsV,
"org.typelevel" %%% "cats-effect-std" % catsEffectV,
"org.typelevel" %%% "cats-mtl" % catsMtlV,
"org.typelevel" %%% "otel4s-core-common" % tempOtel4sV
),
libraryDependencies ++= {
if (tlIsScala3.value) Seq.empty
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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

import cats.mtl.{Ask, Local}
import cats.syntax.functor.*
import cats.syntax.traverse.*
import cats.{~>, Applicative, Show}
import org.typelevel.otel4s.KindTransformer

import scala.collection.immutable.ArraySeq

sealed trait LocalLogContext[F[_]] {
private[log4cats] def currentLogContext: F[Map[String, String]]

private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A]

private[log4cats] final def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] =
withAddedContext {
ctx.view.map { case (k, v) => k -> v.toString }.toMap
}(fa)

def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F]

def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F]
Comment on lines +37 to +39
Copy link
Author

@NthPortal NthPortal Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these allow you to, for example, add trace and span IDs to the context. high/low priority is which wins if there's a key collision (shouldn't ever happen for span context, but could for other things)


def mapK[G[_]: Applicative](implicit kt: KindTransformer[F, G]): LocalLogContext[G]
}

object LocalLogContext {
private[this] type AskContext[F[_]] = Ask[F, Map[String, String]]

private[this] final class MappedKLocal[F[_], G[_], E](
localF: Local[F, E]
)(implicit
val applicative: Applicative[G],
kt: KindTransformer[F, G]
) extends Local[G, E] {
def ask[E2 >: E]: G[E2] =
kt.liftK(localF.ask[E2])
def local[A](ga: G[A])(f: E => E): G[A] =
kt.limitedMapK(ga) {
new (F ~> F) {
def apply[B](fb: F[B]): F[B] = localF.local(fb)(f)
}
}
}

private[this] final class MappedKAsk[F[_], G[_], E](
askF: Ask[F, E],
fk: F ~> G
)(implicit val applicative: Applicative[G])
extends Ask[G, E] {
def ask[E2 >: E]: G[E2] = fk(askF.ask[E2])
}

private[this] final class MultiAskContext[F[_]] private[MultiAskContext] (
asks: Seq[AskContext[F]] /* never empty */
) extends AskContext[F] {
implicit def applicative: Applicative[F] = asks.head.applicative
def ask[E2 >: Map[String, String]]: F[E2] =
asks
.traverse(_.ask[Map[String, String]])
.map(_.reduceLeft(_ ++ _))
def prependLowPriority(ask: AskContext[F]): MultiAskContext[F] =
new MultiAskContext(ask +: asks)
def appendHighPriority(ask: AskContext[F]): MultiAskContext[F] =
new MultiAskContext(asks :+ ask)
}

private[this] object MultiAskContext {
def apply[F[_]](ask: AskContext[F]): MultiAskContext[F] =
ask match {
case multi: MultiAskContext[F] => multi
case other => new MultiAskContext(ArraySeq(other))
}
}

private[this] final class Impl[F[_]](
localCtx: Local[F, Map[String, String]],
askCtx: AskContext[F]
) extends LocalLogContext[F] {
private[log4cats] def currentLogContext: F[Map[String, String]] =
askCtx.ask[Map[String, String]]
private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] =
localCtx.local(fa)(_ ++ ctx)

def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] =
new Impl(
localCtx,
MultiAskContext(askCtx).appendHighPriority(ask)
)

def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] =
new Impl(
localCtx,
MultiAskContext(askCtx).prependLowPriority(ask)
)

def mapK[G[_]](implicit G: Applicative[G], kt: KindTransformer[F, G]): LocalLogContext[G] = {
val localF = localCtx
val askF = askCtx
val localG = new MappedKLocal(localF)
val askG =
if (askF eq localF) localG
else new MappedKAsk(askF, kt.liftK)
new Impl(localG, askG)
}
}

def fromLocal[F[_]](implicit localCtx: Local[F, Map[String, String]]): LocalLogContext[F] =
new Impl(localCtx, localCtx)
}
Loading
Loading