Some reorganization and thinking a lot about data propagation.
This commit is contained in:
parent
c1e7e594e8
commit
e3c0a4260b
12 changed files with 129 additions and 112 deletions
|
@ -47,7 +47,7 @@ lazy val testSettings = Seq(
|
||||||
|
|
||||||
lazy val `gs-log` = project
|
lazy val `gs-log` = project
|
||||||
.in(file("."))
|
.in(file("."))
|
||||||
.aggregate(api)
|
.aggregate(data, api)
|
||||||
.settings(noPublishSettings)
|
.settings(noPublishSettings)
|
||||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||||
|
|
||||||
|
@ -56,6 +56,9 @@ lazy val data = project
|
||||||
.settings(sharedSettings)
|
.settings(sharedSettings)
|
||||||
.settings(testSettings)
|
.settings(testSettings)
|
||||||
.settings(name := s"${gsProjectName.value}-data-v${semVerMajor.value}")
|
.settings(name := s"${gsProjectName.value}-data-v${semVerMajor.value}")
|
||||||
|
.settings(libraryDependencies ++= Seq(
|
||||||
|
Deps.Cats.Core
|
||||||
|
))
|
||||||
|
|
||||||
lazy val api = project
|
lazy val api = project
|
||||||
.in(file("modules/api"))
|
.in(file("modules/api"))
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package gs.log.v0.api
|
package gs.log.v0.api
|
||||||
|
|
||||||
|
import gs.log.v0.data.Log
|
||||||
|
import gs.log.v0.data.LogLevel
|
||||||
|
import cats.Show
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for emitting logs. This and [[Log]] are the primary types that
|
* Interface for emitting logs. This and [[Log]] are the primary types that
|
||||||
* users of this library will interact with.
|
* users of this library will interact with.
|
||||||
|
@ -86,7 +90,9 @@ object Logger:
|
||||||
|
|
||||||
given CanEqual[Name, Name] = CanEqual.derived
|
given CanEqual[Name, Name] = CanEqual.derived
|
||||||
|
|
||||||
extension (name: Name) def str(): String = name
|
given Show[Name] = new Show[Name] {
|
||||||
|
override def show(t: Name): String = t
|
||||||
|
}
|
||||||
|
|
||||||
end Name
|
end Name
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package gs.log.v0.syntax
|
|
||||||
|
|
||||||
import gs.log.v0.data.LogMessage
|
|
||||||
import gs.log.v0.data.LogData
|
|
||||||
|
|
||||||
private object LogMessageBuilder extends LogMessage.Builder
|
|
||||||
private object LogDataSyntax extends LogData.Syntax
|
|
||||||
|
|
||||||
export LogDataSyntax.*
|
|
||||||
export LogMessageBuilder.*
|
|
|
@ -1,57 +0,0 @@
|
||||||
package gs.log.v0.data
|
|
||||||
|
|
||||||
import scala.reflect.ClassTag
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
trait DataRenderer:
|
|
||||||
def render[A](data: A)(using CT: ClassTag[A]): String
|
|
||||||
|
|
||||||
object DataRenderer:
|
|
||||||
|
|
||||||
abstract class SingleTypeRenderer[A]:
|
|
||||||
def render(data: A): String
|
|
||||||
|
|
||||||
object Defaults:
|
|
||||||
|
|
||||||
final class StringRenderer extends SingleTypeRenderer[String]:
|
|
||||||
override def render(data: String): String = data
|
|
||||||
|
|
||||||
final class BooleanRenderer extends SingleTypeRenderer[Boolean]:
|
|
||||||
override def render(data: Boolean): String = data.toString()
|
|
||||||
|
|
||||||
final class IntRenderer extends SingleTypeRenderer[Int]:
|
|
||||||
override def render(data: Int): String = data.toString()
|
|
||||||
|
|
||||||
final class LongRenderer extends SingleTypeRenderer[Long]:
|
|
||||||
override def render(data: Long): String = data.toString()
|
|
||||||
|
|
||||||
final class FloatRenderer extends SingleTypeRenderer[Float]:
|
|
||||||
override def render(data: Float): String = data.toString()
|
|
||||||
|
|
||||||
final class DoubleRenderer extends SingleTypeRenderer[Double]:
|
|
||||||
override def render(data: Double): String = data.toString()
|
|
||||||
|
|
||||||
final class DateRenderer extends SingleTypeRenderer[LocalDate]:
|
|
||||||
override def render(data: LocalDate): String = data.toString()
|
|
||||||
|
|
||||||
final class InstantRenderer extends SingleTypeRenderer[Instant]:
|
|
||||||
override def render(data: Instant): String = data.toString()
|
|
||||||
|
|
||||||
end Defaults
|
|
||||||
|
|
||||||
class RegistryRenderer(
|
|
||||||
private val registry: Map[Class[?], SingleTypeRenderer[?]]
|
|
||||||
):
|
|
||||||
|
|
||||||
def render[A](data: A)(using CT: ClassTag[A]): String =
|
|
||||||
getRendererFor[A].render(data)
|
|
||||||
|
|
||||||
private def getRendererFor[A](using CT: ClassTag[A]): SingleTypeRenderer[A] =
|
|
||||||
Option(registry.get(CT.runtimeClass)) match
|
|
||||||
case Some(r) => r.asInstanceOf[SingleTypeRenderer[A]]
|
|
||||||
case None => throw new IllegalArgumentException("No renderer in registry!")
|
|
||||||
|
|
||||||
end RegistryRenderer
|
|
||||||
|
|
||||||
end DataRenderer
|
|
|
@ -1,7 +1,4 @@
|
||||||
package gs.log.v0.api
|
package gs.log.v0.data
|
||||||
|
|
||||||
import gs.log.v0.data.LogData
|
|
||||||
import gs.log.v0.data.LogMessage
|
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import scala.collection.mutable.{Map => MutMap}
|
import scala.collection.mutable.{Map => MutMap}
|
||||||
|
@ -127,11 +124,11 @@ object Log:
|
||||||
/**
|
/**
|
||||||
* Instantiate a new log with the given message.
|
* Instantiate a new log with the given message.
|
||||||
*
|
*
|
||||||
* @param tags The message.
|
* @param logMessage The message.
|
||||||
* @return New [[Log]] instance.
|
* @return New [[Log]] instance.
|
||||||
*/
|
*/
|
||||||
def message(msg: LogMessage): Log =
|
def message(logMessage: LogMessage): Log =
|
||||||
new Log().message(msg)
|
new Log().message(logMessage)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate a new log with the given exception.
|
* Instantiate a new log with the given exception.
|
||||||
|
@ -155,7 +152,7 @@ object Log:
|
||||||
case class Metadata(
|
case class Metadata(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
timestamp: Instant,
|
timestamp: Instant,
|
||||||
owner: Logger.Name
|
owner: String
|
||||||
)
|
)
|
||||||
|
|
||||||
end Log
|
end Log
|
|
@ -34,7 +34,7 @@ object LogData:
|
||||||
* by callers - [[Safe]] is used to ensure that all data is explicitly
|
* by callers - [[Safe]] is used to ensure that all data is explicitly
|
||||||
* denoted.
|
* denoted.
|
||||||
*/
|
*/
|
||||||
sealed trait Value:
|
trait Value:
|
||||||
type A
|
type A
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
|
@ -42,33 +42,73 @@ object LogData:
|
||||||
*/
|
*/
|
||||||
def data: A
|
def data: A
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for strings.
|
||||||
|
*
|
||||||
|
* @param data The string data.
|
||||||
|
*/
|
||||||
case class Str(data: String) extends Value:
|
case class Str(data: String) extends Value:
|
||||||
override type A = String
|
override type A = String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for Boolean values.
|
||||||
|
*
|
||||||
|
* @param data The Boolean data.
|
||||||
|
*/
|
||||||
case class Bool(data: Boolean) extends Value:
|
case class Bool(data: Boolean) extends Value:
|
||||||
override type A = Boolean
|
override type A = Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for 32-bit integers.
|
||||||
|
*
|
||||||
|
* @param data The integer data.
|
||||||
|
*/
|
||||||
case class Int32(data: Int) extends Value:
|
case class Int32(data: Int) extends Value:
|
||||||
override type A = Int
|
override type A = Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for 64-bit integers.
|
||||||
|
*
|
||||||
|
* @param data The long integer data.
|
||||||
|
*/
|
||||||
case class Int64(data: Long) extends Value:
|
case class Int64(data: Long) extends Value:
|
||||||
override type A = Long
|
override type A = Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for 32-bit floating point numbers.
|
||||||
|
*
|
||||||
|
* @param data The floating point data.
|
||||||
|
*/
|
||||||
case class Float32(data: Float) extends Value:
|
case class Float32(data: Float) extends Value:
|
||||||
override type A = Float
|
override type A = Float
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for 64-bit floating point numbers.
|
||||||
|
*
|
||||||
|
* @param data The double-precision floating point data.
|
||||||
|
*/
|
||||||
case class Float64(data: Double) extends Value:
|
case class Float64(data: Double) extends Value:
|
||||||
override type A = Double
|
override type A = Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for dates.
|
||||||
|
*
|
||||||
|
* @param data The date data.
|
||||||
|
*/
|
||||||
case class Date(data: LocalDate) extends Value:
|
case class Date(data: LocalDate) extends Value:
|
||||||
override type A = LocalDate
|
override type A = LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide standard capture support for instants.
|
||||||
|
*
|
||||||
|
* @param data The instant data.
|
||||||
|
*/
|
||||||
case class Instant(data: java.time.Instant) extends Value:
|
case class Instant(data: java.time.Instant) extends Value:
|
||||||
override type A = java.time.Instant
|
override type A = java.time.Instant
|
||||||
|
|
||||||
// TODO: IS THIS APPROPRIATE?
|
trait Generic[AA, Codec[_]] extends Value:
|
||||||
case class Generic[AA](data: AA) extends Value:
|
|
||||||
override type A = AA
|
override type A = AA
|
||||||
|
def codec: Codec[A]
|
||||||
|
|
||||||
/** Indicates data (in terms of [[Value]]) that has been explicitly marked as
|
/** Indicates data (in terms of [[Value]]) that has been explicitly marked as
|
||||||
* _safe_ to log in some form.
|
* _safe_ to log in some form.
|
||||||
|
@ -177,7 +217,7 @@ object LogData:
|
||||||
* The value that is safe to log in clear text.
|
* The value that is safe to log in clear text.
|
||||||
*/
|
*/
|
||||||
def logClearText[A: Loggable](data: A): ClearText =
|
def logClearText[A: Loggable](data: A): ClearText =
|
||||||
ClearText(Loggable[A].renderForLogs(data))
|
ClearText(Loggable[A].capture(data))
|
||||||
|
|
||||||
/** The specified data is _not_ safe to log in clear text, and must be
|
/** The specified data is _not_ safe to log in clear text, and must be
|
||||||
* encrypted before being output. In this use case, the data is valuable
|
* encrypted before being output. In this use case, the data is valuable
|
||||||
|
@ -201,7 +241,7 @@ object LogData:
|
||||||
* The value to encrypt.
|
* The value to encrypt.
|
||||||
*/
|
*/
|
||||||
def logEncrypted[A: Loggable](data: A): RequiresEncryption =
|
def logEncrypted[A: Loggable](data: A): RequiresEncryption =
|
||||||
RequiresEncryption(Loggable[A].renderForLogs(data))
|
RequiresEncryption(Loggable[A].capture(data))
|
||||||
|
|
||||||
/** The contianed data is _not_ safe to log in clear text, and must be
|
/** The contianed data is _not_ safe to log in clear text, and must be
|
||||||
* hashed before being output. In this use case, the data is valuable to
|
* hashed before being output. In this use case, the data is valuable to
|
||||||
|
@ -227,7 +267,7 @@ object LogData:
|
||||||
* The value to hash.
|
* The value to hash.
|
||||||
*/
|
*/
|
||||||
def logHashed[A: Loggable](data: A): RequiresSecureHash =
|
def logHashed[A: Loggable](data: A): RequiresSecureHash =
|
||||||
RequiresSecureHash(Loggable[A].renderForLogs(data))
|
RequiresSecureHash(Loggable[A].capture(data))
|
||||||
|
|
||||||
trait Syntax:
|
trait Syntax:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package gs.log.v0.api
|
package gs.log.v0.data
|
||||||
|
|
||||||
/** Enumerates all supported log levels, which control logging verbosity.
|
/** Enumerates all supported log levels, which control logging verbosity.
|
||||||
*
|
*
|
|
@ -1,5 +1,7 @@
|
||||||
package gs.log.v0.data
|
package gs.log.v0.data
|
||||||
|
|
||||||
|
import cats.Show
|
||||||
|
|
||||||
/** Opaque type (String) for representing messages that should be logged. This
|
/** Opaque type (String) for representing messages that should be logged. This
|
||||||
* type can _only_ be instantiated using the `log` string interpolator, which
|
* type can _only_ be instantiated using the `log` string interpolator, which
|
||||||
* allows it to be (relatively) "safe" in that users _must_ explicitly declare
|
* allows it to be (relatively) "safe" in that users _must_ explicitly declare
|
||||||
|
@ -17,27 +19,58 @@ object LogMessage:
|
||||||
|
|
||||||
given CanEqual[LogMessage, LogMessage] = CanEqual.derived
|
given CanEqual[LogMessage, LogMessage] = CanEqual.derived
|
||||||
|
|
||||||
|
given Show[LogMessage] = new Show[LogMessage] {
|
||||||
|
override def show(t: LogMessage): String = t
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a string that is explicitly safe to show in clear text.
|
||||||
|
*
|
||||||
|
* @param data The clear text data.
|
||||||
|
*/
|
||||||
|
case class ClearTextString(data: String)
|
||||||
|
|
||||||
|
object ClearTextString:
|
||||||
|
|
||||||
|
given CanEqual[ClearTextString, ClearTextString] = CanEqual.derived
|
||||||
|
|
||||||
|
given Show[ClearTextString] = new Show[ClearTextString] {
|
||||||
|
override def show(t: ClearTextString): String = t.data
|
||||||
|
}
|
||||||
|
|
||||||
|
end ClearTextString
|
||||||
|
|
||||||
extension (logMessage: LogMessage)
|
extension (logMessage: LogMessage)
|
||||||
/** Express this [[LogMessage]] as a string.
|
/** Express this [[LogMessage]] as a string.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* The string rendition of this [[LogMessage]].
|
* The string rendition of this [[LogMessage]].
|
||||||
*/
|
*/
|
||||||
def str(): String = logMessage
|
def unwrap(): String = logMessage
|
||||||
|
|
||||||
trait Builder:
|
trait Syntax:
|
||||||
|
|
||||||
extension (sc: StringContext)(using R: DataRenderer)
|
extension [A: Show](data: A)
|
||||||
|
/**
|
||||||
|
* Explicitly mark this data as _safe_ to render in clear text. Produce a
|
||||||
|
* string representation.
|
||||||
|
*
|
||||||
|
* @return The clear text string representation of this data.
|
||||||
|
*/
|
||||||
|
def clearTextString(): ClearTextString =
|
||||||
|
ClearTextString(Show[A].show(data))
|
||||||
|
|
||||||
|
extension (sc: StringContext)
|
||||||
|
|
||||||
/** String interpolator for instantiating a [[LogMessage]].
|
/** String interpolator for instantiating a [[LogMessage]].
|
||||||
*
|
*
|
||||||
* @param args
|
* @param args
|
||||||
* All interpolation variables. Must be [[LogData.ClearText]]
|
* All interpolation variables. Must be [[LogMessage.ClearTextString]]
|
||||||
* instances.
|
* instances.
|
||||||
* @return
|
* @return
|
||||||
* The new [[LogMessage]].
|
* The new [[LogMessage]].
|
||||||
*/
|
*/
|
||||||
def log(args: LogData.ClearText*): LogMessage =
|
def log(args: ClearTextString*): LogMessage =
|
||||||
sc.s(args.map(d => R.render(d.value))*)
|
sc.s(args.map(cts => cts.data)*)
|
||||||
|
|
||||||
end LogMessage
|
end LogMessage
|
||||||
|
|
|
@ -7,13 +7,13 @@ import java.time.LocalDate
|
||||||
*/
|
*/
|
||||||
trait Loggable[A]:
|
trait Loggable[A]:
|
||||||
/**
|
/**
|
||||||
* Render the given data as some primitive supported by `gs-log`. Note that
|
* Capture the given data as some primitive supported by `gs-log` for
|
||||||
* this type class does **not** support structured types.
|
* propagating data for future rendition.
|
||||||
*
|
*
|
||||||
* @param data The data to render.
|
* @param data The data to capture.
|
||||||
* @return The [[LogData.Value]] housing the output primitive.
|
* @return The [[LogData.Value]] housing the output primitive.
|
||||||
*/
|
*/
|
||||||
def renderForLogs(data: A): LogData.Value
|
def capture(data: A): LogData.Value
|
||||||
|
|
||||||
object Loggable:
|
object Loggable:
|
||||||
|
|
||||||
|
|
10
modules/data/src/main/scala/gs/log/v0/syntax/package.scala
Normal file
10
modules/data/src/main/scala/gs/log/v0/syntax/package.scala
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package gs.log.v0.syntax
|
||||||
|
|
||||||
|
import gs.log.v0.data.LogMessage
|
||||||
|
import gs.log.v0.data.LogData
|
||||||
|
|
||||||
|
private object LogMessageSyntax extends LogMessage.Syntax
|
||||||
|
private object LogDataSyntax extends LogData.Syntax
|
||||||
|
|
||||||
|
export LogDataSyntax.*
|
||||||
|
export LogMessageSyntax.*
|
|
@ -1,33 +1,28 @@
|
||||||
package gs.log.v0.data
|
package gs.log.v0.data
|
||||||
|
|
||||||
|
import gs.log.v0.syntax.*
|
||||||
|
import cats.syntax.all.*
|
||||||
|
|
||||||
class LogMessageTests extends munit.FunSuite:
|
class LogMessageTests extends munit.FunSuite:
|
||||||
|
|
||||||
import LogMessageTests.Syntax.*
|
|
||||||
|
|
||||||
test("should instantiate a log message from a string literal") {
|
test("should instantiate a log message from a string literal") {
|
||||||
val msg = log"1 2 3 4"
|
val msg = log"1 2 3 4"
|
||||||
assert(msg.str() == "1 2 3 4")
|
assertEquals(msg.show, "1 2 3 4")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should support equality checks") {
|
test("should support equality checks") {
|
||||||
val msg = log"1 2 3 4"
|
val msg = log"1 2 3 4"
|
||||||
val alt = log"1 2 3 4"
|
val alt = log"1 2 3 4"
|
||||||
assert(msg == alt)
|
assertEquals(msg, alt)
|
||||||
assert(msg.str() == alt.str())
|
assertEquals(msg.show, alt.show)
|
||||||
assert(msg.toString() == "1 2 3 4")
|
assertEquals(msg.toString(), "1 2 3 4")
|
||||||
assert(msg.hashCode() == "1 2 3 4".hashCode())
|
assertEquals(msg.hashCode(), "1 2 3 4".hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should accept variables safe to log in clear text") {
|
test("should accept variables safe to log in clear text") {
|
||||||
val x = "foo".logClearText()
|
val x = "foo".clearTextString()
|
||||||
val y = 10.logClearText()
|
val y = 10.clearTextString()
|
||||||
val msg: LogMessage = log"x = $x, y = $y"
|
val msg: LogMessage = log"x = $x, y = $y"
|
||||||
val expected = s"x = ${x.value.renderBytes()}, y = ${y.value.renderBytes()}"
|
val expected = s"x = ${x.data}, y = ${y.data}"
|
||||||
assert(msg.str() == expected)
|
assertEquals(msg.show, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
object LogMessageTests:
|
|
||||||
|
|
||||||
private object Syntax extends LogMessage.Builder
|
|
||||||
|
|
||||||
end LogMessageTests
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
sbt.version=1.9.9
|
sbt.version=1.10.0
|
||||||
|
|
Loading…
Add table
Reference in a new issue