Distributed tracing with Micronaut framework

Visualizing data - abstract purple background with motion blur, digital data analysis concept
Reading Time: 7 minutes

1. About

1.1 Micronaut

Micronaut is an open-source JVM-based software framework for building lightweight, modular applications and microservices. It helps to create microservices with small memory footprints and quick startup time.

1.1 Distributed tracing

Troubleshooting interactions between microservices in a distributed architecture can be challenging when operating microservices in production. There are variously distributed tracing solutions, the most popular of which are Zipkin and Jaeger, both of which provide different levels of support for the Open Tracing API (OpenTracing is a tool developed by Zipkin to provide cross-platform compatibility. It offers a vendor-neutral API for adding tracing to the application and distributing that data to a distributed tracing system.). The most popular of which are Zipkin and Jagger, both of which provide varying levels of support for the Open Tracing API.

2. Use case

Let’s take a use case. Rahul is a shopkeeper, he sells items like mobile phones, laptops, etc, he wants to keep track of stock in his shop and wants to know whether his shop has a stock of a particular item or not without checking manually. To fulfill this requirement following three services are developed

  • ItemCatalogue – It returns a list of items. It uses a domain consisting of an item name and an itemId. The itemCatalog service consumes endpoints exposed by other services
  • ItemInventory – It exposes an endpoint to check whether an item has enough stock to satisfy the order. It uses a domain with a stock level and an itemId
  • ItemRecommendation – This consumes previous services and exposes an endpoint, which recommends item names that are in stock

3. Pre-requisite

  • Micronaut
  • Gradle
  • JDK 1.8 or greater installed with JAVA_HOME
  • Enable annotation Processing.

annotation

  • Install Jaeger 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

4. MicroServices

Add tracing dependency in build.gradle file of ItemCatalogue , ItemInventory and ItemRecommedation

implementation("io.micronaut:micronaut-tracing")
//to send tracing spans to Jaeger
runtimeOnly("io.jaegertracing:jaeger-thrift")

4.1 ItemCatalogue

The project structure for itemCatalogue microservice will be as follows-

itemCatalogue-project-structure

The Application.java will be as follows-

package com.example;

import io.micronaut.runtime.Micronaut;
import io.micronaut.context.env.Environment;

public class Application {
    public static void main(String[] args) {
        Micronaut.build(args)
                .mainClass(Application.class)
                .defaultEnvironments(Environment.DEVELOPMENT)
                .start();
    }
}

The item.java will be as follows-

package com.example;

import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;

import javax.validation.constraints.NotBlank;

@Introspected
public class Item {

    @NonNull
    @NotBlank
    private String itemId;

    @NonNull
    @NotBlank
    private String name;

    public Item() {}

    public Item(@NonNull @NotBlank String itemId, @NonNull @NotBlank String name) {
        this.itemId = itemId;
        this.name = name;
    }

    @NonNull
    public String getItemId() { return itemId; }

    public void setItemId(String itemId) { this.itemId = itemId; }

    @NonNull
    public String getName() {
        return name;
    }

    public void setName(@NonNull String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Item item = (Item) o;

        if (!itemId.equals(item.itemId)) return false;
        return name.equals(item.name);
    }

    @Override
    public int hashCode() {
        int result = itemId.hashCode();
        result = 30 * result + name.hashCode();
        return result;
    }
}

The item.java will be as follows-

package com.example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

import java.util.Arrays;
import java.util.List;

@Controller("/items")
public class ItemsController {

    @Get
    public List<Item> index() {
        Item buildingMicroservices = new Item("1491950358", "Building Microservices");
        Item releaseIt = new Item("1680502395", "Release It!");
        Item cidelivery = new Item("0321601912", "Continuous Delivery:");
        return Arrays.asList(buildingMicroservices, releaseIt, cidelivery);
    }
}

Configuration of application.yml file will be as follows-

micronaut:
  application:
    name: itemCatalogue
endpoints:
  health:
    enabled: true
    sensitive: false
#tag::jaeger[]
tracing:
  jaeger:
    enabled: true
    sampler:
      probability: 1.0 # <1>

Configuration of application-dev.yml file will be as follows-

micronaut:
  server:
    port: 8081 # <1>

4.2 ItemInvetory

The project structure for itemInventory microservice will be as follows-

itemInventory-project-structure

The ItemInventory.java will be as follows-

package com.example;

import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Objects;

@Introspected
public class ItemInventory {

    @NonNull
    @NotBlank
    private String itemId;

    @NonNull
    @NotNull
    private Integer stock;

    public ItemInventory() {}

    public ItemInventory(@NonNull @NotBlank String itemId, @NonNull @NotNull Integer stock) {
        this.itemId = itemId;
        this.stock = stock;
    }

    @NonNull
    public String getItemId() {
        return itemId;
    }

    public void setItemId(@NonNull String itemId) {
        this.itemId = itemId;
    }

    @NonNull
    public Integer getStock() {
        return stock;
    }

    public void setStock(@NonNull Integer stock) {
        this.stock = stock;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ItemInventory that = (ItemInventory) o;

        if (!itemId.equals(that.itemId)) return false;
        return stock.equals(that.stock);
    }

    @Override
    public int hashCode() {
        int result = itemId.hashCode();
        result = 31 * result + stock.hashCode();
        return result;
    }
}

The ItemsController.java will be as follows-

package com.example;

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 javax.validation.constraints.NotBlank;
import java.util.Optional;

