Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
wsargent committed May 1, 2022
0 parents commit 0a3b6e2
Show file tree
Hide file tree
Showing 29 changed files with 3,648 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
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/
11 changes: 11 additions & 0 deletions .scalafmt.conf
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
219 changes: 219 additions & 0 deletions README.md
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()))
}
}
```
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
}
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))
}
}
}
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
}
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
Loading

0 comments on commit 0a3b6e2

Please sign in to comment.