DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Exploring Hazelcast With Spring Boot
  • How to Introduce a New API Quickly Using Micronaut
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing

Trending

  • Code Reviews: Building an AI-Powered GitHub Integration
  • Using Java Stream Gatherers To Improve Stateful Operations
  • Advancing Your Software Engineering Career in 2025
  • Chat With Your Knowledge Base: A Hands-On Java and LangChain4j Guide
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Monitoring and Observability
  4. OpenTelemetry Tracing on Spring Boot: Java Agent vs. Micrometer Tracing

OpenTelemetry Tracing on Spring Boot: Java Agent vs. Micrometer Tracing

In this post, compare three different ways to utilize OpenTelemtry Tracing and Spring Boot components: Java agent v1, Java agent v2, and Micrometer Tracing.

By 
Nicolas Fränkel user avatar
Nicolas Fränkel
DZone Core CORE ·
Aug. 12, 24 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
10.3K Views

Join the DZone community and get the full member experience.

Join For Free

My demo of OpenTelemetry Tracing features two Spring Boot components. One uses the Java agent, and I noticed a different behavior when I recently upgraded it from v1.x to v2.x. In the other one, I'm using Micrometer Tracing because I compile to GraalVM native, and it can't process Java agents.

I want to compare these three different ways in this post: Java agent v1, Java agent v2, and Micrometer Tracing.

The Base Application and Its Infrastructure

I'll use the same base application: a simple Spring Boot application, coded in Kotlin. It offers a single endpoint.

  • The function beyond the endpoint is named entry(). 
  • It calls another function named intermediate(). 
  • The latter uses a WebClient instance, the replacement of RestTemplate, to make a call to the above endpoint.
  • To avoid infinite looping, I pass a custom request header: if the entry() function finds it, it doesn't proceed further.

Base application: A simple Spring Boot application, coded in Kotlin

It translates into the following code:

Kotlin
 
@SpringBootApplication
class Agent1xApplication

@RestController
class MicrometerController {

    private val logger = LoggerFactory.getLogger(MicrometerController::class.java)

    @GetMapping("/{message}")
    fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) {
        logger.info("entry: $message")
        if (done == null) intermediate()
    }

    fun intermediate() {
        logger.info("intermediate")
        RestClient.builder()
            .baseUrl("http://localhost:8080/done")
            .build()
            .get()
            .header("X-done", "true")
            .retrieve()
            .toBodilessEntity()
    }
}


For every setup, I'll check two stages: the primary stage, with OpenTelemetry enabled, and a customization stage to create additional internal spans.

Micrometer Tracing

Micrometer Tracing stems from Micrometer, a "vendor-neutral application observability facade."

Micrometer Tracing provides a simple facade for the most popular tracer libraries, letting you instrument your JVM-based application code without vendor lock-in. It is designed to add little to no overhead to your tracing collection activity while maximizing the portability of your tracing effort.

- Micrometer Tracing site

To start with Micrometer Tracing, one needs to add a few dependencies:

  • Spring Boot Actuator, org.springframework.boot:spring-boot-starter-actuator
  • Micrometer Tracing itself, io.micrometer:micrometer-tracing
  • A "bridge" to the target tracing backend API; In my case, it's OpenTelemetry, hence io.micrometer:micrometer-tracing-bridge-otel
  • A concrete exporter to the backend, io.opentelemetry:opentelemetry-exporter-otlp

We don't need a BOM because versions are already defined in the Spring Boot parent.

Yet, we need two runtime configuration parameters: where should the traces be sent, and what is the component's name. They are governed by the MANAGEMENT_OTLP_TRACING_ENDPOINT and SPRING_APPLICATION_NAME variables.

YAML
 
services:
  jaeger:
    image: jaegertracing/all-in-one:1.55
    environment:
      - COLLECTOR_OTLP_ENABLED=true                                     #1
    ports:
      - "16686:16686"
  micrometer-tracing:
    build:
      dockerfile: Dockerfile-micrometer
    environment:
      MANAGEMENT_OTLP_TRACING_ENDPOINT: http://jaeger:4318/v1/traces    #2
      SPRING_APPLICATION_NAME: micrometer-tracing                       #3


  1. Enable the OpenTelemetry collector for Jaeger.
  2. Full URL to the Jaeger OpenTelemetry gRPC endpoint.
  3. Set the OpenTelemetry's service name.

Here's the result:

Micrometer Tracing result

Without any customization, Micrometer creates spans when receiving and sending HTTP requests.

The framework needs to inject magic into the RestClient for sending. We must let the former instantiate the latter for that:

Kotlin
 
class MicrometerTracingApplication {

