Context Propagation for Distributed Tracing in the Java API

Distributed tracing allows your enterprise applications to trace the flow of messages as they travel from your publisher, through the event mesh and to the receiving application. For a detailed overview see Distributed Tracing . For information about version requirements, see Distributed Tracing Version Compatibility.

  • The PubSub+ OpenTelemetry API Libraries support W3C propagators only.
  • For information about configuring OpenTelemetry SDK environment variables see OpenTelemetry SDK Configuration.
  • By default, traces include command line parameters visible to backend applications like Jaeger. It is important to disable this feature for security purposes because these parameters may contain sensitive information such as your user name and password. For instructions, see Disabling Automatic Resource Providers in the OpenTelemetry documentation in GitHub.

Instrumenting Java for Distributed Tracing

Manual Instrumentation involves making changes to your enterprise application's source code, and allows you to inject and extract additional context, such as baggage and trace states, into messages. Context propagation makes it easy to debug and optimize your application. For more information about context propagation in Solace event messages, see Distributed Tracing Context Propagation. The following examples show you how to create spans using the OpenTelemetry API.

Understanding How Context Propagation Enables Distributed Tracing in the PubSub+ Java API

In your client application, you can use the OpenTelemetry API to create a span, which contains metadata about an operation in a distributed system. This span is associated with a context, which includes a unique TraceID. Next, when you use a PubSub+ message producer to publish a message, the Solace OTel integration package injects the context, which contains the TraceID, into the message. As the message travels through the event broker and is received by a consuming application, spans are generated at each step and have the same TraceID present in the original message context. When each span is closed in the publishing or consuming application, the Java OpenTelemetry API sends it to an OpenTelemetry collector, which collects, processes and exports the spans to a backend application that correlates the spans using their unique TraceID. A backend application uses the correlated spans to create a trace, which is an end-to-end snapshot detailing how the message traveled through the distributed system. If you do not use context propagation, then backend applications cannot use a unique TraceID to link the spans, making it difficult to trace the flow of messages through the distributed system.

Dependencies

To enable context propagation for distributed tracing, you must first add the Solace PubSub+ OpenTelemetry Integration For Solace Java API library as a dependency in your application. This library automatically adds the OpenTelemetry API libraries, which are required for context propagation. For OpenTelemetry version compatibility see Distributed Tracing Version Compatibility. Adding the libraries gives you access to the following two interfaces:

  • SolacePubSubPlusJavaTextMapSetter— This interface allows a TextMapPropagator to inject context into a message.
  • SolacePubSubPlusJavaTextMapGetter— This interface allows a TextMapPropagator to extract context from a message.

This guide assumes you are familiar with configuring an instance of the OpenTelemetry class. For instructions for configuring OpenTelemetry objects, see OpenTelemetry Manual Instrumentation in Java in the OpenTelemetry documentation.

To use context propagation in the Java PubSub+ API, include the following packages in your application:

import com.solace.messaging.trace.propagation.SolacePubSubPlusJavaTextMapGetter; // Allows a TextMapPropagator to  extract context from a message.
import com.solace.messaging.trace.propagation.SolacePubSubPlusJavaTextMapSetter; // Allows a TextMapPropagator to  inject context into a message. 
import io.opentelemetry.api.baggage.Baggage;                 // For managing distributed context propagation data
import io.opentelemetry.api.baggage.propagation.BaggageUtil; // Utility functions for working with Baggage
import io.opentelemetry.api.OpenTelemetry;                   // Entry point for OpenTelemetry API
import io.opentelemetry.api.trace.Span;                      // For representing a single operation within a trace
import io.opentelemetry.api.trace.SpanKind;                  // Enum for the role of a Span in a trace, for example SERVER or CLIENT
import io.opentelemetry.api.trace.StatusCode;                // Enum for status codes to indicate success or failure of a Span
import io.opentelemetry.api.trace.Tracer;                    // Interface for creating and managing Spans
import io.opentelemetry.context.Context;                     // For managing context propagation across threads and asynchronous callbacks
import io.opentelemetry.context.Scope;                       // For controlling the current context scope
import io.opentelemetry.context.propagation.TextMapPropagator;       // Interface for injecting and extracting context data in text-based carriers
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; // Predefined attribute keys for tracing
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingDestinationKindValues; // Constants for messaging destination kinds
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingOperationValues;       // Constants for messaging operations

Generating a Send Span on Message Publish

