Skip to content

Conversation

Jay-Lokhande
Copy link

@Jay-Lokhande Jay-Lokhande commented Jun 18, 2025

This PR's objective is to create a SAM type interface in log4cats

Comment on lines 19 to 23
final case class KernelLogLevel(name: String, value: Double) {
def namePadded: String = KernelLogLevel.padded(this)

KernelLogLevel.add(this)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (structure): I'm sick right now, but we should get on a call towards the end of next week and go over bincompat.

Case classes are wonderful for applications, but we're going to tend to avoid them because they don't play nicely with bincompat.

Copy link
Member

Choose a reason for hiding this comment

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

agreed 💯

Comment on lines +41 to +52
def add(level: KernelLogLevel): Unit = synchronized {
val length = level.name.length
map += level.name.toLowerCase -> level
if (length > maxLength) {
maxLength = length
padded = map.map { case (_, level) =>
level -> level.name.padTo(maxLength, ' ').mkString
}
} else {
padded += level -> level.name.padTo(maxLength, ' ').mkString
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: I'm not convinced we need to support dynamically adding log levels, or looking them up by name.

Comment on lines 39 to 48
final def logTrace(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Trace, record)
final def logDebug(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Debug, record)
final def logInfo(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Info, record)
final def logWarn(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Warn, record)
final def logError(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Error, record)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (naming): If we do end up needing these, we should simplify down the names

Suggested change
final def logTrace(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Trace, record)
final def logDebug(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Debug, record)
final def logInfo(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Info, record)
final def logWarn(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Warn, record)
final def logError(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Error, record)
final def trace(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Trace, record)
final def debug(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Debug, record)
final def info(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Info, record)
final def warn(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Warn, record)
final def error(record: Log.Builder => Log.Builder): F[Unit] =
log(KernelLogLevel.Error, record)

Copy link
Author

Choose a reason for hiding this comment

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

lets keep it until final change, I tried to change as suggested but its giving error due to duplicates

@iRevive iRevive mentioned this pull request Jun 20, 2025
15 tasks
@Jay-Lokhande
Copy link
Author

Thank you for all the suggestions, I will rework on it

Comment on lines +36 to +38
// For Java/legacy interop, if needed (not implicit)
val LevelOrdering: Ordering[KernelLogLevel] =
Ordering.by[KernelLogLevel, Int](_.value).reverse
Copy link
Contributor

Choose a reason for hiding this comment

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

Java interop is a non-issue, since attempting to use cats-effect from Java would be painful (at best) and the idioms are all sorts of incompatible.

There's an implicit which provides an Ordering[A] if there is an Order[A] in scope, so this can be removed.

private var _timestamp: Option[FiniteDuration] = None
private var _level: Option[KernelLogLevel] = None
private var _levelValue: Option[Int] = None
private var _message: Option[String] = None
private var _message: () => String = noopMessage
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: is the use case of a log without a message something we want to support?

): SamLogger[F, Ctx] =
new SamLogger[F, Ctx] {
def log(level: KernelLogLevel, record: Builder => Builder): F[Unit] = {
val modifiedRecord = (builder: Builder) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needing to materialize the log and manually copy over the full message hints to me that we may need to refine Builder.

Pretty sure this would materialize message as well, so that's something to check.

Copy link
Contributor

Choose a reason for hiding this comment

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

With the added adaptMessage method, this becomes:

      def log(level: KernelLogLevel, record: Builder => Builder): F[Unit] = {
        l.log(level, record(_).adaptMessage(f))
      }

trait Log[Ctx] {
def timestamp: Option[FiniteDuration]
def level: KernelLogLevel
def message: String
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be lazy all the way to the edge.

Suggested change
def message: String
def message: () => String

Comment on lines 39 to 40
def unsafeThrowable: Throwable
def unsafeContext: Map[String, Ctx]
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't see these used anywhere, I think we can probably remove them.

contextMap: Map[String, A]
)(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] =
contextMap.foldLeft(this) { case (builder, (k, v)) => builder.withContext(k)(v) }

Copy link
Contributor

Choose a reason for hiding this comment

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

Implementing withModifiedString and addContext will be easier if we add some helpers here.

Suggested change
def adaptTimestamp(f: FiniteDuration => FiniteDuration): Builder[Ctx]
def adaptLevel(f: KernelLogLevel => KernelLogLevel): Builder[Ctx]
def adaptMessage(f: String => String): Builder[Ctx]
def adaptThrowable(f: Throwable => Throwable): Builder[Ctx]
def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): Builder[Ctx]
def adaptFileName(f: String => String): Builder[Ctx]
def adaptClassName(f: String => String): Builder[Ctx]
def adaptMethodName(f: String => String): Builder[Ctx]
def adaptLine(f: Int => Int): Builder[Ctx]

private class MutableBuilder[Ctx] extends Builder[Ctx] {
private var _timestamp: Option[FiniteDuration] = None
private var _level: Option[KernelLogLevel] = None
private var _message: Option[String] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

To keep this lazy, we'd need to update here as well

Suggested change
private var _message: Option[String] = None
private var _message: () => String = () => ""

private var _level: Option[KernelLogLevel] = None
private var _message: Option[String] = None
private var _throwable: Option[Throwable] = None
private val _context: mutable.Map[String, Ctx] = mutable.Map.empty[String, Ctx]
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a builder here will make things a bit easier when writing adaptContext

Suggested change
private val _context: mutable.Map[String, Ctx] = mutable.Map.empty[String, Ctx]
private var _context: mutable.Builder[(String, Ctx), Map[String, Ctx]] = Map.newBuilder[String, Ctx]


private class MutableBuilder[Ctx] extends Builder[Ctx] {
private var _timestamp: Option[FiniteDuration] = None
private var _level: Option[KernelLogLevel] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be simplified, since we have a default value we're going to use anyway.

Suggested change
private var _level: Option[KernelLogLevel] = None
private var _level: KernelLogLevel = KernelLogLevel.Info

_line = if (line > 0) Some(line) else None
this
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here's the adapt* methods

Suggested change
}
override def adaptTimestamp(f: FiniteDuration => FiniteDuration): Builder[Ctx] = {
_timestamp = _timestamp.map(f)
this
}
override def adaptLevel(f: KernelLogLevel => KernelLogLevel): Builder[Ctx] = {
_level = f(_level)
this
}
override def adaptMessage(f: String => String): Builder[Ctx] = {
_message = () => f(_message())
this
}
override def adaptThrowable(f: Throwable => Throwable): Builder[Ctx] = {
_throwable = _throwable.map(f)
this
}
override def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): Builder[Ctx] = {
_context = _context.mapResult[Map[String, Ctx]](f)
this
}
override def adaptFileName(f: String => String): Builder[Ctx] = {
_fileName = _fileName.map(f)
this
}
override def adaptClassName(f: String => String): Builder[Ctx] = {
_className = _className.map(f)
this
}
override def adaptMethodName(f: String => String): Builder[Ctx] = {
_methodName = _methodName.map(f)
this
}
override def adaptLine(f: Int => Int): Builder[Ctx] = {
_line = _line.map(f)
this
}
}

Comment on lines 32 to 37
val builder = (b: Log.Builder[String]) => {
var current = b.withMessage(msg)
ctx.foreach { case (k, v) => current = current.withContext[String](k)(v) }
current
}
kernel.logTrace(builder)
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth exercising the methods on Log.Builder to see if they pull their weight.
For example, this can be simplified to:

Suggested change
val builder = (b: Log.Builder[String]) => {
var current = b.withMessage(msg)
ctx.foreach { case (k, v) => current = current.withContext[String](k)(v) }
current
}
kernel.logTrace(builder)
kernel.logTrace(_.withMessage(msg).withContextMap(ctx))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants