-
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.