Distributed tracing

edit

A trace is a group of transactions and spans with a common root. Each trace tracks the entirety of a single request. When a trace travels through multiple services, as is common in a microservice architecture, it is known as a distributed trace.

Why is distributed tracing important?

edit

Distributed tracing enables you to analyze performance throughout your microservice architecture by tracing the entirety of a request — from the initial web request on your front-end service all the way to database queries made on your back-end services.

Tracking requests as they propagate through your services provides an end-to-end picture of where your application is spending time, where errors are occurring, and where bottlenecks are forming. Distributed tracing eliminates individual service’s data silos and reveals what’s happening outside of service borders.

For supported technologies, distributed tracing works out-of-the-box, with no additional configuration required.

How distributed tracing works

edit

Distributed tracing works by injecting a custom traceparent HTTP header into outgoing requests. This header includes information, like trace-id, which is used to identify the current trace, and parent-id, which is used to identify the parent of the current span on incoming requests or the current span on an outgoing request.

When a service is working on a request, it checks for the existence of this HTTP header. If it’s missing, the service starts a new trace. If it exists, the service ensures the current action is added as a child of the existing trace, and continues to propagate the trace.

Trace propagation examples
edit

In this example, Elastic’s Ruby agent communicates with Elastic’s Java agent. Both support the traceparent header, and trace data is successfully propagated.

How traceparent propagation works

In this example, Elastic’s Ruby agent communicates with OpenTelemetry’s Java agent. Both support the traceparent header, and trace data is successfully propagated.

How traceparent propagation works

In this example, the trace meets a piece of middleware that doesn’t propagate the traceparent header. The distributed trace ends and any further communication will result in a new trace.

How traceparent propagation works
W3C Trace Context specification
edit

All Elastic agents now support the official W3C Trace Context specification and traceparent header. See the table below for the minimum required agent version:

Agent name Agent Version

Go Agent

1.6

Java Agent

1.14

.NET Agent

1.3

Node.js Agent

3.4

PHP Agent

1.0

Python Agent

5.4

Ruby Agent

3.5

RUM Agent

5.0

Older Elastic agents use a unique elastic-apm-traceparent header. For backward-compatibility purposes, new versions of Elastic agents still support this header.

Visualize distributed tracing

edit

The APM UI’s timeline visualization provides a visual deep-dive into each of your application’s traces:

Distributed tracing in the APM UI

Manual distributed tracing

edit

Elastic agents automatically propagate distributed tracing context for supported technologies. If your service communicates over a different, unsupported protocol, you can manually propagate distributed tracing context from a sending service to a receiving service with each agent’s API.

Add the traceparent header to outgoing requests
edit

Sending services must add the traceparent header to outgoing requests.

  1. Start a transaction with startTransaction, or a span with startSpan.
  2. Inject the traceparent header into the request object with injectTraceHeaders

Example of manually instrumenting an RPC framework:

// Hook into a callback provided by the RPC framework that is called on outgoing requests
public Response onOutgoingRequest(Request request) throws Exception {
  Span span = ElasticApm.currentSpan() 
          .startSpan("external", "http", null)
          .setName(request.getMethod() + " " + request.getHost());
  try (final Scope scope = transaction.activate()) {
      span.injectTraceHeaders((name, value) -> request.addHeader(name, value)); 
      return request.execute();
  } catch (Exception e) {
      span.captureException(e);
      throw e;
  } finally {
      span.end(); 
  }
}

Create a span representing an external call

Inject the traceparent header into the request object

End the span

Parse the traceparent header on incoming requests
edit

Receiving services must parse the incoming traceparent header, and start a new transaction or span as a child of the received context.

  1. Create a transaction as a child of the incoming transaction with startTransactionWithRemoteParent().
  2. Start and name the transaction with activate() and setName().

Example:

// Hook into a callback provided by the framework that is called on incoming requests
public Response onIncomingRequest(Request request) throws Exception {
    // creates a transaction representing the server-side handling of the request
    Transaction transaction = ElasticApm.startTransactionWithRemoteParent(request::getHeader, request::getHeaders); 
    try (final Scope scope = transaction.activate()) { 
        String name = "a useful name like ClassName#methodName where the request is handled";
        transaction.setName(name); 
        transaction.setType(Transaction.TYPE_REQUEST); 
        return request.handle();
    } catch (Exception e) {
        transaction.captureException(e);
        throw e;
    } finally {
        transaction.end(); 
    }
}

Create a transaction as the child of a remote parent

Activate the transaction

Name the transaction

Add a transaction type

Eventually, end the transaction

Distributed tracing with RUM

edit

Some additional setup may be required to correlate requests correctly with the Real User Monitoring (RUM) agent.

See the RUM distributed tracing guide for information on enabling cross-origin requests, setting up server configuration, and working with dynamically-generated HTML.