From 45232f2e3661f4f8bcf64d202b9b3361b008cd48 Mon Sep 17 00:00:00 2001 From: Jack Henahan Date: Fri, 21 Mar 2025 23:18:58 -0400 Subject: [PATCH] feat!: add ContextPair to StructuredLogger Motivation ========== `addContext` has one sharp edge on it that seems to come up a lot during onboarding: if you want to add a single context element, the syntax is unintuitive. Say we want to add the context `"foo" -> "bar"` to a logger. Naturally, one might reach for `addContext(pairs: (String, Shown)*)`, but alas, after erasure we have ```scala logger.addContext( "foo" -> "bar" // Tuple2 ) ``` which doesn't conform to either of `Map` or `Seq`. This leaves us with these options: ```scala logger.addContext( Map( "foo" -> bar.show ) ) logger.addContext( "foo" -> (bar: Shown) ) ``` Neither feels _great_, and it would be really pleasant to have consistent syntax for single-element and multi-element context data without having to use `Map` literals and explicit `.show`/`.toString`. Result ====== This patch hijacks `StructuredLogger`'s implicit scope to do a bit of juggling and resolve the erasure issue, and in the process cleans up a chunk of duplicative code in its descendants. I've tested this in the work code, and it appears to work as expected for any subtype of `StructuredLogger`. The signature ain't exactly beautiful, but the syntactic consistency is gratifying. --- .../log4cats/SelfAwareStructuredLogger.scala | 8 -------- .../typelevel/log4cats/StructuredLogger.scala | 19 +++++++++++++++---- .../DeferredSelfAwareStructuredLogger.scala | 7 +------ .../extras/DeferredStructuredLogger.scala | 6 ------ 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala index 40907bcf..7824ec8d 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala @@ -17,7 +17,6 @@ package org.typelevel.log4cats import cats.* -import cats.Show.Shown trait SelfAwareStructuredLogger[F[_]] extends SelfAwareLogger[F] with StructuredLogger[F] { override def mapK[G[_]](fk: F ~> G): SelfAwareStructuredLogger[G] = @@ -26,13 +25,6 @@ trait SelfAwareStructuredLogger[F[_]] extends SelfAwareLogger[F] with Structured override def addContext(ctx: Map[String, String]): SelfAwareStructuredLogger[F] = SelfAwareStructuredLogger.withContext(this)(ctx) - override def addContext( - pairs: (String, Shown)* - ): SelfAwareStructuredLogger[F] = - SelfAwareStructuredLogger.withContext(this)( - pairs.map { case (k, v) => (k, v.toString) }.toMap - ) - override def withModifiedString(f: String => String): SelfAwareStructuredLogger[F] = SelfAwareStructuredLogger.withModifiedString[F](this, f) } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala index ee50e011..06cbf72a 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/StructuredLogger.scala @@ -17,7 +17,9 @@ package org.typelevel.log4cats import cats.* +import cats.syntax.foldable.* import cats.Show.Shown +import cats.Show.ContravariantShow trait StructuredLogger[F[_]] extends Logger[F] { def trace(ctx: Map[String, String])(msg: => String): F[Unit] @@ -36,18 +38,27 @@ trait StructuredLogger[F[_]] extends Logger[F] { def addContext(ctx: Map[String, String]): StructuredLogger[F] = StructuredLogger.withContext(this)(ctx) + def addContext(pair: StructuredLogger.ContextPair): StructuredLogger[F] = + addContext(pair.toMap) + def addContext( - pairs: (String, Shown)* + pairs: StructuredLogger.ContextPair* ): StructuredLogger[F] = - StructuredLogger.withContext(this)( - pairs.map { case (k, v) => (k, v.toString) }.toMap - ) + addContext(pairs.toList.foldMap(_.toMap)) override def withModifiedString(f: String => String): StructuredLogger[F] = StructuredLogger.withModifiedString[F](this, f) } object StructuredLogger { + + final case class ContextPair(value: (String, Shown)) extends AnyVal { + def toMap: Map[String, String] = Map(value._1 -> value._2.toString) + } + + implicit def log4catsTupleToContextPair[A: ContravariantShow](pair: (String, A)): ContextPair = + ContextPair((pair._1, Shown.mat(pair._2))) + def apply[F[_]](implicit ev: StructuredLogger[F]): StructuredLogger[F] = ev def withContext[F[_]](sl: StructuredLogger[F])(ctx: Map[String, String]): StructuredLogger[F] = diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala index 778305db..31ed0b0d 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredSelfAwareStructuredLogger.scala @@ -20,7 +20,7 @@ import cats.data.Chain import cats.effect.kernel.Resource.ExitCase import cats.effect.kernel.{Concurrent, Ref, Resource} import cats.syntax.all.* -import cats.{~>, Show} +import cats.~> import org.typelevel.log4cats.SelfAwareStructuredLogger /** @@ -39,11 +39,6 @@ trait DeferredSelfAwareStructuredLogger[F[_]] override def addContext(ctx: Map[String, String]): DeferredSelfAwareStructuredLogger[F] = DeferredSelfAwareStructuredLogger.withContext(this)(ctx) - override def addContext(pairs: (String, Show.Shown)*): DeferredSelfAwareStructuredLogger[F] = - DeferredSelfAwareStructuredLogger.withContext(this)( - pairs.map { case (k, v) => (k, v.toString) }.toMap - ) - override def withModifiedString(f: String => String): DeferredSelfAwareStructuredLogger[F] = DeferredSelfAwareStructuredLogger.withModifiedString(this, f) } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala index 1c0855bc..832fe783 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/extras/DeferredStructuredLogger.scala @@ -16,7 +16,6 @@ package org.typelevel.log4cats.extras -import cats.Show.Shown import cats.data.Chain import cats.effect.kernel.Resource.ExitCase import cats.effect.kernel.{Concurrent, Ref, Resource} @@ -56,11 +55,6 @@ trait DeferredStructuredLogger[F[_]] extends StructuredLogger[F] with DeferredLo override def addContext(ctx: Map[String, String]): DeferredStructuredLogger[F] = DeferredStructuredLogger.withContext(this, ctx) - override def addContext( - pairs: (String, Shown)* - ): DeferredStructuredLogger[F] = - DeferredStructuredLogger.withContext(this, pairs.map { case (k, v) => (k, v.toString) }.toMap) - override def withModifiedString(f: String => String): DeferredStructuredLogger[F] = DeferredStructuredLogger.withModifiedString[F](this, f) }