@Controller("/items")
public class ItemsController {

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

    private Optional<ItemInventory> itemInventoryByItemId(String itemId) {
        if (itemId.equals("1491950358")) {
            return Optional.of(new ItemInventory(itemId, 4));

        } else if (itemId.equals("1680502395")) {
            return Optional.of(new ItemInventory(itemId, 5));
        }
        return Optional.empty();
    }
}

Note : The Application.java file , application.yml will be implemented same as what we did for itemCatalogue Microservice.

Configuration of application-dev.yml file

micronaut:
  server:
    port: 8082 # <1>

4.3 ItemRecommedation

The project structure for itemRecommedation microservice will be as follows-

itemRecommendation-project-structure

Create item.java file

package com.example;

import java.util.Objects;


public class Item {

    private String itemId;
    private String name;

    public Item() {}

    public Item(String itemId, String name) {
        this.itemId = itemId;
        this.name =  name;
    }

    public String getItemId() {
        return itemId;
    }

    public void setItemId(String itemId) {
        this.itemId = itemId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Item item = (Item) o;
        return Objects.equals(itemId, item.itemId) &&
                Objects.equals(name, item.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(itemId, name);
    }
}

Create ItemCatalogueClient

package com.example;

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client
import io.micronaut.retry.annotation.Recoverable;
import org.reactivestreams.Publisher;

@Client("http://localhost:8081") // <1>
@Recoverable(api = ItemCatalogueOperations.class)

public interface ItemCatalogueClient extends ItemCatalogueOperations {

    @Get("/items")
    Publisher<Item> findAll();
}

Create ItemCatalogueOperations class

package com.example;

import org.reactivestreams.Publisher;

public interface ItemCatalogueOperations {
    Publisher<Item> findAll();
}

Create ItemController class

package com.example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import reactor.core.publisher.Flux;
import org.reactivestreams.Publisher;

@Controller("/items")
public class ItemController {

    private final ItemCatalogueOperations itemCatalogueOperations;
    private final ItemInventoryOperations itemInventoryOperations;

    public ItemController(ItemCatalogueOperations itemCatalogueOperations,
                          ItemInventoryOperations itemInventoryOperations) {
        this.itemCatalogueOperations = itemCatalogueOperations;
        this.itemInventoryOperations = itemInventoryOperations;
    }

    @Get
    public Publisher<ItemRecommendation> index() {

        return Flux.from(itemCatalogueOperations.findAll())
                .flatMap(b -> Flux.from(itemInventoryOperations.stock(b.getItemId()))
                        .filter(Boolean::booleanValue)
                        .map(rsp -> b)
                ).map(item -> new ItemRecommendation(item.getName()));
    }
}

Create ItemInventoryClient class

package com.example;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Recoverable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

import javax.validation.constraints.NotBlank;

@Client("http://localhost:8082")
@Recoverable(api = ItemInventoryOperations.class)
public interface ItemInventoryClient extends ItemInventoryOperations {

    @Consumes(MediaType.TEXT_PLAIN)
    @Get("/items/stock/{itemId}")
    Mono<Boolean> stock(@NotBlank String itemId);
}

Create ItemInventoryOperations class

package com.example;

import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import javax.validation.constraints.NotBlank;

public interface ItemInventoryOperations {
    Mono<Boolean> stock(@NotBlank String itemId);
}

Create ItemRecommendation class

package com.example;

import io.micronaut.core.annotation.Introspected;

import java.util.Objects;

@Introspected
public class ItemRecommendation {
    private String name;

    public ItemRecommendation() {}

    public ItemRecommendation(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ItemRecommendation that = (ItemRecommendation) o;
        return Objects.equals(name, that.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name);
    }
}

Note : The Application.java file , application.yml will be implemented same as what we did for itemCatalogue Microservice.

Configuration of application-dev.yml file

micronaut:
  server:
    port: 8080 # <1>

5. Work Flow

Run itemCatalogue microservice. via cmd gradle run

itemCatalogue-run

You can navigate http://localhost:8081/items

itemCatalogue-api

Run itemInventory microservice. via cmd gradle run

itemInventory-run

You can navigate http://localhost:8082/items/stock/1680502395

itemInventory-api

Run itemRecommendation microservice. via cmd gradle run

itemRecommendation-run

You can navigate http://localhost:8080/items

itemRecommendation-api

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

jaeger-ui

jaeger-ui2

In the above image, you can see that:

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

In addition, you can see that item inventory requests are made in parallel.

Note:

IF you see this error io.jaegertracing.internal.exceptions.SenderException: Failed to flush spans, in your console while navigating Jaeger UI. Then open terminal and remove the container via following cmd
    docker stop <Container_ID>
    docker rm <Container_ID>

6. Conclusion

In this blog, we have covered Distributed tracing with Jaeger and the Micronaut framework. Now you are ready with the understanding of Distributed tracing with Jaeger and the Micronaut framework.. For more, you can refer to the documentation: https://blog.knoldus.com/micornaut-data-jpa-hibernate/

7. References

https://guides.micronaut.io/latest/micronaut-microservices-distributed-tracing-jaeger-gradle-java.html

Written by 

Gaurav Dubey is a Java Developer at Knoldus Software LLP. He has done M.CA from Kurukshetra University and completed Bachelor's Degree in Computer Science from M. D. University. He is a tech enthusiast with good knowledge of Java. He is majorly focused in Java practice. On the personal front, he loves to cook and travel.