Skip to content

Commit

Permalink
Manage span metadata better, add more exception metadata (#20)
Browse files Browse the repository at this point in the history
* Manage span metadata better, add more exception metadata

* Add missing file
  • Loading branch information
tomverran committed Sep 9, 2019
1 parent d9b0d39 commit 3df2d8f
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.instances.option._
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.syntax.traverse._
import com.ovoenergy.effect.natchez.DatadogSpan.CompletedSpan
import com.ovoenergy.effect.natchez.DatadogSpan.{CompletedSpan, SpanNames}
import fs2.concurrent.Queue
import io.circe.{Encoder, Printer}
import fs2._
Expand Down Expand Up @@ -80,19 +80,21 @@ object Datadog {
*/
def entryPoint[F[_]: Concurrent: Timer: Clock](
client: Client[F],
service: String
service: String,
resource: String
): Resource[F, EntryPoint[F]] =
for {
queue <- spanQueue
names = SpanNames.withFallback(_, SpanNames("unnamed", service, resource))
_ <- submitter(client, queue)
} yield {
new EntryPoint[F] {
def root(name: String): Resource[F, Span[F]] =
Resource.liftF(SpanIdentifiers.create).flatMap(DatadogSpan.create(queue, name, service)).widen
Resource.liftF(SpanIdentifiers.create).flatMap(DatadogSpan.create(queue, names(name))).widen
def continue(name: String, kernel: Kernel): Resource[F, Span[F]] =
DatadogSpan.fromKernel(queue, name, service, kernel).widen
DatadogSpan.fromKernel(queue, names(name), kernel).widen
def continueOrElseRoot(name: String, kernel: Kernel): Resource[F, Span[F]] =
DatadogSpan.fromKernel(queue, name, service, kernel).widen
DatadogSpan.fromKernel(queue, names(name), kernel).widen
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import cats.effect.{Clock, ExitCase, Resource, Sync}
import cats.syntax.apply._
import cats.syntax.flatMap._
import cats.syntax.functor._
import com.ovoenergy.effect.natchez.DatadogSpan.CompletedSpan
import com.ovoenergy.effect.natchez.DatadogSpan.{CompletedSpan, SpanNames}
import com.ovoenergy.effect.natchez.DatadogTags.forThrowable
import fs2.concurrent.Queue
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto._
Expand All @@ -21,9 +22,7 @@ import natchez.{Kernel, Span, TraceValue}
* while we interact with systems that provide non numeric trace tokens
*/
case class DatadogSpan[F[_]: Sync: Clock](
name: String,
service: String,
resource: String,
names: SpanNames,
ids: SpanIdentifiers,
start: Long,
queue: Queue[F, CompletedSpan],
Expand All @@ -42,6 +41,23 @@ case class DatadogSpan[F[_]: Sync: Clock](

object DatadogSpan {

/**
* Natchez only allows you to set the span name
* but we need also a resource + service which can differ by span. As such
* we allow you to encode this data with an advanced colon based DSL
*/
case class SpanNames(name: String, service: String, resource: String)

object SpanNames {

def withFallback(string: String, fallback: SpanNames): SpanNames =
string.split(':') match {
case Array(service, name, resource) => SpanNames(name, service, resource)
case Array(name, resource) => SpanNames(name, fallback.service, resource)
case Array(name) => SpanNames(name, fallback.service, fallback.resource)
}
}

implicit val config: Configuration =
Configuration.default.withSnakeCaseMemberNames

Expand Down Expand Up @@ -73,6 +89,16 @@ object DatadogSpan {
case ExitCase.Canceled => None
}

/**
* Create some Datadog tags from an exit case,
* i.e. if the span failed include exception details
*/
private def exitTags(exitCase: ExitCase[Throwable]): Map[String, String] =
exitCase match {
case ExitCase.Error(e) => forThrowable(e).mapValues(_.value.toString)
case _ => Map.empty
}

/**
* Given a span, complete it - this involves turning the span into a `CompletedSpan`
* which 1:1 matches the Datadog JSON structure before submitting it to a queue of spans
Expand All @@ -90,34 +116,23 @@ object DatadogSpan {
CompletedSpan(
traceId = datadogSpan.ids.traceId,
spanId = datadogSpan.ids.spanId,
name = datadogSpan.name,
service = datadogSpan.service,
resource = datadogSpan.resource,
name = datadogSpan.names.name,
service = datadogSpan.names.service,
resource = datadogSpan.names.resource,
start = datadogSpan.start,
duration = end - datadogSpan.start,
parentId = datadogSpan.ids.parentId,
error = isError(exitCase),
meta = meta.mapValues(_.value.toString).updated("traceToken", datadogSpan.ids.traceToken)
meta = exitTags(exitCase) ++
meta.mapValues(_.value.toString)
.updated("traceToken", datadogSpan.ids.traceToken)
)
}
.flatMap(datadogSpan.queue.enqueue1)

/**
* Datadog identifies traces through a combination of name, service and resource.
* We set service globally when creating an EntryPoint but we need a bespoke name + resource
* for every trace & natchez only supports name - as such we assume the name is actually both things
* split with a colon character
*/
private def resourceValue(name: String): String =
name.dropWhile(_ != ':').drop(1)

private def nameValue(name: String): String =
name.takeWhile(_ != ':')

def create[F[_]: Sync: Clock](
queue: Queue[F, CompletedSpan],
name: String,
service: String,
names: SpanNames,
meta: Map[String, TraceValue] = Map.empty
)(identifiers: SpanIdentifiers): Resource[F, DatadogSpan[F]] =
Resource.makeCase(
Expand All @@ -126,9 +141,7 @@ object DatadogSpan {
meta <- Ref.of(meta)
} yield
DatadogSpan(
name = nameValue(name),
service = service,
resource = resourceValue(name),
names = names,
identifiers,
start = start,
queue = queue,
Expand All @@ -140,14 +153,13 @@ object DatadogSpan {
for {
meta <- Resource.liftF(parent.meta.get)
ids <- Resource.liftF(SpanIdentifiers.child(parent.ids))
child <- create(parent.queue, name, parent.service, meta)(ids)
child <- create(parent.queue, SpanNames.withFallback(name, parent.names), meta)(ids)
} yield child

def fromKernel[F[_]: Sync: Clock](
queue: Queue[F, CompletedSpan],
name: String,
service: String,
names: SpanNames,
kernel: Kernel
): Resource[F, DatadogSpan[F]] =
Resource.liftF(SpanIdentifiers.fromKernel(kernel)).flatMap(create(queue, name, service))
Resource.liftF(SpanIdentifiers.fromKernel(kernel)).flatMap(create(queue, names))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.ovoenergy.effect.natchez

import natchez.TraceValue

/**
* Some helper functions to construct Datadog tags,
* I took these from the DD-Trace-Java library
*/
object DatadogTags {

sealed trait SpanType

object SpanType {
case object Web extends SpanType
case object Db extends SpanType
}

def env(env: String): (String, TraceValue) =
"env" -> env

def spanType(spanType: SpanType): (String, TraceValue) =
"span.type" -> spanType.toString

def serviceName(name: String): (String, TraceValue) =
"service.name" -> name

def sqlQuery(query: String): (String, TraceValue) =
"sql.query" -> query

def httpMethod(method: String): (String, TraceValue) =
"http.method" -> method

def httpStatusCode(code: Int): (String, TraceValue) =
"http.status_code" -> code

def httpUrl(url: String): (String, TraceValue) =
"http.url" -> url

def errorMessage(what: String): (String, TraceValue) =
"error.msg" -> what

def errorType(what: String): (String, TraceValue) =
"error.type" -> what

def errorStack(what: String): (String, TraceValue) =
"error.stack" -> what

def forThrowable(e: Throwable): Map[String, TraceValue] =
List(
errorMessage(e.getMessage),
errorType(e.getClass.getSimpleName),
errorStack(e.getStackTrace.mkString("\n"))
).toMap
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DatadogTest extends WordSpec with Matchers {
def run(f: EntryPoint[IO] => IO[Unit]): IO[List[Request[IO]]] =
Ref.of[IO, List[Request[IO]]](List.empty).flatMap { ref =>
val client: Client[IO] = Client(r => Resource.liftF(ref.update(_ :+ r).as(Response[IO]())))
entryPoint(client, "test").use(f) >> ref.get
entryPoint(client, "test", "blah").use(f) >> ref.get
}


Expand All @@ -53,12 +53,20 @@ class DatadogTest extends WordSpec with Matchers {
}

"Submit multiple spans across multiple calls when span() is called" in {
val res = run(_.root("bar:res").use(_.span("subspan").use(_ => timer.sleep(1.second)))).unsafeRunSync
val res = run(_.root("bar").use(_.span("subspan").use(_ => timer.sleep(1.second)))).unsafeRunSync
val spans = res.flatTraverse(_.as[List[List[CompletedSpan]]]).unsafeRunSync.flatten
spans.map(_.traceId).distinct.length shouldBe 1
spans.map(_.spanId).distinct.length shouldBe 2
}

"Allow you to override the service name and resource with colons" in {
val res = run(_.root("svc:name:res").use(_ => IO.unit)).unsafeRunSync
val spans = res.flatTraverse(_.as[List[List[CompletedSpan]]]).unsafeRunSync.flatten
spans.head.resource shouldBe "res"
spans.head.service shouldBe "svc"
spans.head.name shouldBe "name"
}

"Inherit metadata into subspans but only at the time of creation" in {
val res = run(
_.root("bar:res").use { root =>
Expand Down

0 comments on commit 3df2d8f

Please sign in to comment.