Skip to content
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

Allow adding effect's contextual environment to the log context #800

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target slf4j/target project/target
run: mkdir -p mtl/native/target testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target mtl/js/target mtl/jvm/target slf4j/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target slf4j/target project/target
run: tar cf targets.tar mtl/native/target testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target mtl/js/target mtl/jvm/target slf4j/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
13 changes: 11 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1")

val catsV = "2.10.0"
val catsEffectV = "3.5.2"
val catsMtlV = "1.4.0"
val slf4jV = "1.7.36"
val munitCatsEffectV = "2.0.0-M3"
val logbackClassicV = "1.2.12"

Global / onChangedBuildSource := ReloadOnSourceChanges

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

lazy val docs = project
.in(file("site"))
Expand All @@ -56,9 +57,17 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
)
.nativeSettings(commonNativeSettings)

lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(commonSettings)
.dependsOn(core)
.settings(
name := "log4cats-mtl",
libraryDependencies += "org.typelevel" %%% "cats-mtl" % catsMtlV
)

lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(commonSettings)
.dependsOn(core, mtl % Test)
.settings(
name := "log4cats-testing",
libraryDependencies ++= Seq(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.mtl

import cats.Applicative
import cats.mtl.Ask

/**
* Represents the ability to access a contextual environment as a map of key-value pairs.
*
* @example
* {{{
* case class LogContext(logId: String)
*
* // how to transform the contextual environment into the log context
* implicit val toLogContext: ToContext[LogContext] =
* ctx => Map("log_id" -> ctx.logId)
*
* // available out of the box for Kleisli, Reader, etc
* implicit val askLogContext: Ask[F, LogContext] = ???
*
* implicit val contextual: Contextual[F] = Contextual.fromAsk
* }}}
*
* @tparam F
* the higher-kinded type of a polymorphic effect
*/
@scala.annotation.implicitNotFound("""
Couldn't find `Contextual` for type `${F}`. Make sure you have the following implicit instances:
1) `org.typelevel.log4cats.mtl.ToContext[Ctx]`
2) `cats.mtl.Ask[${F}, Ctx]`
""")
trait Contextual[F[_]] {

/**
* Retrieves the current contextual environment as a map of key-value pairs.
*/
def current: F[Map[String, String]]
}

object Contextual {

def apply[F[_]](implicit ev: Contextual[F]): Contextual[F] = ev

/**
* Creates a [[Contextual]] instance that always returns the given `ctx`.
*/
def const[F[_]: Applicative](ctx: Map[String, String]): Contextual[F] =
new Contextual[F] {
val current: F[Map[String, String]] = Applicative[F].pure(ctx)
}

implicit def fromAsk[F[_], A: ToContext](implicit A: Ask[F, A]): Contextual[F] =
new Contextual[F] {
def current: F[Map[String, String]] = A.reader(ToContext[A].extract)
}

}
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
package mtl

import cats.FlatMap
import cats.syntax.functor._
import org.typelevel.log4cats.mtl.syntax._

object ContextualLoggerFactory {

/**
* Creates a new [[LoggerFactory]] that returns [[SelfAwareStructuredLogger]] that adds
* information captured by `Contextual` to the context.
*
* @example
* {{{
* case class LogContext(logId: String)
*
* implicit val toLogContext: ToContext[LogContext] =
* ctx => Map("log_id" -> ctx.logId)
*
* implicit val askLogContext: Ask[F, LogContext] = ???
*
* val loggerFactory: LoggerFactory[F] = ??? // the general factory, e.g. Slf4jFactory
* val contextual: LoggerFactory[F] = ContextualLoggerFactory(loggerFactory)
* }}}
*/
def apply[F[_]: Contextual: FlatMap](factory: LoggerFactory[F]): LoggerFactory[F] =
new LoggerFactory[F] {
def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] =
factory.getLoggerFromName(name).contextual

def fromName(name: String): F[SelfAwareStructuredLogger[F]] =
factory.fromName(name).map(logger => logger.contextual)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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
package mtl

import cats.FlatMap
import cats.syntax.flatMap._
import cats.syntax.functor._

private[mtl] class ContextualSelfAwareStructuredLogger[F[_]: Contextual: FlatMap](
sl: SelfAwareStructuredLogger[F]
) extends SelfAwareStructuredLogger[F] {

private val defaultCtx: F[Map[String, String]] = Contextual[F].current

private def modify(ctx: Map[String, String]): F[Map[String, String]] =
defaultCtx.map(c => c ++ ctx)

def isTraceEnabled: F[Boolean] = sl.isTraceEnabled
def isDebugEnabled: F[Boolean] = sl.isDebugEnabled
def isInfoEnabled: F[Boolean] = sl.isInfoEnabled
def isWarnEnabled: F[Boolean] = sl.isWarnEnabled
def isErrorEnabled: F[Boolean] = sl.isErrorEnabled

def error(message: => String): F[Unit] =
defaultCtx.flatMap(sl.error(_)(message))

def warn(message: => String): F[Unit] =
defaultCtx.flatMap(sl.warn(_)(message))

def info(message: => String): F[Unit] =
defaultCtx.flatMap(sl.info(_)(message))

def debug(message: => String): F[Unit] =
defaultCtx.flatMap(sl.debug(_)(message))

def trace(message: => String): F[Unit] =
defaultCtx.flatMap(sl.trace(_)(message))

def error(t: Throwable)(message: => String): F[Unit] =
defaultCtx.flatMap(sl.error(_, t)(message))

def warn(t: Throwable)(message: => String): F[Unit] =
defaultCtx.flatMap(sl.warn(_, t)(message))

def info(t: Throwable)(message: => String): F[Unit] =
defaultCtx.flatMap(sl.info(_, t)(message))

def debug(t: Throwable)(message: => String): F[Unit] =
defaultCtx.flatMap(sl.debug(_, t)(message))

def trace(t: Throwable)(message: => String): F[Unit] =
defaultCtx.flatMap(sl.trace(_, t)(message))

def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
modify(ctx).flatMap(sl.trace(_)(msg))

def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
modify(ctx).flatMap(sl.debug(_)(msg))

def info(ctx: Map[String, String])(msg: => String): F[Unit] =
modify(ctx).flatMap(sl.info(_)(msg))

def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
modify(ctx).flatMap(sl.warn(_)(msg))

def error(ctx: Map[String, String])(msg: => String): F[Unit] =
modify(ctx).flatMap(sl.error(_)(msg))

def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
modify(ctx).flatMap(sl.error(_, t)(message))

def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
modify(ctx).flatMap(sl.warn(_, t)(message))

def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
modify(ctx).flatMap(sl.info(_, t)(message))

def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
modify(ctx).flatMap(sl.debug(_, t)(message))

def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] =
modify(ctx).flatMap(sl.trace(_, t)(message))
}

object ContextualSelfAwareStructuredLogger {

/**
* Creates a new [[SelfAwareStructuredLogger]] that adds information captured by `Contextual` to
* the context.
*
* @example
* {{{
* case class LogContext(logId: String)
*
* implicit val toLogContext: ToContext[LogContext] =
* ctx => Map("log_id" -> ctx.logId)
*
* implicit val askLogContext: Ask[F, LogContext] = ???
*
* val logger: SelfAwareStructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger
* val contextual: SelfAwareStructuredLogger[F] = ContextualSelfAwareStructuredLogger(logger)
* }}}
*/
def apply[F[_]: Contextual: FlatMap](
logger: SelfAwareStructuredLogger[F]
): SelfAwareStructuredLogger[F] =
new ContextualSelfAwareStructuredLogger[F](logger)

}
Loading
Loading