Your publishing application can generate a send span and export it to the OpenTelemetry Collector. The following steps show you how to inject context into a message and generate a send span for a published message:

  1. Create a new span and set span attributes with the setAttribute() method. Next, set the current context as the parent of this span with the setParent() method. Then, start the span with the startSpan() method:
    final Span sendSpan = tracer
            .spanBuilder("mySolacePublisherApp" + " " + MessagingOperationValues.PROCESS)
            .setSpanKind(SpanKind.CLIENT)
            .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.TOPIC)
            // Set more attributes as needed
            //.setAttribute(...)
            .setParent(Context.current())
            .startSpan();
    
  2. (Optional) Create a Baggage object:
  3. Baggage baggage = BaggageUtil.extractBaggage("key1=val1,key2=val2");	
  4. Set the created span from Step 1 (sendSpan in this example) as the new current context. Use a try-with-resources statement to ensure that the span is correctly started and ended, even if an exception occurs:
  5. try (Scope spanScope = sendSpan.makeCurrent()) {
  6. (Optional) Attach the baggage from Step 2 to the current context. Use a try-with-resources statement to ensure that the baggage is correctly associated with the current context and is active only for the duration of the try-with-resources statement:
  7.     try (Scope baggageScope = baggage.storeInContext(Context.current()).makeCurrent()) {
  8. Create your TextMapSetter and TextMapPropagator objects, which allow you to inject context into a message and then propagate that context across your distributed system. Inject the current context, with its attached sendSpan (and optional baggage) into your message with the TextMapPropagator's inject() method:
             final SolacePubSubPlusJavaTextMapSetter setter = new SolacePubSubPlusJavaTextMapSetter();
             final TextMapPropagator propagator = openTelemetry.getPropagators().getTextMapPropagator();
             propagator.inject(Context.current(), message, setter);	
  9. Publish the message, handle exceptions as required, and call the end() method on the span to export the span data:
            messagePublisher.publish(message, messageDestination);
          }
    } catch (Exception e) {  
        sendSpan.recordException(e);                           // Span can record exception if any  
        sendSpan.setStatus(StatusCode.ERROR, e.getMessage());  // Set span status as ERROR/FAILED
    } finally {
        sendSpan.end();
    }
    

For a complete example of distributed tracing in a message publisher, see the howToCreateSpanAndBaggageOnMessagePublish() method in HowToImplementTracingManualInstrumentation.java on the Solace Developer Hub.

Generating a Receive Span on Message Receive

Your consuming application can generate a receive span and then export it to the OpenTelemetry Collector. The following steps show you how to extract tracing context from a received message and generate a receive span:

  1. Create a SolacePubSubPlusJavaTextMapGetter object and use the extract() method to extract the context from a received message:
  2. final SolacePubSubPlusJavaTextMapGetter getter = new SolacePubSubPlusJavaTextMapGetter();
    final Context extractedContext = openTelemetry.getPropagators().getTextMapPropagator()
            .extract(Context.current(), receivedMessage, getter);
    
  3. (Optional) Extract baggage from the extracted context with the fromContextOrNull() method:
  4. Baggage baggage = Baggage.fromContextOrNull(extractedContext);		
  5. Set the extracted context as current context. Use a try-with-resources statement to ensure that the span is correctly started and ended, even if an exception occurs:
  6. try (Scope spanScope = extractedContext.makeCurrent()) {
  7. Create a new span and set span attributes with the setAttribute() method. Next, create a parent-child relationship with the extracted span's context by calling the setParent() method. Then, start the span with the startSpan() method:
  8.     final Span receiveSpan = tracer
                .spanBuilder("mySolaceReceiverApp" + " " + MessagingOperationValues.RECEIVE)
                .setSpanKind(SpanKind.CLIENT)
                .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.TOPIC)
                // Set more attributes as needed
                //.setAttribute(...)
                .setParent(extractedContext)
                .startSpan();
    
  9. Process the message, handle exceptions as required, and call the end() method on the span to export the span data:
  10.     try {
            messageProcessor.accept(receivedMessage);
        } catch (Exception e) {      
            receiveSpan.recordException(e);                            // Span can record exception if any
            receiveSpan.setStatus(StatusCode.ERROR, e.getMessage());   // and set span status as ERROR/FAILED
        } finally {
            receiveSpan.end();
        }
    }
    

For a complete example of distributed tracing in a message subscriber, see the howToCreateNewSpanOnMessageReceive() method in HowToImplementTracingManualInstrumentation.java on the Solace Developer Hub.