    @Bean
    fun restClient(builder: RestClient.Builder) =
        builder.baseUrl("http://localhost:8080/done").build()
}


We can create manual spans in several ways, one via the OpenTelemetry API itself. However, the setup requires a lot of boilerplate code. The most straightforward way is the Micrometer's Observation API. Its main benefit is to use a single API that manages both metrics and traces.

Micrometer's Observation API

Here's the updated code:

Kotlin
 
class MicrometerController(
    private val restClient: RestClient,
    private val registry: ObservationRegistry
) {

    @GetMapping("/{message}")
    fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) {
        logger.info("entry: $message")
        val observation = Observation.start("entry", registry)
        if (done == null) intermediate(observation)
        observation.stop()
    }

    fun intermediate(parent: Observation) {
        logger.info("intermediate")
        val observation = Observation.createNotStarted("intermediate", registry)
            .parentObservation(parent)
            .start()
        restClient.get()
            .header("X-done", "true")
            .retrieve()
            .toBodilessEntity()
        observation.stop()
    }
}


The added observation calls reflect upon the generated traces:

The added observation calls reflect upon the generated traces

OpenTelemetry Agent v1

An alternative to Micrometer Tracing is the generic OpenTelemetry Java Agent. Its main benefit is that it impacts neither the code nor the developers; the agent is a pure runtime-scoped concern.

Shell
 
java -javaagent:opentelemetry-javaagent.jar agent-one-1.0-SNAPSHOT.jar


The agent abides by OpenTelemetry's configuration with environment variables:

YAML
 
services:
  agent-1x:
    build:
      dockerfile: Dockerfile-agent1
    environment:
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317                   #1
      OTEL_RESOURCE_ATTRIBUTES: service.name=agent-1x                   #2
      OTEL_METRICS_EXPORTER: none                                       #3
      OTEL_LOGS_EXPORTER: none                                          #4
    ports:
      - "8081:8080"


  1. Set the protocol, the domain, and the port. The library appends /v1/traces. 
  2. Set the OpenTelemetry's service name.
  3. Export neither the metrics nor the logs.

With no more configuration, we get the following traces:

Traces following no more configuration

The agent automatically tracks requests, both received and sent, as well as functions marked with Spring-related annotations. Traces are correctly nested inside each other, according to the call stack. To trace additional functions, we need to add a dependency to our codebase, io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations. We can now annotate previously untraced functions with the @WithSpan annotation.

Tracing additional functions

The value() part governs the trace's label, while the kind translates as a span.kind attribute. If the value is set to an empty string, which is the default, it outputs the function's name. For my purposes, default values are good enough.

Kotlin
 
@WithSpan
fun intermediate() {
    logger.info("intermediate")
    RestClient.builder()
        .baseUrl("http://localhost:8080/done")
        .build()
        .get()
        .header("X-done", "true")
        .retrieve()
        .toBodilessEntity()
}


It yields the expected new intermediate() trace:

Results showing new intermediate() trace

OpenTelemetry Agent v2

OpenTelemetry released a new major version of the agent in January of this year. I updated my demo with it. Traces are now only created when the app receives and sends requests.

Traces are now only created when the app receives and sends requests

As for the previous version, we can add traces with the @WithSpan annotation. The only difference is that we must also annotate the entry() function. It's not traced by default.

Annotate the entry() function

Discussion

Spring became successful for two reasons: it simplified complex solutions, i.e., EJBs 2, and provided an abstraction layer over competing libraries. Micrometer Tracing started as an abstraction layer over Zipkin and Jaeger, and it made total sense. This argument becomes moot with OpenTelemetry being supported by most libraries across programming languages and trace collectors. The Observation API is still a considerable benefit of Micrometer Tracing, as it uses a single API over Metrics and Traces.

On the Java Agent side, OpenTelemetry configuration is similar across all tech stacks and libraries - environment variables. I was a bit disappointed when I upgraded from v1 to v2, as the new agent is not Spring-aware: Spring-annotated functions are not traced by default. In the end, it's a wise decision. It's much better to be explicit about the spans you want than remove some you don't want to see.

  • Thanks to Jonatan Ivanov for his help and his review.
  • The complete source code for this post can be found on GitHub.

To Go Further

  • OpenTelemetry Traces
  • OpenTelemetry Java integration
  • OpenTelemetry Java examples
  • Distributed Tracing with Spring Boot 3 — Micrometer vs OpenTelemetry
  • Observability With Spring Boot 3
API Java (programming language) Kotlin (programming language) Spring Boot Telemetry

Published at DZone with permission of Nicolas Fränkel, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Exploring Hazelcast With Spring Boot
  • How to Introduce a New API Quickly Using Micronaut
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!