Use OpenTelemetry with Jaeger and the Micronaut Framework for Microservice Distributed Tracing

Use Jaeger distributed tracing to investigate the behavior of your Micronaut applications.

Authors: Sergio del Amo

Micronaut Version: 4.4.0

1. Getting Started

In this guide, we will integrate Jaeger with a Micronaut application composed of three microservices.

As on-the-ground microservice practitioners are quickly realizing, the majority of operational problems that arise when moving to a distributed architecture are ultimately grounded in two areas: networking and observability. It is a much larger problem to network and debug a set of intertwined distributed services versus a single monolithic application.

You will discover how the Micronaut framework eases Jaeger integration.

2. What you will need

To complete this guide, you will need the following:

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Writing the Application

To learn more about this sample application, read the Consul and the Micronaut Framework - Microservices Service Discovery guide. The application contains three microservices.

  • bookcatalogue - This returns a list of books. It uses a domain consisting of a book name and an ISBN.

  • bookinventory - This exposes an endpoint to check whether a book has sufficient stock to fulfil an order. It uses a domain consisting of a stock level and an ISBN.

  • bookrecommendation - This consumes previous services and exposes an endpoint, which recommends book names that are in stock.

The bookcatalogue service consumes endpoints exposed by the other services. The following image illustrates the application flow:

flow

A request to bookrecommendation (http://localhost:8080/books) triggers several requests through our microservices mesh.

5. Jaeger and the Micronaut Framework

5.1. Install Jaeger via Docker

The quickest way to start Jaeger is via Docker:

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

6. OpenTelemetry dependencies

6.1. OpenTelemetry annotations

To enable OpenTelemetry annotations, every service includes the following annotation processor:

build.gradle
annotationProcessor("io.micronaut.tracing:micronaut-tracing-opentelemetry-annotation")

6.2. Micronaut OpenTelemetry HTTP

To enable creation of span objects on every HTTP server request, client request, server response, and client response, each service includes the following dependency:

build.gradle
implementation("io.micronaut.tracing:micronaut-tracing-opentelemetry-http")

6.3. OpenTelemetry Protocol Exporter

An exporter is a component in the OpenTelemetry Collector configured to send data to different systems/back-ends.

Each service adds the OpenTelemetry otlp exporter dependency.

build.gradle
implementation("io.opentelemetry:opentelemetry-exporter-otlp")

Each service sets otlp as exporter in configuration:

bookcatalogue/src/main/resources/application.yml
otel:
  traces:
    exporter: otlp
  exporter:
    otlp:
      endpoint: http://localhost:4317 # Jaeger supports OTLP directly. The default port for OTLP/gRPC is 4317

6.4. Disable OpenTelemetry in Tests

Disable Micronaut Open Telemetry integration in tests:

bookcatalogue/src/test/resources/application-test.yml
micronaut:
  otel:
    enabled: false

6.5. Book Inventory Changes

Annotate the method with @ContinueSpan and the parameter with @SpanTag:

bookinventory/src/main/java/example/micronaut/BooksController.java
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.tracing.annotation.ContinueSpan;
import io.micronaut.tracing.annotation.SpanTag;

import jakarta.validation.constraints.NotBlank;
import java.util.Optional;

@Controller("/books")
public class BooksController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    @ContinueSpan (1)
    public Boolean stock(@SpanTag("stock.isbn") @NotBlank String isbn) { (2)
        return bookInventoryByIsbn(isbn).map(bi -> bi.getStock() > 0).orElse(null);
    }

    private Optional<BookInventory> bookInventoryByIsbn(String isbn) {
        if (isbn.equals("1491950358")) {
            return Optional.of(new BookInventory(isbn, 4));

        } else if (isbn.equals("1680502395")) {
            return Optional.of(new BookInventory(isbn, 0));
        }
        return Optional.empty();
    }
}
1 The @ContinueSpan annotation will continue an existing span, wrapping the method call or reactive type.
2 The @SpanTag annotation can be used on method arguments to include the value of each argument within a Span’s tags. When you use @SpanTag you need either to annotate the method with @NewSpan or @ContinueSpan.

7. Running the Application

Run bookcatalogue microservice:

To run the application, execute ./gradlew run.

...
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

Run bookinventory microservice:

To run the application, execute ./gradlew run.

...
14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 506ms. Server Running: http://localhost:8082

Run bookrecommendation microservice:

To run the application, execute ./gradlew run.

...
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080

You can run a cURL command to test the whole application:

curl http://localhost:8080/books
[{"name":"Building Microservices"}

You can then navigate to http://localhost:16686 to access the Jaeger UI.

The previous request generates the following traces, composed of 9 spans.

jaegerui opentelemetry

In the previous image, you can see that:

  • Whenever a Micronaut HTTP client executes a new network request, it creates a new span.

  • Whenever a Micronaut server receives a request, it creates a new span.

The stock.isbn tags that we configured with @SpanTag are present.

Moreover, you can see the requests to bookinventory are done in parallel.

8. Next Steps

As you have seen in this guide, without any annotations, you can get distributed tracing up and running quickly with the Micronaut framework.

The Micronaut framework includes several annotations to give you more flexibility. We introduced the @ContinueSpan and @SpanTag annotations. Also, you have at your disposal the @NewSpan annotation, which will create a new span, wrapping the method call or reactive type.

Make sure to read more about Tracing with OpenTelemetry in the Micronaut framework.

9. Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.

10. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).