Skip to content

Commit 5b85513

Browse files
committed
update with the suggested changes
1 parent 63ba4c4 commit 5b85513

5 files changed

Lines changed: 238 additions & 83 deletions

File tree

core/shared/src/main/scala/org/typelevel/log4cats/Log.scala

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,14 @@ import scala.concurrent.duration.FiniteDuration
2828
trait Log[Ctx] {
2929
def timestamp: Option[FiniteDuration]
3030
def level: KernelLogLevel
31-
def message: String
31+
def message: () => String
3232
def throwable: Option[Throwable]
3333
def context: Map[String, Ctx]
3434
def fileName: Option[String]
3535
def className: Option[String]
3636
def methodName: Option[String]
3737
def line: Option[Int]
38-
39-
def unsafeThrowable: Throwable
40-
def unsafeContext: Map[String, Ctx]
38+
def levelValue: Int
4139
}
4240

4341
object Log {
@@ -57,15 +55,25 @@ object Log {
5755
)(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] =
5856
contextMap.foldLeft(this) { case (builder, (k, v)) => builder.withContext(k)(v) }
5957

58+
def adaptTimestamp(f: FiniteDuration => FiniteDuration): Builder[Ctx]
59+
def adaptLevel(f: KernelLogLevel => KernelLogLevel): Builder[Ctx]
60+
def adaptMessage(f: String => String): Builder[Ctx]
61+
def adaptThrowable(f: Throwable => Throwable): Builder[Ctx]
62+
def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): Builder[Ctx]
63+
def adaptFileName(f: String => String): Builder[Ctx]
64+
def adaptClassName(f: String => String): Builder[Ctx]
65+
def adaptMethodName(f: String => String): Builder[Ctx]
66+
def adaptLine(f: Int => Int): Builder[Ctx]
67+
6068
def build(): Log[Ctx]
6169
}
6270

6371
def mutableBuilder[Ctx](): Builder[Ctx] = new MutableBuilder[Ctx]()
6472

