Test OpenTelemetry Observability of Your JVM Applications with Local Grafana Backend

Arun Chalise
4 min readJul 22, 2024

--

Photo by Made By Morro on Unsplash

Open Telemetry

Observability platforms are essential for understanding the internal state of our application and the infrastructure they run on. By leveraging Traces, Metrics and Logs data emitted by application and the infrastructure, we gain valuable insights into the application behaviour and performance. Given the numerous observability backends available out there, it is crucial to ensure that our applications and infrastructure when instrumented to generate Traces, Logs and Metrics are not locked into a single vendor. OpenTelemetry addresses this by providing a single set of APIs and specifications to send our instrumentation data to vendor of our choice.

Source code for this demo is available at:

https://github.com/achalise/springboot-observability

Micrometer: Enhancing Observability for JVM applications

For JVM applications, micrometer provides an additional level of abstraction with out of box instrumentation and a unified Observability API that is capable of produce traces, metrics and logs and making the integration process straight forward.

Setting Up a Local Observability Backend

Having a local set up for testing application observability is highly beneficial. grafana/otel-lgtm provides a complete observability backend in a single container which is ideal for demo and local development .

The OpenTelemetry Collector in the container receives OpenTelemetry signals on ports 4317 (gRPC) and 4318 (HTTP). It forwards

  • metrics to a Prometheus database,
  • spans to a Tempo database, and
  • logs to a Loki database

Grafana has all three databases configured as data sources and exposes its Web UI on port 3000 for visualisation and analysis.

Application Configuration

In the set up for this article, we have two services — claimservice and paymentservice . We use micrometer to publish observability data via open telemetry to a local observability backend using grafana/otel-lgtm.

In claimservice we include the following dependencies to enable and export metrics and traces using micrometer to the local observability backend.

 dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")

runtimeOnly ("io.micrometer:micrometer-registry-otlp")
implementation ("io.micrometer:micrometer-tracing-bridge-otel")
implementation ("io.opentelemetry:opentelemetry-exporter-otlp")

developmentOnly("org.springframework.boot:spring-boot-devtools")
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

The endpoints are defined in application.properties

management.otlp.metrics.export.url=http://localhost:4318/v1/metrics
management.otlp.tracing.endpoint=http://localhost:4318/v1/traces

In payment-service , just for illustration, we make use of spring boot starter dependencies for convenience

dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")

implementation("com.grafana:grafana-opentelemetry-starter:1.4.0")
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

This set up will also publish metrics and traces to the local observability backend running on grafana/otel-lgtm by default.

Inside theclaimservice project, there is also a Docker Compose file compose.yaml used by development tools to start the container during application startup in development mode.

Custom instrumentation in our Application — Metrics and Traces Using Micrometer

We have defined a counter for every claim received successfully and also an error counter when there are exceptions. These counters are published as metrics to the observability backend by micrometer. We have also created two custom spans submit.claim and submit.payment .

@RestController
class ClaimController {
private Logger logger = LoggerFactory.getLogger(ClaimController.class);
private final ObservationRegistry observationRegistry;
private final Counter claimCounter;
private final Counter errorCounter;
private final RestClient.Builder restclientBuilder;

public ClaimController(ObservationRegistry observationRegistry, MeterRegistry meterRegistry, RestClient.Builder webClientBuilder) {
this.observationRegistry = observationRegistry;
this.claimCounter = Counter.builder("claim.counter")
.description("Counts Claim Submitted")
.tags("region", "us-east")
.register(meterRegistry);
this.errorCounter = Counter.builder("claim.error.counter")
.description("Counts Errors for Claim Submitted")
.tags("region", "us-east")
.register(meterRegistry);
this.restclientBuilder = webClientBuilder;
}
@PostMapping("/claim")
public Response submitClaim(@RequestBody Claim claim) {
logger.info("Received claim {}", claim);
Observation observation = Observation.createNotStarted("submit.claim", this.observationRegistry);
observation.lowCardinalityKeyValue("claimType", claim.type());
observation.highCardinalityKeyValue("claimAmount", claim.amount().toString());
AtomicReference<String> status = new AtomicReference<>("SUCCESS");
observation.observe(() -> {
// Execute business logic here
try {
Thread.sleep(30);
restclientBuilder.baseUrl("http://localhost:8081/payment").build().post().body(claim).retrieve();
claimCounter.increment();
if(RandomGenerator.getDefault().nextBoolean()) {
throw new RuntimeException("some error");
}
} catch (Exception _) {
errorCounter.increment();
observation.highCardinalityKeyValue("error", "error here");
status.set("ERROR");
}
});
return new Response(status.get(), UUID.randomUUID().toString());
}
}

record Response(String status, String correlationId){}
record Claim(String type, Double amount){}

Running the Demo

To run the demo, follow these steps:

ClaimService

  • cd claimservice
  • ./gradlew bootRun

PaymentService

  • cd paymentservice
  • ./gradlew bootRun

Submit a few requests to generate data for metrics and traces:

POST localhost:8080/claim
Content-Type: application/json

{
"type": "FLOOD_DAMAGE",
"amount": 300.50,
"email": "testuser@email.com"

}

Visualise in Grafana

We can now access grafana at http://localhost:3000 to explore observability data from our demo set up.

Conclusion

By combining OpenTelemetry and Micrometer, we achieve a flexible, vendor-agnostic observability solution for JVM applications. The grafana/otel-lgtm container further simplifies local development and testing, making it easier to integrate and visualise observability data.

Reference:

--

--

No responses yet