Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add position formatter #90

Merged
merged 2 commits into from
Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
70 changes: 60 additions & 10 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,31 @@ 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)

def create(throwableFormat: ThrowableFormat, colorful: Boolean): Formatter =
create(throwableFormat, PositionFormat.Full, colorful)

/**
* 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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops, you're breaking already released API here. Either create overloaded method create that takes three arguments, or put it in the end of arguments list and give it a default value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added overloaded methods to Formatter and circe.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,6 +68,35 @@ 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.
*
Expand Down Expand Up @@ -127,4 +157,24 @@ object Formatter {
writeStackTrace(b, elements.tail, indent)
}
}

private def abbreviate(enclosure: String): String = {
@tailrec
def loop(input: Array[String], builder: StringBuilder): StringBuilder = {
input.length match {
case 0 => builder
case 1 => builder.append(input.head)
case _ =>
val head = input.head
val b = if (head.isEmpty) builder.append(questionMark) else builder.append(head.head)
loop(input.tail, b.append(dot))
}
}

loop(enclosure.split('.'), new StringBuilder).toString()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's reuse dot here as well

Copy link
Contributor Author

@iRevive iRevive Feb 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split treats string argument as a regex. The suitable options are "\\." and '.'.
Should I change it to:

  loop(enclosure.split(packageSeparator), new StringBuilder).toString()
}

private val packageSeparator = "\\."

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, it's a special symbol. Nvm then, it's not worth it

}

private val questionMark = "?"
private val dot = "."

}
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
18 changes: 10 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,30 @@ 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): OFormatter =
create(throwableFormat, PositionFormat.Full)

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