java

Spicing Up Microservices with OpenTelemetry in Micronaut

Tame Distributed Chaos: OpenTelemetry and Micronaut's Symphony for Microservices

Spicing Up Microservices with OpenTelemetry in Micronaut

Understanding the behavior and performance of microservices in a distributed architecture is crucial. It’s where implementing distributed tracing steps in, especially in Micronaut applications. One of the best tools for this job is OpenTelemetry, which makes generating and exporting tracing data pretty straightforward. Here’s a spiced-up, super casual guide on integrating OpenTelemetry with Micronaut to get distributed tracing up and running.

Getting Started

First things first, you need to set up a few essentials to get going. Ensure that JDK 17 or a newer version is installed and that JAVA_HOME is configured properly on your machine. You’ll also need a capable text editor or Integrated Development Environment (IDE), and IntelliJ IDEA fits the bill perfectly for most. Lastly, pick your building tool - whether it’s Gradle or Maven, you’re good.

Setting Up Your Micronaut Application

Start off by creating a new Micronaut application. You can use the Micronaut Command Line Interface (CLI) or Micronaut Launch for this task. If you go with the CLI, you can fire off a command like this:

mn create-app example.micronaut.micronautguide --features=yaml,tracing-opentelemetry-zipkin,http-client --build=maven --lang=java --test=junit

Once the command is executed, you’ll have a brand new Micronaut application with all the necessary features for tracing and HTTP client functionality baked in.

Adding OpenTelemetry Dependencies

To infuse OpenTelemetry into your Micronaut application, you need to plug in the appropriate dependencies into your build.gradle or pom.xml file. Going the Gradle route? You’d add something like this:

dependencies {
    implementation "io.micronaut:micronaut-tracing"
    implementation "io.micronaut.tracing:micronaut-tracing-opentelemetry"
    annotationProcessor "io.micronaut.tracing:micronaut-tracing-opentelemetry-annotation"
}

If you’re more of a Maven person, then this is what you need for your pom.xml:

<dependencies>
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-tracing</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut.tracing</groupId>
        <artifactId>micronaut-tracing-opentelemetry</artifactId>
    </dependency>
</dependencies>

<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.tracing</groupId>
        <artifactId>micronaut-tracing-opentelemetry-annotation</artifactId>
    </path>
</annotationProcessorPaths>

Configuring OpenTelemetry

Next step: Configuration. OpenTelemetry can be configured via the application.yml file. Here’s a sample configuration to get you started:

otel:
  traces:
    exporter: zipkin
  service:
    name: micronaut-guide
  zipkin:
    url: http://localhost:9411

This setup configures OpenTelemetry to shoot the tracing data to a Zipkin server humming along at http://localhost:9411.

Running Jaeger or Zipkin

For visualizing all that tracing data, you need a tracing server like Jaeger or Zipkin. A quick Docker command can spin up Jaeger:

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 \
  jaegertracing/all-in-one:1.34

This Docker command does the trick, starting a Jaeger all-in-one container loaded with the collector, query, and UI components.

Writing Your Application

Picture this: You have three microservices—bookcatalogue, bookinventory, and bookrecommendation. Each handling different tasks and communicating with one another.

Book Catalogue Service

Let’s stitch together a simple Book Catalogue service that returns a list of books:

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

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

    @Get
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> getBooks() {
        // Return a list of books
        return Arrays.asList(
            new Book("Book1", "ISBN1"),
            new Book("Book2", "ISBN2")
        );
    }
}

Book Inventory Service

Next, create a Book Inventory service that checks if a book has enough stock to fulfill an order:

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

@Controller("/books/stock")
public class BookInventoryController {

    @Get("/{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Boolean hasStock(@PathVariable String isbn) {
        // Check if the book has stock
        return true; // Replace with actual logic
    }
}

Book Recommendation Service

Finally, craft a Book Recommendation service that recommends books in stock. It will consume endpoints from the previous services:

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.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;

@Controller("/books/recommend")
public class BookRecommendationController {

    private final HttpClient httpClient;

