- 
                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
Changes from 42 commits
485b98b
              8d8223f
              f6e9d55
              e7f0be4
              7f6dc0e
              9c3c243
              9908c00
              4fb2850
              fc58670
              23b2094
              3f1f34c
              7961c15
              a5708c3
              649468d
              ac5aaee
              53dda4f
              634de7d
              b7f0d10
              859753b
              ac1a39b
              3beb5f7
              44ab02b
              10b0c81
              4b746e2
              2ad479c
              842bb7e
              fd6bab1
              63ba4c4
              5b85513
              022f96b
              3d00cf9
              17cb902
              3df4963
              4602d56
              f139d0c
              401c5ad
              f88c446
              10cd36c
              e8dd135
              f9364f1
              aafe9ba
              ad0143e
              bd68364
              d19ffae
              749fd3d
              72376e8
              b0ddff1
              58f2b51
              b606c3b
              6962ff8
              041375c
              468114f
              cb28f49
              f695de3
              f6bf9a3
              329bc40
              a32d908
              809e560
              1b7aeb9
              2a9fe26
              3a13966
              0f8c152
              21b3e6c
              e69b27d
              8bbf794
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * 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 | ||
| 
     | 
||
| import cats.effect.kernel.Sync | ||
| 
     | 
||
| /** | ||
| * A simple console implementation of LoggerKernel for testing the SAM design. | ||
| */ | ||
| class ConsoleLoggerKernel[F[_], Ctx](implicit F: Sync[F]) extends LoggerKernel[F, Ctx] { | ||
| 
     | 
||
| def log(level: KernelLogLevel, record: Log.Builder[Ctx] => Log.Builder[Ctx]): F[Unit] = { | ||
| F.delay { | ||
| val logRecord = record(Log.mutableBuilder[Ctx]()).build() | ||
| 
     | 
||
| val timestamp = logRecord.timestamp.map(_.toMillis).getOrElse(System.currentTimeMillis()) | ||
| // Use simple timestamp formatting instead of java.time.Instant for Scala Native compatibility | ||
| val timeStr = s"${new java.util.Date(timestamp).toString}" | ||
                
       | 
||
| 
     | 
||
| val levelStr = logRecord.level.namePadded | ||
| val message = logRecord.message | ||
| val className = logRecord.className.map(c => s"[$c]").getOrElse("") | ||
| val fileName = | ||
| logRecord.fileName.map(f => s"($f:${logRecord.line.getOrElse(0)})").getOrElse("") | ||
| 
     | 
||
| val contextStr = if (logRecord.context.nonEmpty) { | ||
| val contextPairs = logRecord.context.map { case (k, v) => s"$k=$v" }.mkString(", ") | ||
| s" {$contextPairs}" | ||
| } else "" | ||
| 
     | 
||
| val throwableStr = logRecord.throwable.map(t => s"\n${t.toString}").getOrElse("") | ||
| 
     | 
||
| val logLine = s"$timeStr $levelStr $className$fileName$contextStr $message$throwableStr" | ||
| 
     | 
||
| println(logLine) | ||
| } | ||
| } | ||
| } | ||
| 
     | 
||
| object ConsoleLoggerKernel { | ||
| def apply[F[_], Ctx](implicit F: Sync[F]): ConsoleLoggerKernel[F, Ctx] = | ||
| new ConsoleLoggerKernel[F, Ctx] | ||
| } | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * 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 | ||
| 
     | 
||
| import scala.concurrent.duration.FiniteDuration | ||
| 
     | 
||
| import org.typelevel.log4cats.Context.Encoder | ||
| 
     | 
||
| /** | ||
| * A value that can be written into a json-like construct, provided a visitor. | ||
| */ | ||
| trait Context[C] { | ||
| def capture[A](a: A)(implicit E: Encoder[A, C]): C | ||
| } | ||
| 
     | 
