From 0a3b6e29b2c138b113c37944c319308befd71a64 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 1 May 2022 15:46:33 -0700 Subject: [PATCH] Initial commit --- .gitignore | 21 + .scalafmt.conf | 11 + README.md | 219 ++++++ .../scala/api/AbstractLoggerSupport.scala | 8 + .../echopraxia/scala/api/Condition.scala | 19 + .../scala/api/DefaultMethodsSupport.scala | 11 + .../echopraxia/scala/api/FieldBuilder.scala | 75 ++ .../echopraxia/scala/api/Level.scala | 43 ++ .../echopraxia/scala/api/LoggerSupport.scala | 16 + .../echopraxia/scala/api/LoggingContext.scala | 55 ++ .../scala/api/ToFieldBuilderResult.scala | 44 ++ .../echopraxia/scala/api/ToValue.scala | 177 +++++ .../echopraxia/scala/api/Utilities.scala | 40 ++ .../echopraxia/scala/async/AsyncLogger.scala | 50 ++ .../scala/async/AsyncLoggerFactory.scala | 29 + .../scala/async/AsyncLoggerMethods.scala | 449 ++++++++++++ .../async/DefaultAsyncLoggerMethods.scala | 675 ++++++++++++++++++ build.sbt | 127 ++++ .../scala/DefaultLoggerMethods.scala | 493 +++++++++++++ .../echopraxia/scala/Logger.scala | 53 ++ .../echopraxia/scala/LoggerFactory.scala | 29 + .../echopraxia/scala/LoggerMethods.scala | 353 +++++++++ logger/src/test/resources/logback-test.xml | 10 + .../echopraxia/scala/ConditionSpec.scala | 255 +++++++ .../echopraxia/scala/ScalaLoggerSpec.scala | 293 ++++++++ .../echopraxia/scala/SourceLoggerSpec.scala | 53 ++ release.sbt | 18 + sonatype.sbt | 21 + version.sbt | 1 + 29 files changed, 3648 insertions(+) create mode 100644 .gitignore create mode 100644 .scalafmt.conf create mode 100644 README.md create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/AbstractLoggerSupport.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/Condition.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/DefaultMethodsSupport.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/FieldBuilder.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/Level.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggerSupport.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggingContext.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToFieldBuilderResult.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToValue.scala create mode 100644 api/src/main/scala/com/tersesystems/echopraxia/scala/api/Utilities.scala create mode 100644 async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLogger.scala create mode 100644 async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerFactory.scala create mode 100644 async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerMethods.scala create mode 100644 async/src/main/scala/com/tersesystems/echopraxia/scala/async/DefaultAsyncLoggerMethods.scala create mode 100644 build.sbt create mode 100644 logger/src/main/scala/com/tersesystems/echopraxia/scala/DefaultLoggerMethods.scala create mode 100644 logger/src/main/scala/com/tersesystems/echopraxia/scala/Logger.scala create mode 100644 logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerFactory.scala create mode 100644 logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerMethods.scala create mode 100644 logger/src/test/resources/logback-test.xml create mode 100644 logger/src/test/scala/com/tersesystems/echopraxia/scala/ConditionSpec.scala create mode 100644 logger/src/test/scala/com/tersesystems/echopraxia/scala/ScalaLoggerSpec.scala create mode 100644 logger/src/test/scala/com/tersesystems/echopraxia/scala/SourceLoggerSpec.scala create mode 100644 release.sbt create mode 100644 sonatype.sbt create mode 100644 version.sbt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..cfd1bfab --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea/ +.bsp/ +.bloop/ +.metals/ +.vscode/ + +bin/ + +project/ +target/ + +*.log + +target/ +.bsp/ diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 00000000..4673b502 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,11 @@ +version = 3.4.0 + +runner.dialect=scala212 + +maxColumn = 100 // For my wide 30" display. + +align.preset = more // For pretty alignment. + +# align.openParenDefnSite = true +# docstrings.style = asterisk +docstrings.style = keep diff --git a/README.md b/README.md new file mode 100644 index 00000000..89bc397e --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# Scala API for Echopraxia + +The Scala API for Echopraxia is a layer over the Java API that works smoothly with Scala types. + +## Quick Start + +Add the following to your `build.sbt` file: + +```scala +libraryDependencies += "com.tersesystems.echopraxia" %% "scala-logger" % "2.0.0-SNAPSHOT" +``` + +To import the Scala API, add the following: + +```scala +import com.tersesystems.echopraxia.sapi._ + +class Example { + val logger = LoggerFactory.getLogger + + def doStuff: Unit = { + logger.info("do some stuff") + } +} +``` + +## Source Code + +The Scala API can integrate source code metadata into logging statements. + +```scala +libraryDependencies += "com.tersesystems.echopraxia" %% "scala-sourcecode" % "1.5.0-SNAPSHOT" +``` + +The API is the same, but you must import `sapi.sourcecode._`: + +```scala +import com.tersesystems.echopraxia.sapi.sourcecode._ + +class Example { + val logger = LoggerFactory.getLogger + + def doStuff: Unit = { + logger.info("do some stuff") + } +} +``` + +Using this method adds the following fields on every statement: + +```scala +trait DefaultLoggerMethods[FB] extends LoggerMethods[FB] { + this: DefaultMethodsSupport[FB] => + + protected def sourceInfoFields(fb: FB)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): util.List[Field] = { + fb.onlyObject( + "sourcecode", + fb.string("file", file.value), + fb.number("line", line.value), + fb.string("enclosing", enc.value) + ) + } + + // ... +} +``` + +## Field Builder + +A field is defined as a `name` and a `value`, where the value can one of the types defined in `Field.Value`. Defining a value like `StringValue` or `BooleanValue` can be tedious, and so the Scala field builder has methods that take `ToValue`, `ToObjectValue`, and `ToArrayValue` type classes. + +The field builder can be imported with `import fb._` to provide a custom DSL that relies on tuples. The built-in type classes are already provided for strings, numbers, and booleans, and there are also type classes for `Option[V: ToValue]` and `Try[V: ToValue]` types. + +There are two basic methods, `fb.value` and `fb.keyValue`, which render fields defined in a parameterized message template with "hello {}" as `hello value` and `hello key=value` in a line oriented layout, respectively. The more specific methods, `fb.string`, `fb.number`, `fb.bool`, `fb.array`, and `fb.obj` have more specific value requirements. Only `fb.array` and `fb.obj` use `key=value` format, and the other methods use the `value` format. + +```scala +import com.tersesystems.echopraxia.sapi._ + +class Example { + val logger = LoggerFactory.getLogger + + def doStuff: Unit = { + logger.info("{} {} {} {}", fb => fb.list { + import fb._ + obj("person" -> + Seq( + value("number" -> 1), + value("bool" -> true), + array("ints" -> Seq(1, 2, 3)), + keyValue("strName" -> "bar") + ) + ) + }) + } +} +``` + +Arrays will take a `Seq` of values, including object values. Object values take a sequence of fields as arguments, and are best defined using the `Field.Value.object`. For example, the first element in the [path example from Json-Path](https://github.com/json-path/JsonPath#path-examples) can be represented as: + +```scala +logger.info("{}", fb => { + fb.onlyObj("store" -> + fb.array("book" -> Seq( + Field.Value.`object`( + fb.string("category", "reference"), + fb.string("author", "Nigel Rees"), + fb.string("title", "Sayings of the Century"), + fb.number("price", 8.95) + ) + )) + ) +}) +``` + +## Custom Field Builder + +You can create your own field builder and define type class instances. For example, to map an `java.time.Instant` to a string, you would add the following: + +```scala +import java.time._ + +final class CustomFieldBuilder extends FieldBuilder { + import java.time.Instant + + implicit val instantToStringValue: ToValue[Instant] = ToValue(i => Field.Value.string(i.toString)) + + def instant(name: String, instant: Instant): Field = keyValue(name, instant) + def instant(tuple: (String, Instant)): Field = instant(tuple._1, tuple._2) +} +``` + +And then you render an instant; + +```scala +logger.info("time {}", fb.only(fb.instant("current", Instant.now))) +``` + +Or you can import the field builder implicit: + +```scala +logger.info("time {}", fb => fb.only { + import fb._ + keyValue("current" -> Instant.now) +}) +``` + +You can also convert maps to an object value more generally: + +```scala +class MapFieldBuilder extends FieldBuilder { + + implicit def mapToObjectValue[V: ToValue]: ToObjectValue[Map[String, V]] = new ToObjectValue[Map[String, V]] { + override def toObjectValue(t: Map[String, V]): Value.ObjectValue = { + val fields: Seq[Field] = t.map { + case (k, v) => + keyValue(k, v.toString) + }.toSeq + Field.Value.`object`(fields.asJava) + } + } +} +``` + +## Custom Logger + +You can create a custom logger which has your own methods and field builders by leveraging the `sapi.support` package. + +```scala +import com.tersesystems.echopraxia.Field +import com.tersesystems.echopraxia.core._ +import com.tersesystems.echopraxia.sapi._ +import com.tersesystems.echopraxia.sapi.support._ + +object CustomLoggerFactory { + private val FQCN: String = classOf[DefaultLoggerMethods[_]].getName + private val fieldBuilder: CustomFieldBuilder = new CustomFieldBuilder + + def getLogger(name: String): CustomLogger = { + val core = CoreLoggerFactory.getLogger(FQCN, name) + new CustomLogger(core, fieldBuilder) + } + + def getLogger(clazz: Class[_]): CustomLogger = { + val core = CoreLoggerFactory.getLogger(FQCN, clazz.getName) + new CustomLogger(core, fieldBuilder) + } + + def getLogger: CustomLogger = { + val core = CoreLoggerFactory.getLogger(FQCN, Caller.resolveClassName) + new CustomLogger(core, fieldBuilder) + } +} + +final class CustomLogger(core: CoreLogger, fieldBuilder: CustomFieldBuilder) + extends AbstractLoggerSupport(core, fieldBuilder) with DefaultLoggerMethods[CustomFieldBuilder] { + + @inline + private def newLogger(coreLogger: CoreLogger): CustomLogger = new CustomLogger(coreLogger, fieldBuilder) + + @inline + def withCondition(scalaCondition: Condition): CustomLogger = newLogger(core.withCondition(scalaCondition.asJava)) + + @inline + def withFields(f: CustomFieldBuilder => java.util.List[Field]): CustomLogger = { + import scala.compat.java8.FunctionConverters._ + newLogger(core.withFields(f.asJava, fieldBuilder)) + } + + @inline + def withThreadContext: CustomLogger = { + import com.tersesystems.echopraxia.support.Utilities + newLogger(core.withThreadContext(Utilities.threadContext())) + } +} +``` diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/AbstractLoggerSupport.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/AbstractLoggerSupport.scala new file mode 100644 index 00000000..24c4404e --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/AbstractLoggerSupport.scala @@ -0,0 +1,8 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.CoreLogger + +abstract class AbstractLoggerSupport[FB](val core: CoreLogger, val fieldBuilder: FB) + extends DefaultMethodsSupport[FB] { + def name: String = core.getName +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Condition.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Condition.scala new file mode 100644 index 00000000..72dace77 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Condition.scala @@ -0,0 +1,19 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.{ + Condition => JCondition, + Level => JLevel, + LoggingContext => JLoggingContext +} + +trait Condition { + self => + + def test(level: Level, context: LoggingContext): Boolean + + def asJava: JCondition = { (level: JLevel, javaContext: JLoggingContext) => + { + self.test(Level.asScala(level), LoggingContext(javaContext)) + } + } +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/DefaultMethodsSupport.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/DefaultMethodsSupport.scala new file mode 100644 index 00000000..835c29b1 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/DefaultMethodsSupport.scala @@ -0,0 +1,11 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.CoreLogger + +trait DefaultMethodsSupport[FB] { + def name: String + + def core: CoreLogger + + def fieldBuilder: FB +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/FieldBuilder.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/FieldBuilder.scala new file mode 100644 index 00000000..956bf20a --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/FieldBuilder.scala @@ -0,0 +1,75 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api._ + +/** + * A field builder that is enhanced with ToValue, ToObjectValue, and ToArrayValue. + */ +trait FieldBuilder { + + def list(fields: Field*): FieldBuilderResult = list(fields) + def list[T: ToFieldBuilderResult](input: T): FieldBuilderResult = ToFieldBuilderResult[T](input) + + // ------------------------------------------------------------------ + // keyValue + + def keyValue[V: ToValue](key: String, value: V): Field = + Field.keyValue(key, ToValue[V].toValue(value)) + def keyValue[V: ToValue](tuple: (String, V)): Field = keyValue(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // value + + def value[V: ToValue](key: String, value: V): Field = + Field.value(key, ToValue[V].toValue(value)) + def value[V: ToValue](tuple: (String, V)): Field = value(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // array + + def array[AV: ToArrayValue](name: String, value: AV): Field = + keyValue(name, implicitly[ToArrayValue[AV]].toArrayValue(value)) + def array[AV: ToArrayValue](tuple: (String, AV)): Field = array(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // object + + def obj[OV: ToObjectValue](name: String, value: OV): Field = + keyValue(name, ToObjectValue[OV].toObjectValue(value)) + def obj[OV: ToObjectValue](tuple: (String, OV)): Field = obj(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // string + + def string(name: String, string: String): Field = value(name, string) + def string(tuple: (String, String)): Field = value(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // number + + def number(name: String, number: Number): Field = value(name, number) + def number(tuple: (String, Number)): Field = value(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // boolean + + def bool(name: String, boolean: Boolean): Field = value(name, boolean) + def bool(tuple: (String, Boolean)): Field = value(tuple._1, tuple._2) + + // ------------------------------------------------------------------ + // null + + def nullField(name: String): Field = keyValue(name, Value.NullValue.instance) + + // ------------------------------------------------------------------ + // exception + + def exception(ex: Throwable): Field = value(Field.EXCEPTION, ex) + def exception(name: String, ex: Throwable): Field = keyValue(name, ex) + + // ------------------------------------------------------------------ + // object + +} + +object FieldBuilder extends FieldBuilder diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Level.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Level.scala new file mode 100644 index 00000000..c878b997 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Level.scala @@ -0,0 +1,43 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.{Level => JLevel} + +object Level { + case object TRACE extends Level(JLevel.TRACE) + case object DEBUG extends Level(JLevel.DEBUG) + case object INFO extends Level(JLevel.INFO) + case object WARN extends Level(JLevel.WARN) + case object ERROR extends Level(JLevel.ERROR) + + def asScala(level: JLevel): Level = level match { + case JLevel.TRACE => TRACE + case JLevel.DEBUG => DEBUG + case JLevel.WARN => WARN + case JLevel.INFO => INFO + case JLevel.ERROR => ERROR + } +} + +sealed class Level(private val level: JLevel) { + def isGreater(r: Level): Boolean = level.compareTo(r.level) > 0 + + def isGreater(r: JLevel): Boolean = level.compareTo(r) > 0 + + def isGreaterOrEqual(r: Level): Boolean = level.compareTo(r.level) >= 0 + + def isGreaterOrEqual(r: JLevel): Boolean = level.compareTo(r) >= 0 + + def isLess(r: Level): Boolean = level.compareTo(r.level) < 0 + + def isLess(r: JLevel): Boolean = level.compareTo(r) < 0 + + def isLessOrEqual(r: Level): Boolean = level.compareTo(r.level) <= 0 + + def isLessOrEqual(r: JLevel): Boolean = level.compareTo(r) <= 0 + + def isEqual(r: Level): Boolean = this.level == r.level + + def isEqual(r: JLevel): Boolean = this.level == r + + def asJava: JLevel = level +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggerSupport.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggerSupport.scala new file mode 100644 index 00000000..8aec0f85 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggerSupport.scala @@ -0,0 +1,16 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.FieldBuilderResult + +trait LoggerSupport[FB] { + + type SELF[T] = LoggerSupport[T] + + def withCondition(scalaCondition: Condition): SELF[FB] + + def withFields(f: FB => FieldBuilderResult): SELF[FB] + + def withThreadContext: SELF[FB] + + def withFieldBuilder[T <: FB](newBuilder: T): SELF[T] +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggingContext.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggingContext.scala new file mode 100644 index 00000000..9a7e5f87 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/LoggingContext.scala @@ -0,0 +1,55 @@ +package com.tersesystems.echopraxia.scala.api + +import com.daodecode.scalaj.collection.immutable._ +import com.tersesystems.echopraxia.api.{Field, LoggingContext => JLoggingContext} + +import scala.compat.java8.OptionConverters._ + +object LoggingContext { + + // This repeats stuff in AbstractLoggingContext + private val ExceptionPath = "$." + Field.EXCEPTION + + def apply(context: JLoggingContext): LoggingContext = { + new LoggingContext(context) + } +} + +/** + * A scala logging context. + */ +class LoggingContext private (context: JLoggingContext) { + + def findString(jsonPath: String): Option[String] = { + context.findString(jsonPath).asScala + } + + def findBoolean(jsonPath: String): Option[Boolean] = { + context.findBoolean(jsonPath).asScala.map(_.booleanValue()) + } + + def findNumber(jsonPath: String): Option[Number] = { + context.findNumber(jsonPath).asScala + } + + def findNull(jsonPath: String): Boolean = { + context.findNull(jsonPath) + } + + def findThrowable(jsonPath: String): Option[Throwable] = { + context.findThrowable(jsonPath).asScala + } + + def findThrowable: Option[Throwable] = { + findThrowable(LoggingContext.ExceptionPath) + } + + def findObject(jsonPath: String): Option[Map[String, Any]] = { + context.findObject(jsonPath).asScala.map(_.deepAsScalaImmutable) + } + + def findList(jsonPath: String): Seq[Any] = { + context.findList(jsonPath).deepAsScalaImmutable + } + +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToFieldBuilderResult.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToFieldBuilderResult.scala new file mode 100644 index 00000000..4965846e --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToFieldBuilderResult.scala @@ -0,0 +1,44 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.{Field, FieldBuilderResult} + +// if using -T here then all the subtypes of iterable also apply +trait ToFieldBuilderResult[-T] { + def toResult(input: T): FieldBuilderResult +} + +trait LowPriorityToFieldBuilderResult { + implicit val iterableToFieldBuilderResult: ToFieldBuilderResult[Iterable[Field]] = + new ToFieldBuilderResult[Iterable[Field]] { + override def toResult(iterable: Iterable[Field]): FieldBuilderResult = + FieldBuilderResult.list(iterable.toArray) + } + + implicit val iteratorToFieldBuilderResult: ToFieldBuilderResult[Iterator[Field]] = + new ToFieldBuilderResult[Iterator[Field]] { + import scala.collection.JavaConverters.asJavaIteratorConverter + override def toResult(iterator: Iterator[Field]): FieldBuilderResult = + FieldBuilderResult.list(iterator.asJava) + } + + // @SuppressWarnings(Array("deprecated")) + // implicit val traversableToFieldBuilderResult: ToFieldBuilderResult[Traversable[Field]] = new ToFieldBuilderResult[Traversable[Field]] { + // override def toResult(traversable: Traversable[Field]): FieldBuilderResult = seqToFieldBuilderResult.toResult(traversable.toSeq) + // } + + // implicit val seqToFieldBuilderResult: ToFieldBuilderResult[Seq[Field]] = new ToFieldBuilderResult[Seq[Field]] { + // override def toResult(iterable: Seq[Field]): FieldBuilderResult = FieldBuilderResult.list(iterable.toArray) + // } + + // array doesn't seem to be covered by Iterable + implicit val arrayToFieldBuilderResult: ToFieldBuilderResult[Array[Field]] = + new ToFieldBuilderResult[Array[Field]] { + override def toResult(array: Array[Field]): FieldBuilderResult = + FieldBuilderResult.list(array) + } +} + +object ToFieldBuilderResult extends LowPriorityToFieldBuilderResult { + def apply[T: ToFieldBuilderResult](input: T): FieldBuilderResult = + implicitly[ToFieldBuilderResult[T]].toResult(input) +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToValue.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToValue.scala new file mode 100644 index 00000000..2c5a61e1 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/ToValue.scala @@ -0,0 +1,177 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.{Field, Value} + +import scala.annotation.implicitNotFound +import scala.util.{Failure, Success, Try} + +/** + * The ToValue trait, used for turning scala things into Value. + * + * Most of the time you will define this in your own field builder. + * For example to define `java.time.Instant` you could do this: + * + * {{{ + * trait InstantFieldBuilder extends FieldBuilder { + * import com.tersesystems.echopraxia.Value + * implicit val instantToStringValue: ToValue[Instant] = ToValue(instantValue) + * def instant(name: String, i: Instant): Field = keyValue(name -> instantValue(i)) + * def instant(tuple: (String, Instant)): Field = keyValue(tuple) + * private def instantValue(i: Instant): Value.StringValue = Value.string(i.toString) + * } + * }}} + * + * And then you can import the field builder context: + * + * {{{ + * logger.info("{}", fb => { + * import fb._ + * fb.onlyKeyValue("instant" -> Instant.now()) + * }) + * }}} + * + * If you are implementing a mapper for a case class, you may want + * to implement `ToObjectValue` as well: + * + * {{{ + * trait PersonFieldBuilder extends FieldBuilder { + * implicit val personToValue: ToValue[Person] = ToValue(personValue) + * implicit val personToObjectValue: ToObjectValue[Person] = ToObjectValue(personValue) + * def person(name: String, person: Person): Field = keyValue(name, personValue(person)) + * def onlyPerson(name: String, p: Person): util.List[Field] = this.only(person(name, p)) + * private def personValue(p: Person): Value.ObjectValue = Value.`object`( + * string("name", p.name), + * number("age", p.age) + * ) + * } + * }}} + * + * @tparam T the object type + */ +@implicitNotFound("Could not find an implicit ToValue[${T}]") +@FunctionalInterface +trait ToValue[-T] { + def toValue(t: T): Value[_] +} + +object ToValue { + def apply[T: ToValue]: ToValue[T] = implicitly[ToValue[T]] + + private def convert[T](f: T => Value[_]): ToValue[T] = (value: T) => f.apply(value) + + implicit val valueToValue: ToValue[Value[_]] = ToValue.convert(identity) + + implicit val stringToStringValue: ToValue[String] = ToValue.convert(Value.string) + + implicit def numberToValue[N <: Number]: ToValue[N] = ToValue.convert(Value.number(_)) + implicit val byteToValue: ToValue[Byte] = ToValue.convert(Value.number(_)) + implicit val shortToValue: ToValue[Short] = ToValue.convert(Value.number(_)) + implicit val intToValue: ToValue[Int] = ToValue.convert(Value.number(_)) + implicit val longToValue: ToValue[Long] = ToValue.convert(Value.number(_)) + implicit val doubleToValue: ToValue[Double] = ToValue.convert(Value.number(_)) + implicit val floatToValue: ToValue[Float] = ToValue.convert(Value.number(_)) + implicit val bigIntToValue: ToValue[BigInt] = ToValue.convert(Value.number(_)) + implicit val bigDecimalToValue: ToValue[BigDecimal] = ToValue.convert(Value.number(_)) + + implicit val booleanToBoolValue: ToValue[Boolean] = ToValue.convert(Value.bool(_)) + implicit val javaBoolToBoolValue: ToValue[java.lang.Boolean] = ToValue.convert(Value.bool) + + implicit def throwableToValue[T <: Throwable]: ToValue[T] = ToValue.convert(Value.exception) + + implicit def optionValue[V: ToValue]: ToValue[Option[V]] = { + case Some(v) => + implicitly[ToValue[V]].toValue(v) + case None => + Value.NullValue.instance + } + + implicit def tryValue[V: ToValue]: ToValue[Try[V]] = { + case Success(v) => + implicitly[ToValue[V]].toValue(v) + case Failure(e) => + ToValue[Throwable].toValue(e) + } + + implicit def eitherValue[A: ToValue, B: ToValue]: ToValue[Either[A, B]] = { + case Left(a) => + ToValue[A].toValue(a) + case Right(r) => + ToValue[B].toValue(r) + } + +} + +/** + * ToArrayValue is used when passing an ArrayValue to a field builder. + * + * {{{ + * val array: Array[Int] = Array(1, 2, 3) + * logger.info("{}", fb => fb.onlyArray("array", array) + * }}} + * + * @tparam T the array type. + */ +@implicitNotFound("Could not find an implicit ToArrayValue[${T}]") +trait ToArrayValue[-T] { + def toArrayValue(t: T): Value.ArrayValue +} + +object ToArrayValue { + + def apply[T: ToArrayValue]: ToArrayValue[T] = implicitly[ToArrayValue[T]] + + implicit val identityArrayValue: ToArrayValue[Value.ArrayValue] = identity(_) + + implicit def iterableToArrayValue[V: ToValue]: ToArrayValue[collection.Iterable[V]] = + (iterable: collection.Iterable[V]) => { + val iterable1 = iterable.map(ToValue[V].toValue) + Value.array(iterable1.toSeq: _*) + } + + implicit def arrayToArrayValue[V: ToValue]: ToArrayValue[Array[V]] = (array: Array[V]) => { + val array1 = array.map(ToValue[V].toValue) + Value.array(array1.toSeq: _*) + } +} + +/** + * ToObjectValue is used when providing an explicit `object` value to + * a field builder. Notable when you have a field or fields in a collection. + * + * {{{ + * val fields: Seq[Field] = ??? + * logger.info("{}", fb => fb.onlyObject("obj", fields) + * }}} + * + * @tparam T the object type + */ +@implicitNotFound("Could not find an implicit ToObjectValue[${T}]") +trait ToObjectValue[-T] { + def toObjectValue(t: T): Value.ObjectValue +} + +object ToObjectValue { + + def apply[T: ToObjectValue]: ToObjectValue[T] = implicitly[ToObjectValue[T]] + + implicit val fieldToObjectValue: ToObjectValue[Field] = { f => + Value.`object`(f) + } + + implicit val iterableToObjectValue: ToObjectValue[collection.Iterable[Field]] = { t => + Value.`object`(t.toSeq: _*) + } + + // Don't include mapToObjectValue by default: keyValue vs value should be a user choice, not + // done automagically by the framework. + // + // implicit def mapToObjectValue[V: ToValue]: ToObjectValue[Map[String, V]] = new ToObjectValue[Map[String, V]] { + // override def toObjectValue(t: Map[String, V]): Value.ObjectValue = { + // val fields: Seq[Field] = t.map { + // case (k, v) => + // Value.keyValue(k, v.toString) + // }.toSeq + // Value.`object`(fields.asJava) + // } + // } +} diff --git a/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Utilities.scala b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Utilities.scala new file mode 100644 index 00000000..4c34ba46 --- /dev/null +++ b/api/src/main/scala/com/tersesystems/echopraxia/scala/api/Utilities.scala @@ -0,0 +1,40 @@ +package com.tersesystems.echopraxia.scala.api + +import com.tersesystems.echopraxia.api.{Field, Value} + +import java.util +import java.util.function.{Function, Supplier} +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal + +object Utilities { + + def getNewInstance[T](newBuilderClass: Class[T]): T = { + try { + newBuilderClass.getDeclaredConstructor().newInstance() + } catch { + case NonFatal(e) => + throw new IllegalStateException(e) + } + } + + val getThreadContextFunction + : Function[Supplier[util.Map[String, String]], Supplier[util.List[Field]]] = + new Function[Supplier[util.Map[String, String]], Supplier[util.List[Field]]] { + override def apply( + mapSupplier: Supplier[util.Map[String, String]] + ): Supplier[util.List[Field]] = { + new Supplier[util.List[Field]]() { + def buildFields(contextMap: util.Map[String, String]): util.List[Field] = { + val list = new util.ArrayList[Field](); + for (entry <- contextMap.entrySet().iterator.asScala) { + list.add(Field.keyValue(entry.getKey, Value.string(entry.getValue))) + } + list + } + + override def get(): util.List[Field] = buildFields(mapSupplier.get) + } + } + } +} diff --git a/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLogger.scala b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLogger.scala new file mode 100644 index 00000000..a4492af2 --- /dev/null +++ b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLogger.scala @@ -0,0 +1,50 @@ +package com.tersesystems.echopraxia.scala.async + +import com.tersesystems.echopraxia.api.{CoreLogger, FieldBuilderResult} +import com.tersesystems.echopraxia.scala.api.{ + AbstractLoggerSupport, + Condition, + LoggerSupport, + Utilities +} + +import scala.compat.java8.FunctionConverters._ + +/** + * Async Logger with source code enabled. + */ +final class AsyncLogger[FB](core: CoreLogger, fieldBuilder: FB) + extends AbstractLoggerSupport[FB](core, fieldBuilder) + with LoggerSupport[FB] + with DefaultAsyncLoggerMethods[FB] { + + @inline + override def name: String = core.getName + + @inline + override def withCondition(scalaCondition: Condition): AsyncLogger[FB] = newLogger( + newCoreLogger = core.withCondition(scalaCondition.asJava) + ) + + @inline + override def withFields(f: FB => FieldBuilderResult): AsyncLogger[FB] = { + newLogger(newCoreLogger = core.withFields[FB](f.asJava, fieldBuilder)) + } + + @inline + override def withThreadContext: AsyncLogger[FB] = newLogger( + newCoreLogger = core.withThreadContext(Utilities.getThreadContextFunction) + ) + + @inline + override def withFieldBuilder[T](newBuilder: T): AsyncLogger[T] = + newLogger(newFieldBuilder = newBuilder) + + @inline + private def newLogger[T]( + newCoreLogger: CoreLogger = core, + newFieldBuilder: T = fieldBuilder + ): AsyncLogger[T] = + new AsyncLogger[T](newCoreLogger, newFieldBuilder) + +} diff --git a/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerFactory.scala b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerFactory.scala new file mode 100644 index 00000000..e68a835f --- /dev/null +++ b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerFactory.scala @@ -0,0 +1,29 @@ +package com.tersesystems.echopraxia.scala.async + +import com.tersesystems.echopraxia.api.{Caller, CoreLoggerFactory} +import com.tersesystems.echopraxia.scala.api.FieldBuilder + +/** + * Async Logger Factory with source code enabled. + */ +object AsyncLoggerFactory { + val FQCN: String = classOf[DefaultAsyncLoggerMethods[_]].getName + + val fieldBuilder: FieldBuilder = new FieldBuilder {} + + def getLogger(name: String): AsyncLogger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, name) + new AsyncLogger(core, fieldBuilder) + } + + def getLogger(clazz: Class[_]): AsyncLogger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, clazz.getName) + new AsyncLogger(core, fieldBuilder) + } + + def getLogger: AsyncLogger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, Caller.resolveClassName) + new AsyncLogger(core, fieldBuilder) + } + +} diff --git a/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerMethods.scala b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerMethods.scala new file mode 100644 index 00000000..5adb08e6 --- /dev/null +++ b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/AsyncLoggerMethods.scala @@ -0,0 +1,449 @@ +package com.tersesystems.echopraxia.scala.async + +import com.tersesystems.echopraxia.api.{FieldBuilderResult, LoggerHandle} +import com.tersesystems.echopraxia.scala.api.Condition + +/** + * Async Logger Methods with source code implicits. + */ +trait AsyncLoggerMethods[FB] { + + // ------------------------------------------------------------------------ + // TRACE + + /** + * Logs using a logger handle at TRACE level. + * + * @param consumer the consumer of the logger handle. + */ + def trace(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs using a condition and a logger handle at TRACE level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + def trace(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at TRACE level. + * + * @param message the given message. + */ + def trace(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at TRACE level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def trace(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at TRACE level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def trace(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level. + * + * @param condition the given condition. + * @param message the message. + */ + def trace(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def trace(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def trace(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + // ------------------------------------------------------------------------ + // DEBUG + + /** + * Logs using a logger handle at DEBUG level. + * + * @param consumer the consumer of the logger handle. + */ + def debug(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs using a condition and a logger handle at DEBUG level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + def debug(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at DEBUG level. + * + * @param message the given message. + */ + def debug(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at DEBUG level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def debug(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at DEBUG level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def debug(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level. + * + * @param condition the given condition. + * @param message the message. + */ + def debug(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def debug(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def debug(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + // ------------------------------------------------------------------------ + // INFO + + /** + * Logs using a logger handle at INFO level. + * + * @param consumer the consumer of the logger handle. + */ + // INFO + def info(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs using a condition and a logger handle at INFO level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + def info(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at INFO level. + * + * @param message the given message. + */ + def info(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at INFO level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def info(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at INFO level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def info(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level. + * + * @param condition the given condition. + * @param message the message. + */ + def info(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def info(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def info(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + // ------------------------------------------------------------------------ + // WARN + + /** + * Logs using a logger handle at WARN level. + * + * @param consumer the consumer of the logger handle. + */ + def warn(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs using a condition and a logger handle at WARN level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + def warn(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at WARN level. + * + * @param message the given message. + */ + def warn(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at WARN level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def warn(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at WARN level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def warn(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + // ------------------------------------------------------------------------ + // ERROR + + /** + * Logs using a logger handle at ERROR level. + * + * @param consumer the consumer of the logger handle. + */ + def error(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs using a condition and a logger handle at ERROR level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + def error(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit +} diff --git a/async/src/main/scala/com/tersesystems/echopraxia/scala/async/DefaultAsyncLoggerMethods.scala b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/DefaultAsyncLoggerMethods.scala new file mode 100644 index 00000000..c2e0bf4f --- /dev/null +++ b/async/src/main/scala/com/tersesystems/echopraxia/scala/async/DefaultAsyncLoggerMethods.scala @@ -0,0 +1,675 @@ +package com.tersesystems.echopraxia.scala.async + +import com.tersesystems.echopraxia.api.Level._ +import com.tersesystems.echopraxia.api.{Field, FieldBuilderResult, LoggerHandle, Value} +import com.tersesystems.echopraxia.scala.api.{Condition, DefaultMethodsSupport} +import sourcecode.{Enclosing, File, Line} + +import java.util.function.Consumer + +import scala.compat.java8.FunctionConverters._ + +/** + * Default Async Logger Methods with source code implicits. + */ +trait DefaultAsyncLoggerMethods[FB] extends AsyncLoggerMethods[FB] { + self: DefaultMethodsSupport[FB] => + + protected def sourceInfoFields( + fb: FB + )(implicit line: Line, file: File, enc: Enclosing): FieldBuilderResult = { + Field.keyValue( + "sourcecode", // XXX make this configurable + Value.`object`( + Field.keyValue("file", Value.string(file.value)), + Field.keyValue("line", Value.number(line.value)), + Field.keyValue("enclosing", Value.string(enc.value)) + ) + ) + } + + /** + * Logs using a logger handle at TRACE level. + * + * @param consumer the consumer of the logger handle. + */ + // ------------------------------------------------------------------------ + // TRACE + override def trace(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, consumer.asJava, fieldBuilder) + } + + /** + * Logs using a condition and a logger handle at TRACE level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + override def trace(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, c.asJava, consumer.asJava, fieldBuilder) + } + + /** + * Logs statement at TRACE level. + * + * @param message the given message. + */ + override def trace(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Logs statement at TRACE level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + override def trace(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, toConsumer(message, f), fieldBuilder) + } + + /** + * Logs statement at TRACE level with exception. + * + * @param message the message. + * @param e the given exception. + */ + override def trace(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + TRACE, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Conditionally logs statement at TRACE level. + * + * @param condition the given condition. + * @param message the message. + */ + override def trace(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, condition.asJava, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Conditionally logs statement at TRACE level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + override def trace(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(TRACE, condition.asJava, toConsumer(message, f), fieldBuilder) + } + + /** + * Conditionally logs statement at TRACE level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + override def trace(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + TRACE, + condition.asJava, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Logs using a logger handle at DEBUG level. + * + * @param consumer the consumer of the logger handle. + */ + // DEBUG + override def debug(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, consumer.asJava, fieldBuilder) + } + + /** + * Logs using a condition and a logger handle at DEBUG level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + override def debug(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, c.asJava, consumer.asJava, fieldBuilder) + } + + /** + * Logs statement at DEBUG level. + * + * @param message the given message. + */ + override def debug(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Logs statement at DEBUG level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + override def debug(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, toConsumer(message, f), fieldBuilder) + } + + /** + * Logs statement at DEBUG level with exception. + * + * @param message the message. + * @param e the given exception. + */ + override def debug(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + DEBUG, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Conditionally logs statement at DEBUG level. + * + * @param condition the given condition. + * @param message the message. + */ + override def debug(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, condition.asJava, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Conditionally logs statement at DEBUG level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + override def debug(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(DEBUG, condition.asJava, toConsumer(message, f), fieldBuilder) + } + + /** + * Conditionally logs statement at DEBUG level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + override def debug(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + DEBUG, + condition.asJava, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Logs using a logger handle at INFO level. + * + * @param consumer the consumer of the logger handle. + */ + // INFO + override def info(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, consumer.asJava, fieldBuilder) + } + + /** + * Logs using a condition and a logger handle at INFO level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + override def info(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, c.asJava, consumer.asJava, fieldBuilder) + } + + /** + * Logs statement at INFO level. + * + * @param message the given message. + */ + override def info(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Logs statement at INFO level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + override def info(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, toConsumer(message, f), fieldBuilder) + } + + /** + * Logs statement at INFO level with exception. + * + * @param message the message. + * @param e the given exception. + */ + override def info(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + INFO, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Conditionally logs statement at INFO level. + * + * @param condition the given condition. + * @param message the message. + */ + override def info(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, condition.asJava, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Conditionally logs statement at INFO level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + override def info(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(INFO, condition.asJava, toConsumer(message, f), fieldBuilder) + } + + /** + * Conditionally logs statement at INFO level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + override def info(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + INFO, + condition.asJava, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Logs using a logger handle at WARN level. + * + * @param consumer the consumer of the logger handle. + */ + // WARN + override def warn(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, consumer.asJava, fieldBuilder) + } + + /** + * Logs using a condition and a logger handle at WARN level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + override def warn(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, c.asJava, consumer.asJava, fieldBuilder) + } + + /** + * Logs statement at WARN level. + * + * @param message the given message. + */ + override def warn(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + /** + * Logs statement at WARN level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + override def warn(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, toConsumer(message, f), fieldBuilder) + } + + /** + * Logs statement at WARN level with exception. + * + * @param message the message. + * @param e the given exception. + */ + override def warn(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + WARN, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + override def warn(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, condition.asJava, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + override def warn(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(WARN, condition.asJava, toConsumer(message, f), fieldBuilder) + } + + override def warn(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + WARN, + condition.asJava, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + /** + * Logs using a logger handle at ERROR level. + * + * @param consumer the consumer of the logger handle. + */ + // ERROR + override def error(consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, consumer.asJava, fieldBuilder) + } + + /** + * Logs using a condition and a logger handle at ERROR level. + * + * @param c the condition + * @param consumer the consumer of the logger handle. + */ + override def error(c: Condition, consumer: LoggerHandle[FB] => Unit)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, c.asJava, consumer.asJava, fieldBuilder) + } + + override def error(message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + override def error(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, toConsumer(message, f), fieldBuilder) + } + + override def error(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + ERROR, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + override def error(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, condition.asJava, (h: LoggerHandle[FB]) => h.log(message), fieldBuilder) + } + + override def error(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog(ERROR, condition.asJava, toConsumer(message, f), fieldBuilder) + } + + override def error(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .asyncLog( + ERROR, + condition.asJava, + (h: LoggerHandle[FB]) => h.log(message, (fb: FB) => onlyException(e)), + fieldBuilder + ) + } + + @inline + private def toConsumer( + message: String, + f: FB => FieldBuilderResult + ): Consumer[LoggerHandle[FB]] = h => h.log(message, f.asJava) + + private def onlyException(e: Throwable): FieldBuilderResult = { + Field.keyValue(Field.EXCEPTION, Value.exception(e)) + } + +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 00000000..069023a5 --- /dev/null +++ b/build.sbt @@ -0,0 +1,127 @@ +import sbt.Keys._ + +val echopraxiaVersion = "2.0.0-SNAPSHOT" + +val scala213 = "2.13.8" +val scala212 = "2.12.14" +val scalaVersions = Seq(scala212, scala213) + +initialize := { + val _ = initialize.value // run the previous initialization + val required = "11" + val current = sys.props("java.specification.version") + assert(current >= required, s"Unsupported JDK: java.specification.version $current != $required") +} + +ThisBuild / organization := "com.tersesystems.echopraxia-scala" +ThisBuild / homepage := Some(url("https://github.com/tersesystems/echopraxia-scala")) + +ThisBuild / startYear := Some(2021) +ThisBuild / licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")) + +ThisBuild / scmInfo := Some( + ScmInfo( + url("https://github.com/tersesystems/echopraxia-scala"), + "scm:git@github.com:tersesystems/echopraxia-scala.git" + ) +) + +ThisBuild / versionScheme := Some("early-semver") + +ThisBuild / resolvers += Resolver.mavenLocal +ThisBuild / scalaVersion := scala212 +ThisBuild / crossScalaVersions := scalaVersions +ThisBuild / scalacOptions := scalacOptionsVersion(scalaVersion.value) + +ThisBuild / Test / parallelExecution := false +Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) + +lazy val api = (project in file("api")) + .settings( + name := "api", + // + libraryDependencies += "com.tersesystems.echopraxia" % "api" % echopraxiaVersion, + libraryDependencies += "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2", + libraryDependencies += "org.scala-lang.modules" %% "scala-collection-compat" % "2.7.0", + libraryDependencies += "com.daodecode" %% "scalaj-collection" % "0.3.1" + ) + +lazy val logger = (project in file("logger")) + .settings( + name := "logger", + // + libraryDependencies += "com.lihaoyi" %% "sourcecode" % "0.2.8", + // + libraryDependencies += "com.tersesystems.echopraxia" % "logstash" % echopraxiaVersion % Test, + libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.11" % Test + ) + .dependsOn(api % "compile->compile;test->compile") + +lazy val asyncLogger = (project in file("async")) + .settings( + name := "async-logger", + // + libraryDependencies += "com.lihaoyi" %% "sourcecode" % "0.2.8", + // + libraryDependencies += "com.tersesystems.echopraxia" % "logstash" % echopraxiaVersion % Test, + libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.11" % Test + ) + .dependsOn(api % "compile->compile;test->compile") + +lazy val root = (project in file(".")) + .settings( + Compile / doc / sources := Seq.empty, + Compile / packageDoc / publishArtifact := false, + publishArtifact := false, + publish / skip := true + ) + .aggregate(api, logger, asyncLogger) + +def scalacOptionsVersion(scalaVersion: String): Seq[String] = { + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, n)) if n >= 13 => + Seq( + "-unchecked", + "-deprecation", + "-feature", + "-encoding", + "UTF-8", + "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", + "-language:postfixOps", + "-Xlint", + "-Ywarn-dead-code", + "-Yrangepos", + "-Xsource:2.13", + "-release", + "8" + ) ++ optimizeInline + case Some((2, n)) if n == 12 => + Seq( + "-unchecked", + "-deprecation", + "-feature", + "-encoding", + "UTF-8", + "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", + "-language:postfixOps", + "-Xlint", + "-Ywarn-dead-code", + "-Yrangepos", + "-Xsource:2.12", + "-Yno-adapted-args", + "-release", + "8" + ) ++ optimizeInline + + } +} + +val optimizeInline = Seq( + "-opt:l:inline", + "-opt-inline-from:com.tersesystems.echopraxia.**", + "-opt-warnings:any-inline-failed" +) diff --git a/logger/src/main/scala/com/tersesystems/echopraxia/scala/DefaultLoggerMethods.scala b/logger/src/main/scala/com/tersesystems/echopraxia/scala/DefaultLoggerMethods.scala new file mode 100644 index 00000000..c70ec7ce --- /dev/null +++ b/logger/src/main/scala/com/tersesystems/echopraxia/scala/DefaultLoggerMethods.scala @@ -0,0 +1,493 @@ +package com.tersesystems.echopraxia.scala + +import com.tersesystems.echopraxia.api.Level._ +import com.tersesystems.echopraxia.api.{Field, FieldBuilderResult, Value} +import com.tersesystems.echopraxia.scala.api.{Condition, DefaultMethodsSupport} +import sourcecode.{Enclosing, File, Line} + +import scala.compat.java8.FunctionConverters._ + +/** + * Default Logger methods with source code implicits. + * + * This implementation uses the protected `sourceInfoFields` method to add + * source code information as context fields, adding a `sourcecode` object + * containing `line`, `file`, and `enclosing` fields. + * + * You can subclass this method and override `sourceInfoFields` to provide + * your own implementation. + */ +trait DefaultLoggerMethods[FB] extends LoggerMethods[FB] { + this: DefaultMethodsSupport[FB] => + + protected def sourceInfoFields( + fb: FB + )(implicit line: Line, file: File, enc: Enclosing): FieldBuilderResult = { + Field.keyValue( + "sourcecode", // XXX make this configurable + Value.`object`( + Field.keyValue("file", Value.string(file.value)), + Field.keyValue("line", Value.number(line.value)), + Field.keyValue("enclosing", Value.string(enc.value)) + ) + ) + } + + /** @return true if the logger level is TRACE or higher. */ + def isTraceEnabled: Boolean = core.isEnabled(TRACE) + + /** + * @param condition the given condition. + * @return true if the logger level is TRACE or higher and the condition is met. + */ + def isTraceEnabled(condition: Condition): Boolean = { + core.isEnabled(TRACE, condition.asJava) + } + + /** + * Logs statement at TRACE level. + * + * @param message the given message. + */ + def trace( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit = { + core.withFields(sourceInfoFields, fieldBuilder).log(TRACE, message) + } + + /** + * Logs statement at TRACE level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def trace(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(TRACE, message, f.asJava, fieldBuilder) + } + + /** + * Logs statement at TRACE level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def trace(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(TRACE, message, (fb: FB) => onlyException(e), fieldBuilder) + } + + /** + * Conditionally logs statement at TRACE level. + * + * @param condition the given condition. + * @param message the message. + */ + def trace(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(TRACE, condition.asJava, message) + } + + /** + * Conditionally logs statement at TRACE level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def trace(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(TRACE, condition.asJava, message, f.asJava, fieldBuilder) + } + + /** + * Conditionally logs statement at TRACE level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def trace(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(TRACE, condition.asJava, message, (fb: FB) => onlyException(e), fieldBuilder) + } + + /** @return true if the logger level is DEBUG or higher. */ + def isDebugEnabled: Boolean = core.isEnabled(DEBUG) + + /** + * @param condition the given condition. + * @return true if the logger level is DEBUG or higher and the condition is met. + */ + def isDebugEnabled(condition: Condition): Boolean = core.isEnabled(DEBUG, condition.asJava) + + /** + * Logs statement at DEBUG level. + * + * @param message the given message. + */ + def debug( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit = { + core.log(DEBUG, message) + } + + /** + * Logs statement at DEBUG level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def debug(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(DEBUG, message, f.asJava, fieldBuilder) + } + + /** + * Logs statement at DEBUG level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def debug(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(DEBUG, message, (fb: FB) => onlyException(e), fieldBuilder) + } + + /** + * Conditionally logs statement at DEBUG level. + * + * @param condition the given condition. + * @param message the message. + */ + def debug(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(DEBUG, condition.asJava, message) + } + + /** + * Conditionally logs statement at DEBUG level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def debug(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(DEBUG, condition.asJava, message, (fb: FB) => onlyException(e), fieldBuilder) + } + + /** + * Conditionally logs statement at DEBUG level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def debug(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(DEBUG, condition.asJava, message, f.asJava, fieldBuilder) + } + + /** @return true if the logger level is INFO or higher. */ + def isInfoEnabled: Boolean = core.isEnabled(INFO) + + /** + * @param condition the given condition. + * @return true if the logger level is INFO or higher and the condition is met. + */ + def isInfoEnabled(condition: Condition): Boolean = core.isEnabled(INFO, condition.asJava) + + /** + * Logs statement at INFO level. + * + * @param message the given message. + */ + def info( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit = { + core.withFields(sourceInfoFields, fieldBuilder).log(INFO, message) + } + + /** + * Logs statement at INFO level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def info(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(INFO, message, f.asJava, fieldBuilder) + } + + /** + * Logs statement at INFO level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def info(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(INFO, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + /** + * Conditionally logs statement at INFO level. + * + * @param condition the given condition. + * @param message the message. + */ + def info(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(INFO, condition.asJava, message) + } + + /** + * Conditionally logs statement at INFO level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def info(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(INFO, condition.asJava, message, f.asJava, fieldBuilder) + } + + /** + * Conditionally logs statement at INFO level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def info(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(INFO, condition.asJava, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + /** @return true if the logger level is WARN or higher. */ + def isWarnEnabled: Boolean = core.isEnabled(WARN) + + /** + * @param condition the given condition. + * @return true if the logger level is WARN or higher and the condition is met. + */ + def isWarnEnabled(condition: Condition): Boolean = core.isEnabled(WARN, condition.asJava) + + /** + * Logs statement at WARN level. + * + * @param message the given message. + */ + def warn( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit = { + core.withFields(sourceInfoFields, fieldBuilder).log(WARN, message) + } + + /** + * Logs statement at WARN level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def warn(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(WARN, message, f.asJava, fieldBuilder) + } + + /** + * Logs statement at WARN level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def warn(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(WARN, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + def warn(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(WARN, condition.asJava, message) + } + + def warn(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(WARN, condition.asJava, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + def warn(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core.log(WARN, condition.asJava, message, f.asJava, fieldBuilder) + } + + /** @return true if the logger level is ERROR or higher. */ + def isErrorEnabled: Boolean = core.isEnabled(ERROR) + + /** + * @param condition the given condition. + * @return true if the logger level is ERROR or higher and the condition is met. + */ + def isErrorEnabled(condition: Condition): Boolean = core.isEnabled(ERROR, condition.asJava) + + def error( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit = { + core.withFields(sourceInfoFields, fieldBuilder).log(ERROR, message) + } + + def error(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(ERROR, message, f.asJava, fieldBuilder) + } + + def error(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(ERROR, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + def error(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(ERROR, condition.asJava, message) + } + + def error(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(ERROR, condition.asJava, message, f.asJava, fieldBuilder) + } + + def error(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit = { + core + .withFields(sourceInfoFields, fieldBuilder) + .log(ERROR, condition.asJava, message, ((fb: FB) => onlyException(e)), fieldBuilder) + } + + private def onlyException(e: Throwable): FieldBuilderResult = { + (Field.keyValue(Field.EXCEPTION, Value.exception(e))) + } + +} diff --git a/logger/src/main/scala/com/tersesystems/echopraxia/scala/Logger.scala b/logger/src/main/scala/com/tersesystems/echopraxia/scala/Logger.scala new file mode 100644 index 00000000..5620f829 --- /dev/null +++ b/logger/src/main/scala/com/tersesystems/echopraxia/scala/Logger.scala @@ -0,0 +1,53 @@ +package com.tersesystems.echopraxia.scala + +import com.tersesystems.echopraxia.api.{CoreLogger, FieldBuilderResult} +import com.tersesystems.echopraxia.scala.api.{ + AbstractLoggerSupport, + Condition, + LoggerSupport, + Utilities +} + +import scala.compat.java8.FunctionConverters.enrichAsJavaFunction + +/** + * Logger with source code implicit parameters. + */ +final class Logger[FB](core: CoreLogger, fieldBuilder: FB) + extends AbstractLoggerSupport(core, fieldBuilder) + with LoggerSupport[FB] + with DefaultLoggerMethods[FB] { + + @inline + override def name: String = core.getName + + @inline + override def withCondition(condition: Condition): Logger[FB] = { + newLogger(newCoreLogger = core.withCondition(condition.asJava)) + } + + @inline + override def withFields(f: FB => FieldBuilderResult): Logger[FB] = { + newLogger(newCoreLogger = core.withFields(f.asJava, fieldBuilder)) + } + + @inline + override def withThreadContext: Logger[FB] = { + newLogger( + newCoreLogger = core.withThreadContext(Utilities.getThreadContextFunction) + ) + } + + @inline + override def withFieldBuilder[NEWFB](newFieldBuilder: NEWFB): Logger[NEWFB] = { + newLogger(newFieldBuilder = newFieldBuilder) + } + + @inline + private def newLogger[T]( + newCoreLogger: CoreLogger = core, + newFieldBuilder: T = fieldBuilder + ): Logger[T] = + new Logger[T](newCoreLogger, newFieldBuilder) + +} diff --git a/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerFactory.scala b/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerFactory.scala new file mode 100644 index 00000000..5e288269 --- /dev/null +++ b/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerFactory.scala @@ -0,0 +1,29 @@ +package com.tersesystems.echopraxia.scala + +import com.tersesystems.echopraxia.api.{Caller, CoreLoggerFactory} +import com.tersesystems.echopraxia.scala.api.FieldBuilder + +/** + * LoggerFactory for a logger with source code enabled. + */ +object LoggerFactory { + val FQCN: String = classOf[DefaultLoggerMethods[_]].getName + + val fieldBuilder: FieldBuilder = new FieldBuilder {} + + def getLogger(name: String): Logger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, name) + new Logger(core, fieldBuilder) + } + + def getLogger(clazz: Class[_]): Logger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, clazz.getName) + new Logger(core, fieldBuilder) + } + + def getLogger: Logger[FieldBuilder] = { + val core = CoreLoggerFactory.getLogger(FQCN, Caller.resolveClassName) + new Logger(core, fieldBuilder) + } + +} diff --git a/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerMethods.scala b/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerMethods.scala new file mode 100644 index 00000000..2ad70e01 --- /dev/null +++ b/logger/src/main/scala/com/tersesystems/echopraxia/scala/LoggerMethods.scala @@ -0,0 +1,353 @@ +package com.tersesystems.echopraxia.scala + +import com.tersesystems.echopraxia.api.FieldBuilderResult +import com.tersesystems.echopraxia.scala.api.Condition + +/** + * Logger methods with source code implicits + */ +trait LoggerMethods[FB] { + + /** @return true if the logger level is TRACE or higher. */ + def isTraceEnabled: Boolean + + /** + * @param condition the given condition. + * @return true if the logger level is TRACE or higher and the condition is met. + */ + def isTraceEnabled(condition: Condition): Boolean + + /** + * Logs statement at TRACE level. + * + * @param message the given message. + */ + def trace( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit + + /** + * Logs statement at TRACE level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def trace(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at TRACE level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def trace(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level. + * + * @param condition the given condition. + * @param message the message. + */ + def trace(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def trace(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at TRACE level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def trace(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** @return true if the logger level is DEBUG or higher. */ + def isDebugEnabled: Boolean + + /** + * @param condition the given condition. + * @return true if the logger level is DEBUG or higher and the condition is met. + */ + def isDebugEnabled(condition: Condition): Boolean + + /** + * Logs statement at DEBUG level. + * + * @param message the given message. + */ + def debug( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit + + /** + * Logs statement at DEBUG level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def debug(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at DEBUG level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def debug(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level. + * + * @param condition the given condition. + * @param message the message. + */ + def debug(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def debug(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at DEBUG level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def debug(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** @return true if the logger level is INFO or higher. */ + def isInfoEnabled: Boolean + + /** + * @param condition the given condition. + * @return true if the logger level is INFO or higher and the condition is met. + */ + def isInfoEnabled(condition: Condition): Boolean + + /** + * Logs statement at INFO level. + * + * @param message the given message. + */ + def info( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit + + /** + * Logs statement at INFO level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def info(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at INFO level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def info(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level. + * + * @param condition the given condition. + * @param message the message. + */ + def info(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level using a field builder function. + * + * @param condition the given condition. + * @param message the message. + * @param f the field builder function. + */ + def info(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Conditionally logs statement at INFO level with exception. + * + * @param condition the given condition. + * @param message the message. + * @param e the given exception. + */ + def info(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** @return true if the logger level is WARN or higher. */ + def isWarnEnabled: Boolean + + /** + * @param condition the given condition. + * @return true if the logger level is WARN or higher and the condition is met. + */ + def isWarnEnabled(condition: Condition): Boolean + + /** + * Logs statement at WARN level. + * + * @param message the given message. + */ + def warn( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit + + /** + * Logs statement at WARN level using a field builder function. + * + * @param message the message. + * @param f the field builder function. + */ + def warn(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** + * Logs statement at WARN level with exception. + * + * @param message the message. + * @param e the given exception. + */ + def warn(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def warn(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + /** @return true if the logger level is ERROR or higher. */ + def isErrorEnabled: Boolean + + /** + * @param condition the given condition. + * @return true if the logger level is ERROR or higher and the condition is met. + */ + def isErrorEnabled(condition: Condition): Boolean + + def error( + message: String + )(implicit line: sourcecode.Line, file: sourcecode.File, enc: sourcecode.Enclosing): Unit + + def error(message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String, f: FB => FieldBuilderResult)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit + + def error(condition: Condition, message: String, e: Throwable)(implicit + line: sourcecode.Line, + file: sourcecode.File, + enc: sourcecode.Enclosing + ): Unit +} diff --git a/logger/src/test/resources/logback-test.xml b/logger/src/test/resources/logback-test.xml new file mode 100644 index 00000000..06a93f1f --- /dev/null +++ b/logger/src/test/resources/logback-test.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/logger/src/test/scala/com/tersesystems/echopraxia/scala/ConditionSpec.scala b/logger/src/test/scala/com/tersesystems/echopraxia/scala/ConditionSpec.scala new file mode 100644 index 00000000..d9cc4577 --- /dev/null +++ b/logger/src/test/scala/com/tersesystems/echopraxia/scala/ConditionSpec.scala @@ -0,0 +1,255 @@ +package com.tersesystems.echopraxia.scala + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.tersesystems.echopraxia.scala.api.{Condition, Level} +import org.scalatest.BeforeAndAfterEach +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +import java.util + +class ConditionSpec extends AnyFunSpec with BeforeAndAfterEach with Matchers { + + private val logger = LoggerFactory.getLogger(getClass) + + describe("findBoolean") { + + it("should match") { + val condition: Condition = (_, ctx) => ctx.findBoolean("$.foo").getOrElse(false) + logger.debug(condition, "found a foo == true", _.bool("foo", true)) + + matchThis("found a foo == true") + } + + it("should return None if no match") { + val condition: Condition = (_, ctx) => ctx.findBoolean("$.foo").getOrElse(false) + logger.debug(condition, "found a foo == true") + + noMatch + } + + it("should return None if match is not boolean") { + val condition: Condition = (_, ctx) => ctx.findBoolean("$.foo").getOrElse(false) + logger.debug(condition, "found a foo == true", _.string("foo", "bar")) + + noMatch + } + } // findBoolean + + describe("findString") { + + it("should return some on match") { + val condition: Condition = (_, ctx) => ctx.findString("$.foo").contains("bar") + logger.debug(condition, "found a foo == bar", _.string("foo", "bar")) + + matchThis("found a foo == bar") + } + + it("should none on no match") { + val condition: Condition = (_, ctx) => ctx.findString("$.foo").contains("bar") + logger.debug(condition, "found a foo == bar") + + noMatch + } + + it("should none on wrong type") { + val condition: Condition = (_, ctx) => ctx.findString("$.foo").contains("bar") + logger.debug(condition, "found a foo == bar", _.number("foo", 1)) + + noMatch + } + } // findString + + describe("findNumber") { + it("should some on match") { + val condition: Condition = (_, ctx) => ctx.findNumber("$.foo").exists(_.intValue() == 1) + logger.debug(condition, "found a number == 1", _.number("foo", 1)) + + matchThis("found a number == 1") + } + } // findNumber + + describe("findNull") { + it("should work with findNull") { + val condition: Condition = (_, ctx) => ctx.findNull("$.foo") + logger.debug(condition, "found a null!", _.nullField("foo")) + + matchThis("found a null!") + } + } + + describe("findList") { + it("should work with list of same type") { + val condition: Condition = (_, ctx) => { + val list = ctx.findList("$.foo") + val result = list.contains("derp") + result + } + logger.debug( + condition, + "found a list with derp in it!", + _.array("foo", Array("derp")) + ) + + matchThis("found a list with derp in it!") + } + + it("should work with list with different type values") { + val condition: Condition = (_, ctx) => { + val nummatch = ctx.findList("$.foo").contains(1) + val strmatch = ctx.findList("$.foo").contains("derp") + nummatch && strmatch + } + logger.debug( + condition, + "found a list with 1 in it!", + fb => { + import com.tersesystems.echopraxia.api.Value._ + fb.array("foo", array(string("derp"), number(1), bool(false))) + } + ) + + matchThis("found a list with 1 in it!") + } + + it("should match on list containing objects") { + val condition: Condition = (_, ctx) => { + val obj = ctx.findList("$.array") + obj.head match { + case map: Map[String, Any] => + map.get("a").contains(1) && map.get("c").contains(false) + case _ => + false + } + } + logger.debug( + condition, + "complex object", + fb => { + import com.tersesystems.echopraxia.api.Value._ + val objectValue: ObjectValue = `object`( + fb.value("a" -> 1), + fb.keyValue("b" -> "two"), + fb.value("c" -> false) + ) + fb.array("array", Seq(objectValue)) + } + ) + } + } + + describe("object") { + it("should match on simple object") { + logger + .withCondition((_, ctx) => ctx.findObject("$.foo").get("key").equals("value")) + .debug("simple map", fb => fb.obj("foo", fb.keyValue("key" -> "value"))) + + matchThis("simple map") + } + + it("should not match on no argument") { + val condition: Condition = (_, ctx) => { + ctx.findObject("$.foo").isDefined + } + logger.debug(condition, "no match", _.number("bar", 1)) + + noMatch + } + + it("should not match on incorrect type") { + val condition: Condition = (_, ctx) => { + ctx.findObject("$.foo").isDefined + } + logger.debug(condition, "no match", _.number("foo", 1)) + + noMatch + } + + it("should match on a complex object") { + val condition: Condition = (_, ctx) => { + val obj = ctx.findObject("$.foo") + val value1: Any = obj.get("a") + value1 == (1) + } + logger.debug( + condition, + "complex object", + fb => + fb.obj( + "foo" -> Seq( + fb.value("a" -> 1), + fb.keyValue("b" -> "two"), + fb.value("c" -> false) + ) + ) + ) + + matchThis("complex object") + } + } // object + + it("should match on list") { + val condition: Condition = (_, ctx) => { + val opt: Seq[Any] = ctx.findList("$.foo") + opt.nonEmpty + } + logger.debug(condition, "match list", fb => fb.array("foo" -> Seq(1, 2, 3))) + + matchThis("match list") + } + it("should not match on an object mismatch") { + val condition: Condition = (_, ctx) => { + val opt: Option[Map[String, Any]] = ctx.findObject("$.foo") + opt.isDefined + } + logger.debug(condition, "no match", fb => fb.keyValue("foo" -> true)) + + noMatch + } + + it("should match on level") { + val condition: Condition = (level, _) => level.isGreater(Level.DEBUG) + + logger.info(condition, "matches on level") + matchThis("matches on level") + } + + describe("throwable") { + + it("should match a subclass of throwable") { + val t = new Exception() + logger.info("matches on throwable {}", fb => fb.keyValue("derp" -> t)) + matchThis("matches on throwable {}") + } + } + + private def noMatch = { + val listAppender: ListAppender[ILoggingEvent] = getListAppender + val list: util.List[ILoggingEvent] = listAppender.list + list must be(empty) + } + + private def matchThis(message: String) = { + val listAppender: ListAppender[ILoggingEvent] = getListAppender + val list: util.List[ILoggingEvent] = listAppender.list + val event: ILoggingEvent = list.get(0) + event.getFormattedMessage must be(message) + } + + override def beforeEach(): Unit = { + getListAppender.list.clear() + } + + private def loggerContext: LoggerContext = { + org.slf4j.LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + } + + private def getListAppender: ListAppender[ILoggingEvent] = { + loggerContext + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) + .getAppender("LIST") + .asInstanceOf[ListAppender[ILoggingEvent]] + } +} diff --git a/logger/src/test/scala/com/tersesystems/echopraxia/scala/ScalaLoggerSpec.scala b/logger/src/test/scala/com/tersesystems/echopraxia/scala/ScalaLoggerSpec.scala new file mode 100644 index 00000000..9fc350e6 --- /dev/null +++ b/logger/src/test/scala/com/tersesystems/echopraxia/scala/ScalaLoggerSpec.scala @@ -0,0 +1,293 @@ +package com.tersesystems.echopraxia.scala + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.tersesystems.echopraxia.api.{Field, Value} +import com.tersesystems.echopraxia.scala.api._ +import org.scalatest.BeforeAndAfterEach +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +import java.time.Instant +import java.util +import scala.util.{Success, Try} + +class ScalaLoggerSpec extends AnyFunSpec with BeforeAndAfterEach with Matchers { + + private val logger = LoggerFactory.getLogger(getClass).withFieldBuilder(new MyFieldBuilder) + + describe("withCondition") { + + it("should use a scala withCondition") { + val condition: Condition = (level: Level, context: LoggingContext) => true + logger.withCondition(condition) + } + } + + describe("withFields") { + it("should use withFields") { + logger.withFields(fb => fb.string("derp", "herp")) + } + } + + describe("results") { + + it("should log a seq automatically") { + val seq = Array("one", "two", "three") + logger.debug( + "single tuple {}", + fb => fb.list(seq.zipWithIndex.map { case (value, i) => fb.string(i.toString, value) }) + ) + matchThis("single tuple {}") + } + + } + + describe("tuple") { + + it("should log using a single tuple using keyValue") { + logger.debug("single tuple {}", _.keyValue("foo" -> "bar")) + matchThis("single tuple {}") + } + + it("should log using a single tuple using onlyValue") { + logger.debug("single tuple {}", _.value("foo" -> "bar")) + matchThis("single tuple {}") + } + + it("should log using multiple tuples using an import") { + logger.debug( + "multiple tuples {}", + fb => { + import fb._ + fb.list(keyValue("foo" -> "bar"), keyValue("k2" -> "v2")) + } + ) + + matchThis("multiple tuples {}") + } + } + + describe("seq") { + + it("should log using a Seq of String") { + val seq = Seq("one", "two", "three") + logger.debug("seq {}", _.array("someSeq", seq)) + + matchThis("seq {}") + } + + it("should log using a Seq of Instant") { + logger.debug( + "seq {}", + { fb => + import fb._ + val seq: List[Instant] = List(Instant.now(), Instant.now(), Instant.now()) + (fb.array("someSeq", seq)) + } + ) + + matchThis("seq {}") + } + + it("should log using a Seq of Instant in tuple style") { + logger.debug( + "seq {}", + { fb => + import fb._ + val seq: List[Instant] = List(Instant.now(), Instant.now(), Instant.now()) + (fb.array("someSeq" -> seq)) + } + ) + + matchThis("seq {}") + } + + it("should log using a Seq of boolean") { + logger.debug( + "seq {}", + { fb => + import fb._ + array("someSeq", Seq(true, false, true)) + } + ) + + matchThis("seq {}") + } + } + + describe("more complex ToValue") { + + it("should log a Try") { + logger.debug("try {}", fb => (fb.keyValue("result", Try(true)))) + matchThis("try {}") + } + + it("should log a Success") { + logger.debug("success {}", fb => (fb.keyValue("result", Success(true)))) + matchThis("success {}") + } + + it("should log an option try") { + logger.debug("option try {}", fb => (fb.keyValue("result", Option(Try(true))))) + matchThis("option try {}") + } + + it("should log an Either") { + val either: Either[Int, String] = Either.cond(System.currentTimeMillis() > 1, "foo", 1) + logger.debug("either {}", _.keyValue("result" -> either)) + matchThis("either {}") + } + + } + + describe("instant and person") { + + it("should log an instant as a string") { + logger.debug( + "mapping time = {}", + fb => { + import fb._ + (keyValue("iso_timestamp" -> Instant.now())) + } + ) + matchThis("mapping time = {}") + } + + it("should log a person as a value or object value") { + logger.debug( + "person1 {} person2 {}", + fb => { + import fb._ + fb.list( + fb.keyValue("person1" -> Person("Eloise", 1)), + fb.obj("person2" -> Person("Eloise", 1)) + ) + } + ) + matchThis("person1 {} person2 {}") + } + + it("should work with a map with different values") { + logger.info( + "testing {}", + fb => { + import fb._ + val any = Map("int" -> 1, "str" -> "foo", "instant" -> Instant.now()) + val fields = any.map { + case (k: String, v: String) => + string(k, v) + case (k: String, v: Int) => + number(k, v) + case (k: String, v: Instant) => + instant(k, v) + } + obj("foo", fields) + } + ) + } + + it("should log a person as an object") { + logger.debug( + "person = {}", + fb => { + import fb._ + obj("owner", keyValue("person" -> Person("Eloise", 1))) + } + ) + + matchThis("person = {}") + } + + it("should custom with tuples") { + logger.debug( + "list of tuples = {}", + fb => { + import fb._ + list( + keyValue("owner" -> Person("Eloise", 1)), + keyValue("iso_timestamp" -> Instant.now()), + keyValue("foo" -> "bar"), + keyValue("something" -> true) + ) + } + ) + + matchThis("list of tuples = {}") + } + + it("should handle Option[Foo] as Null") { + val c: Condition = (_: Level, ctx: LoggingContext) => ctx.findNull("$.foo") + + logger.debug( + c, + "option[foo] = {}", + fb => { + import fb._ + val optPerson: Option[Person] = None + (fb.value("foo", optPerson)) + } + ) + matchThis("option[foo] = {}") + } + + it("should handle Option[Foo] as not null") { + val c: Condition = (_: Level, ctx: LoggingContext) => ctx.findObject("$.foo").isDefined + + logger.debug( + c, + "option[foo] = {}", + fb => { + import fb._ + val optPerson: Option[Person] = Some(Person("eloise", 1)) + (fb.value("foo", optPerson)) + } + ) + matchThis("option[foo] = {}") + } + } + + class MyFieldBuilder extends FieldBuilder { + + // Instant type + implicit val instantToStringValue: ToValue[Instant] = ToValue(instantValue) + def instant(name: String, i: Instant): Field = keyValue(name, instantValue(i)) + private def instantValue(i: Instant) = Value.string(i.toString) + + // Person type + implicit val personToValue: ToValue[Person] = ToValue(personValue) + implicit val personToObjectValue: ToObjectValue[Person] = ToObjectValue(personValue(_)) + + def person(name: String, person: Person): Field = keyValue(name, personValue(person)) + private def personValue(p: Person): Value.ObjectValue = Value.`object`( + string("name", p.name), + number("age", p.age) + ) + + } + + private def matchThis(message: String) = { + val listAppender: ListAppender[ILoggingEvent] = getListAppender + val list: util.List[ILoggingEvent] = listAppender.list + val event: ILoggingEvent = list.get(0) + event.getMessage must be(message) + } + + override def beforeEach(): Unit = { + getListAppender.list.clear() + } + + private def loggerContext: LoggerContext = { + org.slf4j.LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + } + + private def getListAppender: ListAppender[ILoggingEvent] = { + loggerContext + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) + .getAppender("LIST") + .asInstanceOf[ListAppender[ILoggingEvent]] + } +} + +case class Person(name: String, age: Int) diff --git a/logger/src/test/scala/com/tersesystems/echopraxia/scala/SourceLoggerSpec.scala b/logger/src/test/scala/com/tersesystems/echopraxia/scala/SourceLoggerSpec.scala new file mode 100644 index 00000000..7b4c89ff --- /dev/null +++ b/logger/src/test/scala/com/tersesystems/echopraxia/scala/SourceLoggerSpec.scala @@ -0,0 +1,53 @@ +package com.tersesystems.echopraxia.scala + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.tersesystems.echopraxia.scala.api.{Condition, Level, LoggingContext} +import org.scalatest.BeforeAndAfterEach +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +import java.util + +class SourceLoggerSpec extends AnyFunSpec with BeforeAndAfterEach with Matchers { + + private val logger = LoggerFactory.getLogger + + describe("source code") { + it("should return source code info") { + val condition: Condition = (level: Level, context: LoggingContext) => { + context.findString("$.sourcecode.file") match { + case Some(file) if file.endsWith("LoggerSpec.scala") => + true + case _ => + false + } + } + logger.info(condition, "logs if has sourcecode.file") + matchThis("logs if has sourcecode.file") + } + } + + private def matchThis(message: String) = { + val listAppender: ListAppender[ILoggingEvent] = getListAppender + val list: util.List[ILoggingEvent] = listAppender.list + val event: ILoggingEvent = list.get(0) + event.getMessage must be(message) + } + + private def loggerContext: LoggerContext = { + org.slf4j.LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + } + + override def beforeEach(): Unit = { + getListAppender.list.clear() + } + + private def getListAppender: ListAppender[ILoggingEvent] = { + loggerContext + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) + .getAppender("LIST") + .asInstanceOf[ListAppender[ILoggingEvent]] + } +} diff --git a/release.sbt b/release.sbt new file mode 100644 index 00000000..392ea254 --- /dev/null +++ b/release.sbt @@ -0,0 +1,18 @@ +// https://github.com/xerial/sbt-sonatype#using-with-sbt-release-plugin +import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ + +releaseCrossBuild := true // true if you cross-build the project for multiple Scala versions +releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, + tagRelease, + releaseStepCommandAndRemaining("+publishSigned"), + releaseStepCommand("sonatypeBundleRelease"), + setNextVersion, + commitNextVersion, + pushChanges +) \ No newline at end of file diff --git a/sonatype.sbt b/sonatype.sbt new file mode 100644 index 00000000..f5f09ae9 --- /dev/null +++ b/sonatype.sbt @@ -0,0 +1,21 @@ +// Project independent sonatype settings. + +sonatypeProfileName := "com.tersesystems" + +ThisBuild / developers := List( + Developer( + id = "tersesystems", + name = "Terse Systems", + email = "will@tersesystems.com", + url = url("https://tersesystems.com") + ) +) + +publishMavenStyle := true + +// https://github.com/xerial/sbt-sonatype#buildsbt +ThisBuild / publishTo := sonatypePublishToBundle.value + +// https://github.com/sbt/sbt-pgp#configuration-signing-key +// tersesystems signing key +usePgpKeyHex("9033D60F5F798D53") \ No newline at end of file diff --git a/version.sbt b/version.sbt new file mode 100644 index 00000000..e91862e6 --- /dev/null +++ b/version.sbt @@ -0,0 +1 @@ +ThisBuild / version := "0.1.0-SNAPSHOT"