6573
private class MutableBuilder[Ctx] extends Builder[Ctx] {
6674
private var _timestamp: Option[FiniteDuration] = None
67-
private var _level: Option[KernelLogLevel] = None
68-
private var _message: Option[String] = None
75+
private var _level: KernelLogLevel = KernelLogLevel.Info
76+
private var _message: () => String = () => ""
6977
private var _throwable: Option[Throwable] = None
7078
private val _context: mutable.Map[String, Ctx] = mutable.Map.empty[String, Ctx]
7179
private var _fileName: Option[String] = None
@@ -75,16 +83,15 @@ object Log {
7583

7684
def build(): Log[Ctx] = new Log[Ctx] {
7785
override def timestamp: Option[FiniteDuration] = _timestamp
78-
override def level: KernelLogLevel = _level.getOrElse(KernelLogLevel.Info)
79-
override def message: String = _message.getOrElse("")
86+
override def level: KernelLogLevel = _level
87+
override def message: () => String = _message
8088
override def throwable: Option[Throwable] = _throwable
8189
override def context: Map[String, Ctx] = _context.toMap
8290
override def className: Option[String] = _className
8391
override def fileName: Option[String] = _fileName
8492
override def methodName: Option[String] = _methodName
8593
override def line: Option[Int] = _line.filter(_ > 0)
86-
override def unsafeThrowable: Throwable = _throwable.get
87-
override def unsafeContext: Map[String, Ctx] = _context.toMap
94+
override def levelValue: Int = _level.value
8895
}
8996

9097
override def withTimestamp(value: FiniteDuration): this.type = {
@@ -93,12 +100,60 @@ object Log {
93100
}
94101

95102
override def withLevel(level: KernelLogLevel): this.type = {
96-
_level = Some(level)
103+
_level = level
97104
this
98105
}
99106

100107
override def withMessage(message: => String): this.type = {
101-
_message = Some(message)
108+
_message = () => message
109+
this
110+
}
111+
112+
override def adaptMessage(f: String => String): this.type = {
113+
_message = () => f(_message())
114+
this
115+
}
116+
117+
override def adaptTimestamp(f: FiniteDuration => FiniteDuration): this.type = {
118+
_timestamp = _timestamp.map(f)
119+
this
120+
}
121+
122+
override def adaptLevel(f: KernelLogLevel => KernelLogLevel): this.type = {
123+
_level = f(_level)
124+
this
125+
}
126+
127+
override def adaptThrowable(f: Throwable => Throwable): this.type = {
128+
_throwable = _throwable.map(f)
129+
this
130+
}
131+
132+
override def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): this.type = {
133+
val newContext = Map.newBuilder[String, Ctx]
134+
newContext.addAll(f(_context.toMap))
135+
_context.clear()
136+
_context.addAll(newContext.result())
137+
this
138+
}
139+
140+
override def adaptFileName(f: String => String): this.type = {
141+
_fileName = _fileName.map(f)
142+
this
143+
}
144+
145+
override def adaptClassName(f: String => String): this.type = {
146+
_className = _className.map(f)
147+
this
148+
}
149+
150+
override def adaptMethodName(f: String => String): this.type = {
151+
_methodName = _methodName.map(f)
152+
this
153+
}
154+
155+
override def adaptLine(f: Int => Int): this.type = {
156+
_line = _line.map(f)
102157
this
103158
}
104159

core/shared/src/main/scala/org/typelevel/log4cats/Recordable.scala

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@ trait Recordable[Ctx, A] {
2727
object Recordable {
2828
def apply[Ctx, A](implicit ev: Recordable[Ctx, A]): ev.type = ev
2929

30-
// Basic string message recording
3130
implicit def stringLoggable[Ctx]: Recordable[Ctx, String] = new Recordable[Ctx, String] {
3231
def record(value: => String) = _.withMessage(value)
3332
}
3433

35-
// Context key-value pair recording with proper encoding
3634
implicit def contextPairLoggable[Ctx, A](implicit
3735
encoder: Context.Encoder[A, Ctx]
3836
): Recordable[Ctx, (String, A)] =
@@ -43,13 +41,11 @@ object Recordable {
4341
}
4442
}
4543

46-
// Throwable recording
4744
implicit def throwableLoggable[Ctx, T <: Throwable]: Recordable[Ctx, T] =
4845
new Recordable[Ctx, T] {
4946
def record(value: => T): LogRecord[Ctx] = _.withThrowable(value)
5047
}
5148

52-
// Numeric value recording with automatic string conversion
5349
implicit def intLoggable[Ctx](implicit
5450
encoder: Context.Encoder[Int, Ctx]
5551
): Recordable[Ctx, Int] =
@@ -90,18 +86,13 @@ object Recordable {
9086
}
9187
}
9288

93-
// Map recording for structured data
9489
implicit def mapLoggable[Ctx, A](implicit
9590
encoder: Context.Encoder[A, Ctx]
9691
): Recordable[Ctx, Map[String, A]] =
9792
new Recordable[Ctx, Map[String, A]] {
9893
def record(value: => Map[String, A]): LogRecord[Ctx] = {
9994
val map = value
100-
(builder: Log.Builder[Ctx]) => {
101-
var current = builder
102-
map.foreach { case (k, v) => current = current.withContext(k)(v) }
103-
current
104-
}
95+
(builder: Log.Builder[Ctx]) => builder.withContextMap(map)
10596
}
10697
}
10798
}

core/shared/src/main/scala/org/typelevel/log4cats/SamLogger.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ object SamLogger {
121121
def log(level: KernelLogLevel, record: Builder => Builder): F[Unit] = {
122122
val modifiedRecord = (builder: Builder) => {
123123
val originalLog = record(builder).build()
124-
val modifiedMessage = f(originalLog.message)
124+
val modifiedMessage = f(originalLog.message())
125125

126126
val newBuilder = Log
127127
.mutableBuilder[Ctx]()
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
/**
20+
* Adapter layer to bridge the new SAM LoggerKernel with the existing Logger interface. This allows
21+
* gradual migration from the old multi-method design to the new SAM design.
22+
*
23+
* ## Usage
24+
*
25+
* ### Converting SAM LoggerKernel to old Logger interface
26+
* ```scala
27+
* val kernel = Slf4jLoggerKernel.fromName[IO]("MyApp")
28+
* val oldLogger = SamLoggerAdapter.toLogger(kernel)
29+
*
30+
* // Use old API
31+
* oldLogger.info("Works with old API")
32+
* oldLogger.error(exception)("Works with old API")
33+
* ```
34+
*
35+
* ### Converting old Logger to SAM LoggerKernel
36+
* ```scala
37+
* val oldLogger = Slf4jLogger.getLogger[IO]
38+
* val samLogger = SamLoggerAdapter.loggerToSamLogger(oldLogger)
39+
*
40+
* // Use new SAM API
41+
* samLogger.info("Works with new API", ("key", "value"))
42+
* ```
43+
*
44+
* ## Limitations
45+
*
46+
* - **Context Loss**: When converting from LoggerKernel → Logger → LoggerKernel, structured
47+
* context information (key-value pairs) is lost because the old Logger interface doesn't
48+
* support structured logging. Only message and throwable information is preserved.
49+
* - **Performance**: The `toLoggerKernel` method creates a full log record to extract message and
50+
* throwable, which has some overhead.
51+
*
52+
* ## Best Practices
53+
*
54+
* - Use `toLogger` for backward compatibility when migrating to new backends
55+
* - Use `loggerToSamLogger` when you need structured logging capabilities
56+
* - Avoid round-trip conversions (LoggerKernel → Logger → LoggerKernel) as they lose context
57+
*/
58+
object SamLoggerAdapter {
59+
60+
/**
61+
* Convert a LoggerKernel to the existing Logger interface.
62+
*
63+
* This is the primary use case for the adapter - allowing new SAM-based backends to work with
64+
* existing code that uses the old Logger interface.
65+
*
66+
* **Note**: Structured context information is not accessible through the old Logger interface, so
67+
* any context added via the SAM API will not be visible when using the returned Logger.
68+
*/
69+
def toLogger[F[_], Ctx](kernel: LoggerKernel[F, Ctx]): Logger[F] = new Logger[F] {
70+
def error(message: => String): F[Unit] =
71+
kernel.log(KernelLogLevel.Error, _.withMessage(message))
72+
73+
def error(t: Throwable)(message: => String): F[Unit] =
74+
kernel.log(KernelLogLevel.Error, _.withMessage(message).withThrowable(t))
75+
76+
def warn(message: => String): F[Unit] =
77+
kernel.log(KernelLogLevel.Warn, _.withMessage(message))
78+
79+
def warn(t: Throwable)(message: => String): F[Unit] =
80+
kernel.log(KernelLogLevel.Warn, _.withMessage(message).withThrowable(t))
81+
82+
def info(message: => String): F[Unit] =
83+
kernel.log(KernelLogLevel.Info, _.withMessage(message))
84+
85+
def info(t: Throwable)(message: => String): F[Unit] =
86+
kernel.log(KernelLogLevel.Info, _.withMessage(message).withThrowable(t))
87+
88+
def debug(message: => String): F[Unit] =
89+
kernel.log(KernelLogLevel.Debug, _.withMessage(message))
90+
91+
def debug(t: Throwable)(message: => String): F[Unit] =
92+
kernel.log(KernelLogLevel.Debug, _.withMessage(message).withThrowable(t))
93+
94+
def trace(message: => String): F[Unit] =
95+
kernel.log(KernelLogLevel.Trace, _.withMessage(message))
96+
97+
def trace(t: Throwable)(message: => String): F[Unit] =
98+
kernel.log(KernelLogLevel.Trace, _.withMessage(message).withThrowable(t))
99+
}
100+
101+
/**
102+
* Convert the existing Logger interface to a LoggerKernel.
103+
*
104+
* **Important**: This conversion has limitations:
105+
* - Only message and throwable information is preserved
106+
* - All structured context (key-value pairs) is lost because the old Logger interface doesn't
107+
* support structured logging
108+
* - This method is primarily intended for testing and round-trip compatibility
109+
*
110+
* For production use, prefer using the SAM LoggerKernel directly or use `toLogger` to convert
111+
* from LoggerKernel to Logger.
112+
*/
113+
def toLoggerKernel[F[_], Ctx](logger: Logger[F]): LoggerKernel[F, Ctx] =
114+
new LoggerKernel[F, Ctx] {
115+
def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = {
116+
// Build the log record to extract message and throwable
117+
// Note: Context information is intentionally not preserved as the old Logger
118+
// interface doesn't support structured logging
119+
val logRecord = record(Log.mutableBuilder[Ctx]()).build()
120+
val message = logRecord.message()
121+
val throwable = logRecord.throwable
122+
123+
// Route to appropriate logger method based on level and throwable presence
124+
(level, throwable) match {
125+
case (KernelLogLevel.Error, Some(t)) => logger.error(t)(message)
126+
case (KernelLogLevel.Error, None) => logger.error(message)
127+
case (KernelLogLevel.Warn, Some(t)) => logger.warn(t)(message)
128+
case (KernelLogLevel.Warn, None) => logger.warn(message)
129+
case (KernelLogLevel.Info, Some(t)) => logger.info(t)(message)
130+
case (KernelLogLevel.Info, None) => logger.info(message)
131+
case (KernelLogLevel.Debug, Some(t)) => logger.debug(t)(message)
132+
case (KernelLogLevel.Debug, None) => logger.debug(message)
133+
case (KernelLogLevel.Trace, Some(t)) => logger.trace(t)(message)
134+
case (KernelLogLevel.Trace, None) => logger.trace(message)
135+
case _ => logger.info(message) // fallback for unknown levels
136+
}
137+
}
138+
}
139+
140+
/**
141+
* Convert a SamLogger to the existing Logger interface.
142+
*
143+
* This is a convenience method that delegates to `toLogger`.
144+
*/
145+
def samLoggerToLogger[F[_], Ctx](samLogger: SamLogger[F, Ctx]): Logger[F] = toLogger(samLogger)
146+
147+
/**
148+
* Convert the existing Logger interface to a SamLogger.
149+
*
150+
* This allows old Logger implementations to be used with the new SAM API, enabling structured
151+
* logging capabilities.
152+
*
153+
* **Note**: The underlying Logger implementation must support the old interface. Any structured
154+
* context added via the SAM API will be lost when the underlying Logger processes the log (since
155+
* it only receives message and throwable).
156+
*/
157+
def loggerToSamLogger[F[_], Ctx](logger: Logger[F]): SamLogger[F, Ctx] =
158+
SamLogger.wrap(toLoggerKernel(logger))
159+
}

0 commit comments

Comments
 (0)