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 support to collect messages without throwables #11

Merged
merged 3 commits into from
Jun 30, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ import scala.collection.immutable.Queue
private[client] case class ExceptionAggregate(
totalCount: Long = 0,
severity: Severity = Severity.Error,
latestExceptions: Queue[ExceptionWithContext] = Queue.empty,
latestExceptions: Queue[ExceptionOccurrence] = Queue.empty,
createdAt: ZonedDateTime = ZonedDateTime.now()
) {
// limit memory consumption keep only N exceptions per aggregation key
val maxExceptions = 10

def aggregationKey: String = latestExceptions.head.aggregationKey

def add(exceptionWithContext: ExceptionWithContext): ExceptionAggregate = {
require(latestExceptions.isEmpty || aggregationKey == exceptionWithContext.aggregationKey)
def add(exceptionOcurrence: ExceptionOccurrence): ExceptionAggregate = {
require(latestExceptions.isEmpty || aggregationKey == exceptionOcurrence.aggregationKey)

val truncatedLatest = if (latestExceptions.size < maxExceptions) latestExceptions else latestExceptions.dequeue._2
copy(
totalCount = totalCount + 1,
severity = exceptionWithContext.severity,
latestExceptions = truncatedLatest enqueue exceptionWithContext
severity = exceptionOcurrence.severity,
latestExceptions = truncatedLatest enqueue exceptionOcurrence
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ class ExceptionCollector {

/** Collect an exception without providing an HTTP context.
*/
def add(throwable: Throwable, severity: Severity = Severity.Error): Unit = addExceptionWithContext(
def add(throwable: Throwable, severity: Severity = Severity.Error): Unit = addExceptionOccurrence(
ExceptionWithContext(throwable, severity)
)

/** Collect a message without providing an HTTP context.
*/
def addMessage(key: String, message: String, severity: Severity = Severity.Info): Unit = addExceptionOccurrence(
ExceptionMessage(key, message, severity)
)

/** Collect an exception providing an HTTP context.
*/
def addWithContext(exceptionWithContext: ExceptionWithContext): Unit = addExceptionWithContext(exceptionWithContext)
def addWithContext(ExceptionOccurrence: ExceptionOccurrence): Unit = addExceptionOccurrence(ExceptionOccurrence)

/** Get a dump of all exception aggregates.
*/
def getExceptionAggregates: Seq[ExceptionAggregate] = {
exceptions.values.asScala.toSeq
}

private def addExceptionWithContext(exception: ExceptionWithContext): Unit = {
private def addExceptionOccurrence(exception: ExceptionOccurrence): Unit = {
exceptions.compute(
exception.aggregationKey,
new BiFunction[String, ExceptionAggregate, ExceptionAggregate] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ class ExceptionExporter(exceptionCollector: ExceptionCollector) {
jsonMapper.writeValueAsString(payload)
}

private def jsonException(t: Throwable): Map[String, Any] = Map(
private def jsonExceptionWithContext(t: Throwable): Map[String, Any] = Map(
"class" -> t.getClass.getName,
"message" -> t.getMessage,
"stacktrace" -> t.getStackTrace.map(_.toString),
"cause" -> Option(t.getCause).map(jsonException)
"cause" -> Option(t.getCause).map(jsonExceptionWithContext)
)

private def jsonExceptionMessage(m: String): Map[String, Any] = Map(
"message" -> m
)

private def jsonHttpContext(httpContext: HttpContext): Map[String, Any] =
Expand All @@ -37,13 +41,21 @@ class ExceptionExporter(exceptionCollector: ExceptionCollector) {
"request_body" -> httpContext.requestBody
)

private def jsonErrorWithContext(e: ExceptionWithContext): Map[String, Any] = Map(
"error" -> jsonException(e.throwable),
"severity" -> Severity.toString(e.severity),
"uuid" -> e.uuid.toString,
"timestamp" -> e.timestamp.format(rfc3339TimeFormat),
"http_context" -> e.httpContext.map(jsonHttpContext)
)
private def jsonErrorWithContext(e: ExceptionOccurrence): Map[String, Any] = {
val error = e match {
case ExceptionWithContext(throwable, _, _, _, _) => jsonExceptionWithContext(throwable)
case ExceptionMessage(_, message, _, _, _, _) => jsonExceptionMessage(message)
case _ => Map.empty
}

Map(
"error" -> error,
"severity" -> Severity.toString(e.severity),
"uuid" -> e.uuid.toString,
"timestamp" -> e.timestamp.format(rfc3339TimeFormat),
"http_context" -> e.httpContext.map(jsonHttpContext)
)
}

private def jsonAggregatedErrors(aggregate: ExceptionAggregate): Map[String, Any] = Map(
"aggregation_key" -> aggregate.aggregationKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.soundcloud.periskop.client

import java.time.ZonedDateTime
import java.util.UUID

import scala.util.hashing.MurmurHash3

/** Additional HTTP-related context for ExceptionWithContext.
Expand All @@ -14,16 +13,28 @@ case class HttpContext(
requestBody: Option[String]
)

trait ExceptionOccurrence {
def exceptionThrowable: Option[Throwable]
def severity: Severity
def uuid: UUID
def timestamp: ZonedDateTime
def httpContext: Option[HttpContext]
def aggregationKey: String
def message: String
}

/** Wraps an exception with useful metadata.
*/
case class ExceptionWithContext(
throwable: Throwable,
severity: Severity,
uuid: UUID = UUID.randomUUID,
timestamp: ZonedDateTime = ZonedDateTime.now,
httpContext: Option[HttpContext] = None
) {
val severity: Severity,
val uuid: UUID = UUID.randomUUID,
val timestamp: ZonedDateTime = ZonedDateTime.now,
val httpContext: Option[HttpContext] = None
) extends ExceptionOccurrence {
val className: String = throwable.getClass.getName
val message: String = throwable.getMessage()
val exceptionThrowable = Some(throwable)

/** Key used to group exceptions (and then limit the number of kept exceptions FIFO-style).
*
Expand All @@ -39,3 +50,16 @@ case class ExceptionWithContext(
s"$className@${backtraceHash.toHexString}"
}
}

/** A simple message without throwable.
*/
case class ExceptionMessage(
val aggregationKey: String,
val message: String,
val severity: Severity,
val uuid: UUID = UUID.randomUUID,
val timestamp: ZonedDateTime = ZonedDateTime.now,
val httpContext: Option[HttpContext] = None
) extends ExceptionOccurrence {
lazy val exceptionThrowable = None
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ class ExceptionAggregateSpec extends Specification {

a.totalCount ==== 3
a.latestExceptions.length ==== 3
a.latestExceptions.head.throwable ==== e
a.latestExceptions.head.asInstanceOf[ExceptionWithContext].throwable ==== e
a.latestExceptions.head.severity ==== Severity.Error

a.latestExceptions.last.throwable ==== a.latestExceptions.head.throwable
a.latestExceptions.last.asInstanceOf[ExceptionWithContext].throwable ==== a.latestExceptions.head
.asInstanceOf[ExceptionWithContext]
.throwable
a.latestExceptions.last !=== a.latestExceptions.head
}

Expand All @@ -45,8 +47,8 @@ class ExceptionAggregateSpec extends Specification {

a.totalCount ==== 15
a.latestExceptions.length ==== 10
a.latestExceptions.head.throwable.getMessage ==== "foo 6"
a.latestExceptions.last.throwable.getMessage ==== "foo 15"
a.latestExceptions.head.asInstanceOf[ExceptionWithContext].throwable.getMessage ==== "foo 6"
a.latestExceptions.last.asInstanceOf[ExceptionWithContext].throwable.getMessage ==== "foo 15"
}

"throws if the aggregation key does not match" in new Context {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,55 @@ import org.specs2.specification.Scope

class ExceptionCollectorSpec extends Specification with Mockito {

class FooException(msg: String) extends RuntimeException
"#add" >> {
class FooException(msg: String) extends RuntimeException

trait Context extends Scope {
trait Context extends Scope {

val collector = new ExceptionCollector
val collector = new ExceptionCollector

val e: Throwable = new FooException("bar")
val ewc = ExceptionWithContext(e, Severity.Error)
}
val e: Throwable = new FooException("bar")
val ewc = ExceptionWithContext(e, Severity.Error)
}

"saves exceptions internally and exposes them" in new Context {
collector.add(e)
collector.add(e)
collector.add(new RuntimeException("bar"))

val a: Seq[ExceptionAggregate] = collector.getExceptionAggregates.sortBy(_.aggregationKey)
a.length ==== 2

"saves exceptions internally and exposes them" in new Context {
collector.add(e)
collector.add(e)
collector.add(new RuntimeException("bar"))
a.head.totalCount === 2
a.head.latestExceptions.head.asInstanceOf[ExceptionWithContext].throwable ==== e

val a: Seq[ExceptionAggregate] = collector.getExceptionAggregates.sortBy(_.aggregationKey)
a.length ==== 2
a.last.totalCount === 1
}

a.head.totalCount === 2
a.head.latestExceptions.head.throwable ==== e
"accepts raw exceptions" in new Context {
collector.add(e)

a.last.totalCount === 1
collector.getExceptionAggregates.head.latestExceptions.head.asInstanceOf[ExceptionWithContext].throwable ==== e
}
}

"accepts raw exceptions" in new Context {
collector.add(e)
"#addMessage" >> {
trait Context extends Scope {
val collector = new ExceptionCollector
}

"saves messages internally and exposes them" in new Context {
collector.addMessage("key1", "message1", Severity.Info)
collector.addMessage("key1", "message2", Severity.Info)
collector.addMessage("key2", "message3", Severity.Info)

val a: Seq[ExceptionAggregate] = collector.getExceptionAggregates.sortBy(_.aggregationKey)
a.length ==== 2

a.head.totalCount === 2
a.head.latestExceptions.head.asInstanceOf[ExceptionMessage].message ==== "message1"

collector.getExceptionAggregates.head.latestExceptions.head.throwable ==== e
a.last.totalCount === 1
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ class ExceptionExporterSpec extends Specification with Mockito {
val uuid1 = UUID.fromString("c3c24195-27ae-4455-ba5a-7b504a7699a4")
val uuid2 = UUID.fromString("f12feecd-7518-46c3-88a6-38d57804e81a")
val uuid3 = UUID.fromString("ceeefdf5-cdee-4f1b-b139-b2c739d16dcf")
val uuid4 = UUID.fromString("e42e2212-fdf9-4550-95bd-0b9b4996904c")

val timestamp1 = ZonedDateTime.parse("2018-01-02T11:22:33+00:00")
val timestamp2 = ZonedDateTime.parse("2018-01-02T11:22:55+00:00")
val timestamp3 = ZonedDateTime.parse("2018-01-03T11:22:55+00:00")

val exceptionAggregates: Seq[ExceptionAggregate] = Seq(
ExceptionAggregate(
Expand Down Expand Up @@ -88,6 +90,21 @@ class ExceptionExporterSpec extends Specification with Mockito {
httpContext = None
)
)
),
ExceptionAggregate(
totalCount = 1,
severity = Severity.Info,
createdAt = timestamp3,
latestExceptions = Queue(
ExceptionMessage(
severity = Severity.Info,
message = "some info",
aggregationKey = "some-key",
uuid = uuid4,
timestamp = timestamp3,
httpContext = None
)
)
)
)

Expand Down Expand Up @@ -206,6 +223,23 @@ class ExceptionExporterSpec extends Specification with Mockito {
| "http_context": null
| }
| ]
| },
| {
| "aggregation_key": "some-key",
| "total_count": 1,
| "severity": "info",
| "created_at": "2018-01-03T11:22:55.000Z",
| "latest_errors": [
| {
| "error": {
| "message": "some info"
| },
| "severity": "info",
| "uuid": "e42e2212-fdf9-4550-95bd-0b9b4996904c",
| "timestamp": "2018-01-03T11:22:55.000Z",
| "http_context": null
| }
| ]
| }
| ]
|}
Expand Down
Loading