-
-
Notifications
You must be signed in to change notification settings - Fork 80
SAM Type Interface #920
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
base: main
Are you sure you want to change the base?
SAM Type Interface #920
Conversation
core/shared/src/main/scala/org/typelevel/log4cats/JsonLike.scala
Outdated
Show resolved
Hide resolved
| final case class KernelLogLevel(name: String, value: Double) { | ||
| def namePadded: String = KernelLogLevel.padded(this) | ||
|
|
||
| KernelLogLevel.add(this) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we haven't been able to get on a call, this is what it would look like as a sealed trait, which is the usual way to avoid bincompat issues:
sealed trait KernelLogLevel {
def value: Int
def name: String
def namePadded: String = name.padTo(5, ' ').mkString
}
object KernelLogLevel {
implicit final val log4catsCatsInstances: Eq[KernelLogLevel] & Hash[KernelLogLevel] & Order[KernelLogLevel] & Show[KernelLogLevel] =
new Hash[KernelLogLevel] with Order[KernelLogLevel] with Show[KernelLogLevel] {
override def eqv(x: KernelLogLevel, y: KernelLogLevel): Boolean = x.value == y.value
override def hash(x: KernelLogLevel): Int = x.value.##
override def compare(x: KernelLogLevel, y: KernelLogLevel): Int = x.value.compare(y.value)
override def show(t: KernelLogLevel): String = t.name
}
private final class LogLevel(override val name: String, override val value: Int) extends KernelLogLevel {
override def toString: String = name
override def hashCode(): Int = value.##
override def equals(obj: Any): Boolean = obj match {
case that: LogLevel => this.value == that.value
case _ => false
}
}
val Trace: KernelLogLevel = new LogLevel("TRACE", 100)
val Debug: KernelLogLevel = new LogLevel("DEBUG", 200)
val Info: KernelLogLevel = new LogLevel("INFO", 300)
val Warn: KernelLogLevel = new LogLevel("WARN", 400)
val Error: KernelLogLevel = new LogLevel("ERROR", 500)
val Fatal: KernelLogLevel = new LogLevel("FATAL", 600)
}In this case though, we should be safe to use a sealed trait + case objects, because the old LogLevel has been very stable. The downside is that, if we do need to add one, it'll break bin- & source-compat. The upside is we get comprehensiveness checks.
import cats.{Order, Show}
import cats.kernel.{Eq, Hash}
sealed abstract class KernelLogLevel(val name: String, val value: Int) {
def namePadded: String = name.padTo(5, ' ').mkString
override def toString: String = name
}
object KernelLogLevel {
implicit final val log4catsCatsInstances: Eq[KernelLogLevel] & Hash[KernelLogLevel] & Order[KernelLogLevel] & Show[KernelLogLevel] =
new Hash[KernelLogLevel] with Order[KernelLogLevel] with Show[KernelLogLevel] {
override def eqv(x: KernelLogLevel, y: KernelLogLevel): Boolean = x == y
override def hash(x: KernelLogLevel): Int = x.##
override def compare(x: KernelLogLevel, y: KernelLogLevel): Int = x.value.compare(y.value)
override def show(t: KernelLogLevel): String = t.name
}
case object Trace extends KernelLogLevel("TRACE", 100)
case object Debug extends KernelLogLevel("DEBUG", 200)
case object Info extends KernelLogLevel("INFO", 300)
case object Warn extends KernelLogLevel("WARN", 400)
case object Error extends KernelLogLevel("ERROR", 500)
case object Fatal extends KernelLogLevel("FATAL", 600)
}
core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala
Outdated
Show resolved
Hide resolved
core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala
Outdated
Show resolved
Hide resolved
core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala
Outdated
Show resolved
Hide resolved
core/shared/src/main/scala/org/typelevel/log4cats/LoggerKernel.scala
Outdated
Show resolved
Hide resolved
js-console/src/main/scala/org/typelevel/log4cats/console/ConsoleLogger.scala
Outdated
Show resolved
Hide resolved
|
Thank you for all the suggestions, I will rework on it |
core/shared/src/main/scala/org/typelevel/log4cats/KernelLogLevel.scala
Outdated
Show resolved
Hide resolved
|
@morgen-peschke just a gentle reminder regarding the PR review whenever you get a chance. I understand things can get busy thank you for your time and help! |
build.sbt
Outdated
| // Added new kernel methods - these are new API additions | ||
| ProblemFilters.exclude[ReversedMissingMethodProblem]("org.typelevel.log4cats.Logger.kernel"), | ||
| ProblemFilters.exclude[ReversedMissingMethodProblem]( | ||
| "org.typelevel.log4cats.extras.DeferredLogger.kernel" | ||
| ), | ||
| ProblemFilters.exclude[ReversedMissingMethodProblem]( | ||
| "org.typelevel.log4cats.extras.DeferredSelfAwareStructuredLogger.kernel" | ||
| ), | ||
| ProblemFilters.exclude[ReversedMissingMethodProblem]( | ||
| "org.typelevel.log4cats.extras.DeferredStructuredLogger.kernel" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bumping tlBaseVersion will tell MiMa that you're going to do a minor version bump, and these will go away.
ThisBuild / tlBaseVersion := "2.8"
| * A SAM-based Logger that provides a user-friendly interface. This is the new design that will | ||
| * eventually replace the current Logger trait. | ||
| */ | ||
| trait SamLogger[F[_], Ctx] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleanup: compare with Logger and you'll see they're basically identical so you can remove SamLogger
| * This implementation uses the new SAM LoggerKernel design for better performance and middleware | ||
| * compatibility. | ||
| */ | ||
| trait SamStructuredLogger[F[_]] extends Logger[F] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleanup: compare with StructuredLogger and you'll see they're basically identical (especially their APIs) so this one can be deleted
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are only used by SamStructuredLogger, which can be removed now that StructuredLogger delegates to LoggerKernel.
Now would be a good time to remove them.
| protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { | ||
| def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = | ||
| Applicative[F].pure(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleanup: use the implementation you already have
| protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { | |
| def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = | |
| Applicative[F].pure(()) | |
| } | |
| protected def kernel: LoggerKernel[F, String] = NoOpLoggerKernel[F, String] |
| protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { | ||
| def log(level: KernelLogLevel, record: Log.Builder[String] => Log.Builder[String]): F[Unit] = | ||
| Applicative[F].pure(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concern: This loses the strict eval behavior (might be worth writing a test to catch this regression before fixing)
One way to solve this would be to add something like this to NoOpLoggerKernel:
def strict[F[_]: Applicative, Ctx]: LoggerKernel[F, Ctx] = new LoggerKernel[F, Ctx] {
override def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = {
val _ = record(Log.strictNoOpBuilder[Ctx])
Applicative[F].unit
}
}Which would require adding strictNoOpBuilder to Log, which is pretty straightforward (the critical bit is in the implementation of withMessage, comparing to the implementation of NoOpLogger.strictImpl#void on main may be informative):
private class NoOpBuilder[Ctx] extends Builder[Ctx] {
override def withTimestamp(value: FiniteDuration): Log.Builder[Ctx] = this
override def withLevel(level: KernelLogLevel): Log.Builder[Ctx] = this
override def withMessage(message: => String): Log.Builder[Ctx] = {
val _ = message
this
}
override def withThrowable(throwable: Throwable): Log.Builder[Ctx] = this
override def withContext[A](name: String)(ctx: A)(implicit E: Context.Encoder[A, Ctx]): Log.Builder[Ctx] = this
override def withFileName(name: String): Log.Builder[Ctx] = this
override def withClassName(name: String): Log.Builder[Ctx] = this
override def withMethodName(name: String): Log.Builder[Ctx] = this
override def withLine(line: Int): Log.Builder[Ctx] = this
override def adaptTimestamp(f: FiniteDuration => FiniteDuration): Log.Builder[Ctx] = this
override def adaptLevel(f: KernelLogLevel => KernelLogLevel): Log.Builder[Ctx] = this
override def adaptMessage(f: String => String): Log.Builder[Ctx] = this
override def adaptThrowable(f: Throwable => Throwable): Log.Builder[Ctx] = this
override def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): Log.Builder[Ctx] = this
override def adaptFileName(f: String => String): Log.Builder[Ctx] = this
override def adaptClassName(f: String => String): Log.Builder[Ctx] = this
override def adaptMethodName(f: String => String): Log.Builder[Ctx] = this
override def adaptLine(f: Int => Int): Log.Builder[Ctx] = this
override def build(): Log[Ctx] = new Log[Ctx] {
override def timestamp: Option[FiniteDuration] = None
override def level: KernelLogLevel = KernelLogLevel.Info
override def message: () => String = () => ""
override def throwable: Option[Throwable] = None
override def context: Map[String, Ctx] = Map.empty
override def fileName: Option[String] = None
override def className: Option[String] = None
override def methodName: Option[String] = None
override def line: Option[Int] = None
override def levelValue: Int = KernelLogLevel.Info.value
}
}| def isWarnEnabled: F[Boolean] = Sync[F].pure(warnEnabled) | ||
| def isErrorEnabled: F[Boolean] = Sync[F].pure(errorEnabled) | ||
|
|
||
| protected def kernel: LoggerKernel[F, String] = new LoggerKernel[F, String] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleanup: the loggers should already route everything through their kernels, so most of the methods in the testing loggers should be able to be removed and be implemented using a TestingLoggerKernel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Sam* loggers can be removed, since they're dups of the old interfaces, but I wanted to highlight something important:
because of this we will lose
Structured Logging in SamLogger
...
Throwable Logging
This is intentional, otherwise the types won't help us avoid using capabilities our logger doesn't support.
It might not seem so bad to quietly log only what the logger can support - until you're trying to resolve a customer issue, only to find out the info you need is missing because the context part of the log was dropped 😢
| final case class KernelLogLevel(name: String, value: Double) { | ||
| def namePadded: String = KernelLogLevel.padded(this) | ||
|
|
||
| KernelLogLevel.add(this) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we haven't been able to get on a call, this is what it would look like as a sealed trait, which is the usual way to avoid bincompat issues:
sealed trait KernelLogLevel {
def value: Int
def name: String
def namePadded: String = name.padTo(5, ' ').mkString
}
object KernelLogLevel {
implicit final val log4catsCatsInstances: Eq[KernelLogLevel] & Hash[KernelLogLevel] & Order[KernelLogLevel] & Show[KernelLogLevel] =
new Hash[KernelLogLevel] with Order[KernelLogLevel] with Show[KernelLogLevel] {
override def eqv(x: KernelLogLevel, y: KernelLogLevel): Boolean = x.value == y.value
override def hash(x: KernelLogLevel): Int = x.value.##
override def compare(x: KernelLogLevel, y: KernelLogLevel): Int = x.value.compare(y.value)
override def show(t: KernelLogLevel): String = t.name
}
private final class LogLevel(override val name: String, override val value: Int) extends KernelLogLevel {
override def toString: String = name
override def hashCode(): Int = value.##
override def equals(obj: Any): Boolean = obj match {
case that: LogLevel => this.value == that.value
case _ => false
}
}
val Trace: KernelLogLevel = new LogLevel("TRACE", 100)
val Debug: KernelLogLevel = new LogLevel("DEBUG", 200)
val Info: KernelLogLevel = new LogLevel("INFO", 300)
val Warn: KernelLogLevel = new LogLevel("WARN", 400)
val Error: KernelLogLevel = new LogLevel("ERROR", 500)
val Fatal: KernelLogLevel = new LogLevel("FATAL", 600)
}In this case though, we should be safe to use a sealed trait + case objects, because the old LogLevel has been very stable. The downside is that, if we do need to add one, it'll break bin- & source-compat. The upside is we get comprehensiveness checks.
import cats.{Order, Show}
import cats.kernel.{Eq, Hash}
sealed abstract class KernelLogLevel(val name: String, val value: Int) {
def namePadded: String = name.padTo(5, ' ').mkString
override def toString: String = name
}
object KernelLogLevel {
implicit final val log4catsCatsInstances: Eq[KernelLogLevel] & Hash[KernelLogLevel] & Order[KernelLogLevel] & Show[KernelLogLevel] =
new Hash[KernelLogLevel] with Order[KernelLogLevel] with Show[KernelLogLevel] {
override def eqv(x: KernelLogLevel, y: KernelLogLevel): Boolean = x == y
override def hash(x: KernelLogLevel): Int = x.##
override def compare(x: KernelLogLevel, y: KernelLogLevel): Int = x.value.compare(y.value)
override def show(t: KernelLogLevel): String = t.name
}
case object Trace extends KernelLogLevel("TRACE", 100)
case object Debug extends KernelLogLevel("DEBUG", 200)
case object Info extends KernelLogLevel("INFO", 300)
case object Warn extends KernelLogLevel("WARN", 400)
case object Error extends KernelLogLevel("ERROR", 500)
case object Fatal extends KernelLogLevel("FATAL", 600)
}… sam structuredlogger tests
This PR's objective is to create a SAM type interface in log4cats