Connecting the Dots
Petra Bierleutgeb
@pbvie
with Distributed Tracing
we'll focus on this one
What, Why, How and Where
handle order request
create order event
publish order
get user
handle order request
create order event
publish order
get user
trace
span
baggage items
tags
see: https://github.com/opentracing/specification/blob/master/semantic_conventions.md
see: https://github.com/opentracing/specification/blob/master/semantic_conventions.md
error log
followsFrom
childOf
Tracing-agnostic applications vs. tracing as part of application code
...and a look at opentracing-java
Span span = tracer.buildSpan("someWork").start();
try (Scope scope = tracer.scopeManager().activate(span, false)) {
// Do things.
} catch(Exception ex) {
Tags.ERROR.set(span, true);
span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex));
} finally {
span.finish();
}
Better ergonimics with
exTra
def withSpan[T](span: Span)(op: Span => T): T =
try {
op(span)
} finally {
span.finish()
}
usage
withSpan(tracer.buildSpan("myOp").start()) { span =>
// do something
}
def withSpan[T](op: Span => T)(implicit span: Span): T =
try {
op(span)
} finally {
span.finish()
}
usage
implicit val span = ...
// some other code
withSpan { _ =>
// do something
}
def withSpan[T](span: Span)(op: Span => T): T =
try {
op(span)
} catch {
case t: Throwable =>
span.setTag(Tags.ERROR.getKey, true)
span.log(
Map(
Fields.EVENT -> "error",
Fields.ERROR_OBJECT -> t
).asJava
)
throw t
} finally {
span.finish()
}
implicit val span = ...
withSpan { _ =>
7 / 0
}
span is reported and marked as error
final case class TracingContext(tracer: Tracer, span: Span)
def calculateSum(a: Int, b: Int)(implicit tc: TracingContext): Int = {
val childSpan = tc.tracer.buildSpan("calculateSum").asChildOf(tc.span).start()
withSpan(childSpan) { _ =>
a + b
}
}
def calculateSum(a: Int, b: Int)(implicit tc: TracingContext): Int = {
val childSpan = tc.tracer.buildSpan("calculateSum").asChildOf(tc.span).start()
withSpan(childSpan) { _ =>
a + b
}
}
this is a very common pattern...let's implement a more concise syntax for it
def withSpan[T](op: TracingContext => T)(implicit tc: TracingContext): T =
try {
op(tc.span)
} catch {
case t: Throwable =>
tc.span.setTag(Tags.ERROR.getKey, true)
tc.span.log(errorLog(t).asJava)
throw t
} finally {
tc.span.finish()
}
updating withSpan to work with TracingContext
def withChildSpan[T](
operationName: String
)(op: TracingContext => T)(implicit tc: TracingContext): T = {
val childSpan = tc.tracer.buildSpan(operationName).asChildOf(tc.span).start()
val childTracingContext = tc.copy(span = childSpan)
withSpan(op)(childTracingContext)
}
now we can implement withChildSpan
withSpan(TracingContext.withNewRootSpan("example")(tracer)) { implicit tc =>
withChildSpan("child-a") { _ =>
Thread.sleep(20)
}
withChildSpan("child-b") { _ =>
Thread.sleep(10)
}
withChildSpan("child-c") { _ =>
Thread.sleep(15)
7 / 0
}
}
withSpan(TracingContext.withNewRootSpan("example")(tracer)) { implicit tc =>
withChildSpan("async-child") { _ =>
Future {
blocking(Thread.sleep(3000))
}
}
}
duration of 'async-child' is only 7.14ms
def withSpan[T](op: TracingContext => T)(implicit tc: TracingContext): T =
try {
op(tc.span)
} catch {
case t: Throwable =>
tc.span.setTag(Tags.ERROR.getKey, true)
tc.span.log(errorLog(t).asJava)
throw t
} finally {
tc.span.finish()
}
def withSpanAsync[T](op: TracingContext => Future[T])
(implicit ec: ExecutionContext, tc: TracingContext): Future[T] =
op(tc)
.recoverWith {
case t: Throwable =>
tc.span.setTag(Tags.ERROR.getKey, true)
tc.span.log(errorLog(t).asJava)
Future.failed(t)
}
.andThen {
case _ => tc.span.finish()
}
Transporting span context across service boundaries
Service A
Service B
?
Traces usually involve multiple services
trace id
span id
parent id
flags
public interface TextMap extends Iterable<Map.Entry<String, String>> {
Iterator<Map.Entry<String,String>> iterator();
void put(String key, String value);
}
class KafkaHeaderInjector(private val headers: Headers) extends TextMap {
override def iterator(): util.Iterator[JavaMap.Entry[String, String]] =
throw new UnsupportedOperationException(
"should only be used with Tracer.inject()"
)
override def put(key: String, value: String): Unit =
headers.add(key, value.getBytes)
}
val record = new ProducerRecord(...)
tc.tracer.inject(tc.span.context(),
Format.Builtin.TEXT_MAP,
new KafkaHeaderInjector(record.headers()))
producer.send(record)
Glühwein Ltd.
mulled wine
user service
order service
preparation service
client
gRPC req/resp
Akka HTTP
Kafka
Akka Actors
thirsty customer
user service
order service
preparation service
client
gRPC req/resp
Akka HTTP
Kafka
Akka Actors
thirsty customer
user service
order service
preparation service
client
gRPC req/resp
Akka HTTP
Kafka
Akka Actors
thirsty customer
user service
order service
preparation service
client
gRPC req/resp
Akka HTTP
Kafka
Akka Actors
thirsty customer
user service
order service
preparation service
client
gRPC req/resp
Akka HTTP
Kafka
Akka Actors
thirsty customer
Result
Thank you!
Questions?
Icon made by Freepik from flaticon.com