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
Rust's Const Generics: Revolutionizing Array Abstractions with Zero Runtime Overhead

Rust's const generics allow creating types parameterized by constant values, enabling powerful array abstractions without runtime overhead. They facilitate fixed-size array types, type-level numeric computations, and expressive APIs. This feature eliminates runtime checks, enhances safety, and improves performance by enabling compile-time size checks and optimizations for array operations.

Blog Image
Java Developers: Stop Doing This If You Want to Succeed in 2024

Java developers must evolve in 2024: embrace new versions, modern tools, testing, cloud computing, microservices, design patterns, and performance optimization. Contribute to open source, prioritize security, and develop soft skills.

Blog Image
Micronaut's Compile-Time Magic: Supercharging Java Apps with Lightning-Fast Dependency Injection

Micronaut's compile-time dependency injection boosts Java app performance with faster startup and lower memory usage. It resolves dependencies during compilation, enabling efficient runtime execution and encouraging modular, testable code design.

Blog Image
Java's Hidden Power: Mastering Advanced Type Features for Flexible Code

Java's polymorphic engine design uses advanced type features like bounded type parameters, covariance, and contravariance. It creates flexible frameworks that adapt to different types while maintaining type safety, enabling powerful and adaptable code structures.

Blog Image
Unlock Hidden Performance: Circuit Breaker Patterns That Will Change Your Microservices Forever

Circuit breakers prevent cascading failures in microservices, acting as protective bubbles. They monitor failures, adapt to scenarios, and unlock performance by quickly failing calls to struggling services, promoting resilient architectures.

Blog Image
Java Security Best Practices: Essential Techniques for Protecting Applications from Common Vulnerabilities

Learn essential Java application security techniques including password hashing, input validation, and TLS configuration. Protect against SQL injection, XSS, and CSRF attacks with practical code examples.