    public BookRecommendationController(@Client("http://localhost:8081") HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    @Get
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> recommendBooks() {
        // Fetch books from catalogue
        List<Book> books = httpClient.toBlocking().retrieve("/books", List.class);

        // Check stock for each book
        List<String> recommendedBooks = new ArrayList<>();
        for (Book book : books) {
            if (httpClient.toBlocking().retrieve("/books/stock/" + book.getIsbn(), Boolean.class)) {
                recommendedBooks.add(book.getName());
            }
        }

        return recommendedBooks;
    }
}

Using Tracing Annotations

To overlay tracing, sprinkle in a few tracing annotations provided by Micronaut:

import io.micronaut.tracing.annotation.NewSpan;
import io.micronaut.tracing.annotation.SpanTag;

@Controller("/books/recommend")
public class BookRecommendationController {

    @Get
    @Produces(MediaType.APPLICATION_JSON)
    @NewSpan("recommend-books")
    public List<String> recommendBooks() {
        // Fetch books from catalogue
        List<Book> books = httpClient.toBlocking().retrieve("/books", List.class);

        // Check stock for each book
        List<String> recommendedBooks = new ArrayList<>();
        for (Book book : books) {
            if (httpClient.toBlocking().retrieve("/books/stock/" + book.getIsbn(), Boolean.class)) {
                recommendedBooks.add(book.getName());
            }
        }

        return recommendedBooks;
    }

    @NewSpan("check-stock")
    public Boolean checkStock(@SpanTag("isbn") String isbn) {
        // Check stock logic here
        return true; // Replace with actual logic
    }
}

Running the Application

To kick things off, navigate into each service directory and run:

gradle run

This launches each microservice on different ports. Access the services via their respective URLs and watch the magic happen.

Visualizing Traces

When everything’s up and running, check out the Jaeger UI at http://localhost:16686 to visualize the traces. You can see the spans created by each service and their interactions.

Conclusion

Incorporating distributed tracing with OpenTelemetry in Micronaut applications isn’t rocket science. By adding the necessary dependencies, tweaking configurations, and using tracing annotations, you unlock deep insights into your microservices’ behavior. This is a game-changer for troubleshooting and fine-tuning your distributed system’s performance.

Distributed tracing is a cornerstone for modern microservice architectures. Micronaut paired with OpenTelemetry provides a robust and user-friendly solution to achieve top-tier tracing capabilities. Follow these steps, and you’ll have a seamless, efficient tracing setup, empowering you to monitor and debug like a pro!

Keywords: Micronaut, OpenTelemetry, distributed tracing, microservices, JDK 17, IntelliJ IDEA, Maven, Gradle, Zipkin, Jaeger



Similar Posts
Blog Image
How to Integrate Vaadin with RESTful and GraphQL APIs for Dynamic UIs

Vaadin integrates with RESTful and GraphQL APIs, enabling dynamic UIs. It supports real-time updates, error handling, and data binding. Proper architecture and caching enhance performance and maintainability in complex web applications.

Blog Image
10 Essential Java Data Validation Techniques for Clean Code

Learn effective Java data validation techniques using Jakarta Bean Validation, custom constraints, and programmatic validation. Ensure data integrity with practical code examples for robust, secure applications. Discover how to implement fail-fast validation today.

Blog Image
7 Advanced Java Bytecode Manipulation Techniques for Optimizing Performance

Discover 7 advanced Java bytecode manipulation techniques to enhance your applications. Learn to optimize, add features, and improve performance at runtime. Explore ASM, Javassist, ByteBuddy, and more.

Blog Image
Build Real-Time Applications: Using WebSockets and Push with Vaadin

WebSockets enable real-time communication in web apps. Vaadin, a Java framework, offers built-in WebSocket support for creating dynamic, responsive applications with push capabilities, enhancing user experience through instant updates.

Blog Image
Rust's Const Evaluation: Supercharge Your Code with Compile-Time Magic

Const evaluation in Rust allows complex calculations at compile-time, boosting performance. It enables const functions, const generics, and compile-time lookup tables. This feature is useful for optimizing code, creating type-safe APIs, and performing type-level computations. While it has limitations, const evaluation opens up new possibilities in Rust programming, leading to more efficient and expressive code.

Blog Image
Tag Your Tests and Tame Your Code: JUnit 5's Secret Weapon for Developers

Unleashing the Power of JUnit 5 Tags: Streamline Testing Chaos into Organized Simplicity for Effortless Efficiency