-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0a3b6e2
Showing
29 changed files
with
3,648 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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())) | ||
} | ||
} | ||
``` |
8 changes: 8 additions & 0 deletions
8
api/src/main/scala/com/tersesystems/echopraxia/scala/api/AbstractLoggerSupport.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
19 changes: 19 additions & 0 deletions
19
api/src/main/scala/com/tersesystems/echopraxia/scala/api/Condition.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
api/src/main/scala/com/tersesystems/echopraxia/scala/api/DefaultMethodsSupport.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
75 changes: 75 additions & 0 deletions
75
api/src/main/scala/com/tersesystems/echopraxia/scala/api/FieldBuilder.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.