Skip to content

Commit

Permalink
Add position formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Feb 11, 2020
1 parent e4ffd1f commit 97274b0
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 25 deletions.
13 changes: 12 additions & 1 deletion benchmarks/src/main/scala/io/odin/Benchmarks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import cats.effect.{ContextShift, IO, Timer}
import io.odin.loggers.DefaultLogger
import io.odin.syntax._
import io.odin.formatter.Formatter
import io.odin.formatter.options.ThrowableFormat
import io.odin.formatter.options.{PositionFormat, ThrowableFormat}
import io.odin.json.{Formatter => JsonFormatter}
import io.odin.meta.Position
import org.openjdk.jmh.annotations._
Expand Down Expand Up @@ -208,11 +208,19 @@ class FormatterBenchmarks extends OdinBenchmarks {

private val formatterDepth = Formatter.create(
ThrowableFormat(ThrowableFormat.Depth.Fixed(2), ThrowableFormat.Indent.NoIndent),
PositionFormat.Full,
colorful = false
)

private val formatterDepthIndent = Formatter.create(
ThrowableFormat(ThrowableFormat.Depth.Fixed(2), ThrowableFormat.Indent.Fixed(4)),
PositionFormat.Full,
colorful = false
)

private val abbreviated = Formatter.create(
ThrowableFormat.Default,
PositionFormat.AbbreviatePackage,
colorful = false
)

Expand All @@ -237,6 +245,9 @@ class FormatterBenchmarks extends OdinBenchmarks {
@Benchmark
def depthIndentFormatter(): Unit = formatterDepthIndent.format(loggerMessage)

@Benchmark
def abbreviatedPositionFormatter(): Unit = abbreviated.format(loggerMessage)

}

@State(Scope.Benchmark)
Expand Down
69 changes: 55 additions & 14 deletions core/src/main/scala/io/odin/formatter/Formatter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package io.odin.formatter

import cats.syntax.show._
import io.odin.LoggerMessage
import io.odin.formatter.options.ThrowableFormat
import io.odin.formatter.options.{PositionFormat, ThrowableFormat}
import io.odin.meta.Position
import perfolation._

import scala.annotation.tailrec
Expand All @@ -16,31 +17,28 @@ object Formatter {

val BRIGHT_BLACK = "\u001b[30;1m"

val default: Formatter = Formatter.create(ThrowableFormat.Default, colorful = false)
val default: Formatter = Formatter.create(ThrowableFormat.Default, PositionFormat.Full, colorful = false)

val colorful: Formatter = Formatter.create(ThrowableFormat.Default, colorful = true)
val colorful: Formatter = Formatter.create(ThrowableFormat.Default, PositionFormat.Full, colorful = true)

/**
* Creates new formatter with provided options
*
* @param throwableFormat @see [[formatThrowable]]
* @param positionFormat @see [[formatPosition]]
* @param colorful use different color for thread name, level, position and throwable
*/
def create(throwableFormat: ThrowableFormat, colorful: Boolean): Formatter = {
def create(throwableFormat: ThrowableFormat, positionFormat: PositionFormat, colorful: Boolean): Formatter = {

@inline def withColor(color: String, message: String): String =
if (colorful) p"$color$message$RESET" else message

(msg: LoggerMessage) => {
val ctx = withColor(MAGENTA, formatCtx(msg.context))
val timestamp = p"${msg.timestamp.t.F}T${msg.timestamp.t.T},${msg.timestamp.t.milliOfSecond}"
val timestamp = formatTimestamp(msg.timestamp)
val threadName = withColor(GREEN, msg.threadName)
val level = withColor(BRIGHT_BLACK, msg.level.show)

val position = {
val lineNumber = if (msg.position.line >= 0) p":${msg.position.line}" else ""
withColor(BLUE, p"${msg.position.enclosureName}$lineNumber")
}
val position = withColor(BLUE, formatPosition(msg.position, positionFormat))

val throwable = msg.exception match {
case Some(t) =>
Expand All @@ -67,14 +65,43 @@ object Formatter {
builder.toString()
}

/**
* Formats timestamp using the following format: yyyy-MM-ddTHH:mm:ss,SSS
*/
def formatTimestamp(timestamp: Long): String = {
val date = timestamp.t
p"${date.F}T${date.T},${date.milliOfSecond}"
}

/**
* The result differs depending on the format:
*
* [[PositionFormat.Full]] - prints full position
* 'io.odin.formatter.Formatter formatPosition:75' formatted as 'io.odin.formatter.Formatter formatPosition:75'
*
* [[PositionFormat.AbbreviatePackage]] - prints abbreviated package and full enclosing
* 'io.odin.formatter.Formatter formatPosition:75' formatted as 'i.o.f.Formatter formatPosition:75'
*/
def formatPosition(position: Position, format: PositionFormat): String = {
val lineNumber = if (position.line >= 0) p":${position.line}" else ""

val enclosure = format match {
case PositionFormat.Full => position.enclosureName
case PositionFormat.AbbreviatePackage => abbreviate(position.enclosureName)

}

p"$enclosure$lineNumber"
}

/**
* Default Throwable printer is twice as slow. This method was borrowed from scribe library.
*
* The result differs depending on the format:
* `ThrowableFormat.Depth.Full` - prints all elements of a stack trace
* `ThrowableFormat.Depth.Fixed` - prints N elements of a stack trace
* `ThrowableFormat.Indent.NoIndent` - prints a stack trace without indentation
* `ThrowableFormat.Indent.Fixed` - prints a stack trace prepending every line with N spaces
* [[ThrowableFormat.Depth.Full]] - prints all elements of a stack trace
* [[ThrowableFormat.Depth.Fixed]] - prints N elements of a stack trace
* [[ThrowableFormat.Indent.NoIndent]] - prints a stack trace without indentation
* [[ThrowableFormat.Indent.Fixed]] - prints a stack trace prepending every line with N spaces
*/
def formatThrowable(t: Throwable, format: ThrowableFormat): String = {
val indent = format.indent match {
Expand Down Expand Up @@ -127,4 +154,18 @@ object Formatter {
writeStackTrace(b, elements.tail, indent)
}
}

private def abbreviate(enclosure: String): String = {
@tailrec
def loop(input: List[String], builder: StringBuilder): StringBuilder = {
input match {
case Nil => builder
case head :: Nil => builder.append(head)
case head :: tail => loop(tail, builder.append(head.headOption.getOrElse('?')).append('.'))
}
}

loop(enclosure.split('.').toList, new StringBuilder).toString()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.odin.formatter.options

sealed trait PositionFormat

object PositionFormat {
final case object Full extends PositionFormat
final case object AbbreviatePackage extends PositionFormat
}
34 changes: 33 additions & 1 deletion core/src/test/scala/io/odin/formatter/FormatterSpec.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package io.odin.formatter

import io.odin.OdinSpec
import io.odin.formatter.options.ThrowableFormat
import io.odin.formatter.options.{PositionFormat, ThrowableFormat}
import io.odin.formatter.options.ThrowableFormat.{Depth, Indent}
import io.odin.formatter.FormatterSpec.TestException
import io.odin.meta.Position
import org.scalacheck.Gen

import scala.util.control.NoStackTrace
Expand Down Expand Up @@ -67,6 +68,31 @@ class FormatterSpec extends OdinSpec {
trace.length shouldBe expectedLength
}

behavior of "Formatter.formatPosition with PositionFormat"

it should "support PositionFormat" in forAll(positionFormatGen, Gen.posNum[Int]) { (format, line) =>
val position = Position("file.scala", "io.odin.formatter.Formatter enclosingMethod", "", line)

val expected = format match {
case PositionFormat.Full => s"${position.enclosureName}:$line"
case PositionFormat.AbbreviatePackage => s"i.o.f.Formatter enclosingMethod:$line"
}

val result = Formatter.formatPosition(position, format)

result shouldBe expected
}

it should "not abbreviate empty package" in {
val position = Position("file.scala", "enclosingMethod", "", -1)

val full = Formatter.formatPosition(position, PositionFormat.Full)
val abbreviated = Formatter.formatPosition(position, PositionFormat.AbbreviatePackage)

full shouldBe "enclosingMethod"
abbreviated shouldBe "enclosingMethod"
}

private lazy val indentGen: Gen[Indent] =
Gen.oneOf(
Gen.const(Indent.NoIndent),
Expand All @@ -79,6 +105,12 @@ class FormatterSpec extends OdinSpec {
Gen.posNum[Int].map(size => Depth.Fixed(size))
)

private lazy val positionFormatGen: Gen[PositionFormat] =
Gen.oneOf(
Gen.const(PositionFormat.Full),
Gen.const(PositionFormat.AbbreviatePackage)
)

}

object FormatterSpec {
Expand Down
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,14 @@ Beside copy-pasting the existing formatter to adjust it for one's needs, it's po

```scala
object Formatter {
def create(throwableFormat: ThrowableFormat, colorful: Boolean): Formatter
def create(throwableFormat: ThrowableFormat, positionFormat: PositionFormat, colorful: Boolean): Formatter
}
```

- [`ThrowableFormat`](https://github.com/valskalla/odin/blob/master/core/src/main/scala/io/odin/formatter/options/ThrowableFormat.scala)
allows to tweak the rendering of exceptions, specifically indentation and stack depth.
- [`PositionFormat`](https://github.com/valskalla/odin/blob/master/core/src/main/scala/io/odin/formatter/options/PositionFormat.scala)
allows to tweak the rendering of position.
- `colorful` flag enables logs highlighting.

## Minimal level
Expand Down
15 changes: 7 additions & 8 deletions json/src/main/scala/io/odin/json/Formatter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,27 @@ import io.circe.Encoder
import io.odin.LoggerMessage
import io.odin.formatter.{Formatter => OFormatter}
import io.odin.formatter.Formatter._
import io.odin.formatter.options.ThrowableFormat
import perfolation._
import io.odin.formatter.options.{PositionFormat, ThrowableFormat}

object Formatter {

val json: OFormatter = create(ThrowableFormat.Default)
val json: OFormatter = create(ThrowableFormat.Default, PositionFormat.Full)

def create(throwableFormat: ThrowableFormat): OFormatter = {
implicit val encoder: Encoder[LoggerMessage] = loggerMessageEncoder(throwableFormat)
def create(throwableFormat: ThrowableFormat, positionFormat: PositionFormat): OFormatter = {
implicit val encoder: Encoder[LoggerMessage] = loggerMessageEncoder(throwableFormat, positionFormat)
(msg: LoggerMessage) => msg.asJson.noSpaces
}

def loggerMessageEncoder(throwableFormat: ThrowableFormat): Encoder[LoggerMessage] =
def loggerMessageEncoder(throwableFormat: ThrowableFormat, positionFormat: PositionFormat): Encoder[LoggerMessage] =
Encoder.forProduct7("level", "message", "context", "exception", "position", "thread_name", "timestamp")(m =>
(
m.level.show,
m.message.value,
m.context,
m.exception.map(t => formatThrowable(t, throwableFormat)),
p"${m.position.enclosureName}:${m.position.line}",
formatPosition(m.position, positionFormat),
m.threadName,
p"${m.timestamp.t.F}T${m.timestamp.t.T}"
formatTimestamp(m.timestamp)
)
)

Expand Down

0 comments on commit 97274b0

Please sign in to comment.