||
| object Context { | ||
| trait Encoder[A, B] { | ||
| def encode(a: A): B | ||
| } | ||
| 
     | 
||
| object Encoder { | ||
| def apply[A, B](implicit ev: Encoder[A, B]): ev.type = ev | ||
| 
     | 
||
| // Identity encoder for when input and output types are the same | ||
| implicit def identityEncoder[A]: Encoder[A, A] = a => a | ||
| 
     | 
||
| implicit val stringToStringEncoder: Encoder[String, String] = a => a | ||
| 
     | 
||
| implicit val intToStringEncoder: Encoder[Int, String] = _.toString | ||
| 
     | 
||
| implicit val longToStringEncoder: Encoder[Long, String] = _.toString | ||
| 
     | 
||
| implicit val doubleToStringEncoder: Encoder[Double, String] = _.toString | ||
| 
     | 
||
| implicit val booleanToStringEncoder: Encoder[Boolean, String] = if (_) "true" else "false" | ||
| 
     | 
||
| // Removed Instant encoder for Scala Native compatibility | ||
| // implicit val instantToStringEncoder: Encoder[Instant, String] = | ||
| // DateTimeFormatter.ISO_INSTANT.format(_) | ||
| 
     | 
||
| implicit val finiteDurationToStringEncoder: Encoder[FiniteDuration, String] = _.toString | ||
| } | ||
| } | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| * 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 | ||
| 
     | 
||
| import cats.Order | ||
| 
     | 
||
| final case class KernelLogLevel(name: String, value: Int) { | ||
| def namePadded: String = KernelLogLevel.padded(this) | ||
| 
     | 
||
| KernelLogLevel.add(this) | ||
| } | ||
| 
     | 
||
| object KernelLogLevel { | ||
| private var maxLength = 0 | ||
| 
     | 
||
| private var map = Map.empty[String, KernelLogLevel] | ||
| private var padded = Map.empty[KernelLogLevel, String] | ||
| 
     | 
||
| implicit final val orderKernelLogLevel: Order[KernelLogLevel] = | ||
| Order.by[KernelLogLevel, Int](-_.value) | ||
| 
     | 
||
| // For Java/legacy interop, if needed (not implicit) | ||
| val LevelOrdering: Ordering[KernelLogLevel] = | ||
| Ordering.by[KernelLogLevel, Int](_.value).reverse | ||
                
       | 
||
| 
     | 
||
| val Trace: KernelLogLevel = KernelLogLevel("TRACE", 100) | ||
| val Debug: KernelLogLevel = KernelLogLevel("DEBUG", 200) | ||
| val Info: KernelLogLevel = KernelLogLevel("INFO", 300) | ||
| val Warn: KernelLogLevel = KernelLogLevel("WARN", 400) | ||
| val Error: KernelLogLevel = KernelLogLevel("ERROR", 500) | ||
| val Fatal: KernelLogLevel = KernelLogLevel("FATAL", 600) | ||
| 
     | 
||
| 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 | ||
| } | ||
| } | ||
                
       | 
||
| 
     | 
||
| def get(name: String): Option[KernelLogLevel] = map.get(name.toLowerCase) | ||
| 
     | 
