Skip to Content
Course content

129: Distributed Tracing in Scala Services

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're tracking a high-priority package across the country. The package has one unique tracking number, but as it moves, it hits several different checkpoints: the local post office, a regional sorting hub, a cargo plane, and finally, another sorting hub in the destination city. Each of those stops logs a "scan" event. If the package disappears, you don't just see that it's "gone"; you can see exactly which hub held onto it for too long or where it was misrouted. That's exactly what distributed tracing does for your requests.

In a Scala microservices environment, your "package" is an incoming HTTP request. The "tracking number" is the Trace ID, and every individual operation—a database query, a call to another service, or a heavy computation—is a Span. The magic happens when you pass that Trace ID from one service to the next, allowing you to reconstruct the entire journey in a tool like Jaeger or Zipkin.

The Postal Service of Microservices

Let's map that analogy directly to how we actually build this. When a request hits your first Scala service, you generate a Trace ID. Every time that request calls another function or service, you create a child Span.

  • The Trace ID: The global identifier for the entire request lifecycle. It's like the tracking number on the box.
  • The Span: A timed block of work. "Fetching user profile from Postgres" is a span. "Calling the Payment API" is another span.
  • Context Propagation: This is the act of stuffing the Trace ID into the HTTP headers (usually as traceparent) so the next service knows it's part of the same journey.

The Nightmare of Async Boundaries

Here is where things get tricky in Scala. In a simple Java app, you might use a ThreadLocal to store the current trace context. But we aren't writing simple blocking Java; we're using Futures, ZIO, or Cats Effect. Since these libraries shift execution across different threads, a ThreadLocal will lose your trace context the moment you hit a flatMap. I've spent way too many hours debugging "broken traces" because a context wasn't propagated across a thread boundary.

To fix this, we use OpenTelemetry (OTel). Instead of relying on threads, OTel allows us to explicitly wrap our logic or use "context-aware" wrappers that carry the state along with the functional effect. I highly recommend sticking to the OpenTelemetry SDK rather than trying to roll your own header-passing logic—don't reinvent the wheel here.

Wiring up OpenTelemetry in Scala

You don't want to manually start and stop spans everywhere; that would clutter your business logic with boilerplate. Instead, we usually wrap our service calls. Here is a simplified look at how you'd wrap a call to an external Inventory service using the OTel API:

import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.Tracer

val tracer: Tracer = GlobalOpenTelemetry.getTracer("inventory-service-client")

def checkStock(productId: String): Future[Boolean] = {
  val span = tracer.spanBuilder("checkStock").startSpan()
  
  // We make the span 'current' so that any nested spans 
  // (like the actual HTTP client call) know who their parent is.
  val scope = span.makeCurrent()
  
  try {
    inventoryClient.getStock(productId).map { result =>
      span.setAttribute("product.id", productId)
      span.setAttribute("stock.available", result)
      result
    }.recover {
      case ex => 
        span.recordException(ex)
        throw ex
    }
  } finally {
    // Always close the scope and the span, or you'll leak memory
    scope.close()
    span.end()
  }
}


Notice the setAttribute call. This is where tracing becomes powerful. Instead of just knowing that checkStock was slow, you can see that it was specifically slow for productId = "ultra-heavy-item-123". That's the difference between "the system is slow" and "this specific product is causing a database bottleneck."

Rating
0 0

There are no comments for now.

to be the first to leave a comment.