||
| def apply(name: String): KernelLogLevel = get(name).getOrElse( | ||
| throw new RuntimeException(s"Level not found by name: $name") | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| /* | ||
| * 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 | ||
| 
     | 
||
| import scala.collection.mutable | ||
| import scala.concurrent.duration.FiniteDuration | ||
| 
     | 
||
| /** | ||
| * Low-level interface exposing methods to enrich a log record with relevant information. The | ||
| * methods are designed to capture elements that cannot be easily captured from a monadic context | ||
| * (or by running an effect). Elements such as timestamps should be provided by means of | ||
| * middlewares. | ||
| */ | ||
| trait Log[Ctx] { | ||
| def timestamp: Option[FiniteDuration] | ||
| def level: KernelLogLevel | ||
| def message: () => String | ||
| def throwable: Option[Throwable] | ||
| def context: Map[String, Ctx] | ||
| def fileName: Option[String] | ||
| def className: Option[String] | ||
| def methodName: Option[String] | ||
| def line: Option[Int] | ||
| def levelValue: Int | ||
| } | ||
| 
     | 
||
| object Log { | ||
| trait Builder[Ctx] { | ||
| def withTimestamp(value: FiniteDuration): Builder[Ctx] | ||
| def withLevel(level: KernelLogLevel): Builder[Ctx] | ||
| def withMessage(message: => String): Builder[Ctx] | ||
| def withThrowable(throwable: Throwable): Builder[Ctx] | ||
| def withContext[A](name: String)(ctx: A)(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] | ||
| def withFileName(name: String): Builder[Ctx] | ||
| def withClassName(name: String): Builder[Ctx] | ||
| def withMethodName(name: String): Builder[Ctx] | ||
| def withLine(line: Int): Builder[Ctx] | ||
| 
     | 
||
| final def withContextMap[A]( | ||
| contextMap: Map[String, A] | ||
| )(implicit E: Context.Encoder[A, Ctx]): Builder[Ctx] = | ||
| contextMap.foldLeft(this) { case (builder, (k, v)) => builder.withContext(k)(v) } | ||
| 
     | 
||
                
      
                  morgen-peschke marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| 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] | ||
| 
     | 
||
| def build(): Log[Ctx] | ||
| } | ||
| 
     | 
||
| def mutableBuilder[Ctx](): Builder[Ctx] = new MutableBuilder[Ctx]() | ||
| 
     | 
||
| private class MutableBuilder[Ctx] extends Builder[Ctx] { | ||
| private var _timestamp: Option[FiniteDuration] = None | ||
| private var _level: KernelLogLevel = KernelLogLevel.Info | ||
| private var _message: () => String = () => "" | ||
| private var _throwable: Option[Throwable] = None | ||
| private var _context: mutable.Builder[(String, Ctx), Map[String, Ctx]] = | ||
| Map.newBuilder[String, Ctx] | ||
| private var _fileName: Option[String] = None | ||
| private var _className: Option[String] = None | ||
| private var _methodName: Option[String] = None | ||
| private var _line: Option[Int] = None | ||
| 
     | 
||
| def build(): Log[Ctx] = new Log[Ctx] { | ||
| override def timestamp: Option[FiniteDuration] = _timestamp | ||
| override def level: KernelLogLevel = _level | ||
| override def message: () => String = _message | ||
| override def throwable: Option[Throwable] = _throwable | ||
| override def context: Map[String, Ctx] = _context.result() | ||
| override def className: Option[String] = _className | ||
| override def fileName: Option[String] = _fileName | ||
| override def methodName: Option[String] = _methodName | ||
| override def line: Option[Int] = _line.filter(_ > 0) | ||
| override def levelValue: Int = _level.value | ||
| } | ||
| 
     | 
||
| override def withTimestamp(value: FiniteDuration): this.type = { | ||
| _timestamp = Some(value) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withLevel(level: KernelLogLevel): this.type = { | ||
| _level = level | ||
| this | ||
| } | ||
| 
     | 
||
| override def withMessage(message: => String): this.type = { | ||
| _message = () => message | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptMessage(f: String => String): this.type = { | ||
| _message = () => f(_message()) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptTimestamp(f: FiniteDuration => FiniteDuration): this.type = { | ||
| _timestamp = _timestamp.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptLevel(f: KernelLogLevel => KernelLogLevel): this.type = { | ||
| _level = f(_level) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptThrowable(f: Throwable => Throwable): this.type = { | ||
| _throwable = _throwable.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptContext(f: Map[String, Ctx] => Map[String, Ctx]): this.type = { | ||
| val currentContext = _context.result() | ||
| _context = Map.newBuilder[String, Ctx] | ||
| f(currentContext).foreach { case (k, v) => _context += (k -> v) } | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptFileName(f: String => String): this.type = { | ||
| _fileName = _fileName.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptClassName(f: String => String): this.type = { | ||
| _className = _className.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptMethodName(f: String => String): this.type = { | ||
| _methodName = _methodName.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def adaptLine(f: Int => Int): this.type = { | ||
| _line = _line.map(f) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withThrowable(throwable: Throwable): this.type = { | ||
| _throwable = Some(throwable) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withContext[A]( | ||
| name: String | ||
| )(ctx: A)(implicit E: Context.Encoder[A, Ctx]): this.type = { | ||
| _context += (name -> E.encode(ctx)) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withFileName(name: String): this.type = { | ||
| _fileName = Some(name) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withClassName(name: String): this.type = { | ||
| _className = Some(name) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withMethodName(name: String): this.type = { | ||
| _methodName = Some(name) | ||
| this | ||
| } | ||
| 
     | 
||
| override def withLine(line: Int): this.type = { | ||
| _line = if (line > 0) Some(line) else None | ||
| this | ||
| } | ||
| } | ||
                
      
                  morgen-peschke marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| } | ||
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: Since this isn't something that's supported by the current interfaces, we should probably hold off on adding support for macro-based source code location capture - at least until the rest is done.
If we can figure out how to do it without breaking bin and/or source compat, then we can circle back to add it once the rest is finished.
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.
Reminder to remove the
sourcecodemacros until we can figure out the bin/source-compat story.