DEV Community

Yonatan Karp-Rudin
Yonatan Karp-Rudin

Posted on • Originally published at yonatankarp.com on

Observability in Action Part 1: Enhancing Your Codebase with OpenTelemetry

Image description

TL;DR: In this article, you'll learn how to build a client library for fetching cat facts from an API using Kotlin. The library can handle multiple concurrent API calls and return unique facts. OpenTelemetry and service instrumentation will be covered in later parts of this series to enhance observability.

Introduction

In this series, we'll delve into the steps for adding observability to your codebase. Initially, we'll develop a library that retrieves data from a remote API. Following that, we'll construct a service using this library to fetch and save this data in a database.

As we progress, we'll infuse observability into the service, demonstrating their behavior in an environment resembling production. Once the service is equipped with instrumentation, we'll introduce a filter to it that dismisses overly large requests. This filter, too, will be instrumented.

To wrap up, we'll integrate an OpenTelemetry collector into our service. This will gather all traces and metrics, transmitting them to an external location to mitigate any service overhead.

Series Outline

All code examples for this series are available on GitHub:

kitten poking cat's nose

Introduction to Service Instrumentation

What is service instrumentation? And why do I need it?

Service instrumentation is the process of collecting data from different components in your system (e.g. services) to benefit insights into the system's performance, behavior, and usage. This data can be used to optimize the system, troubleshoot, and improve the user experience.

More specifically, we will use OpenTelemetry. OpenTelemetry documentation states:

OpenTelemetry, also known as OTel for short, is a vendor-neutral open-source Observability framework for instrumenting, generating, collecting, and exporting telemetry data such as traces, metrics, logs. As an industry-standard, it is natively supported by a number of vendors.

That means that OpenTelemetry is a framework that allows you to easily add instrumentation to your codebase and collect the data in a vendor-agnostic way. It supports multiple programming languages and provides a unified API for collecting and exporting telemetry data to various backends.

OpenTelemetry also provides a set of libraries and integrations that make it easy to instrument popular frameworks, libraries, and services. With OpenTelemetry, developers can easily add telemetry to their services and gain visibility into their systems' performance and behavior.

For more information about observability, check out a great article "How Observability Changed My (Developer) Life" written by a colleague of mine, Mariusz Sotysiak.

Cat in helmet

Building the Client Library

We'll kick off by developing our client library named cat-fact-client. This library will fetch cat facts from the Cat Facts API.

At its core, our library is straightforward. It endeavors to fetch a specified number of facts. While the API restricts fact selection, we compensate by invoking the API multiple times, as required, making the best effort to serve the requested number of facts.

Our library will utilize:

  • Kotlin - The crux of our library, it will be scripted in Kotlin using coroutines.

  • Gradle - Our trusted build system and dependency manager.

  • Retrofit - Our choice for an HTTP client.

  • Jackson - Essential for serialization, particularly as well be integrating with Spring Boot which defaults to Jackson.

Lets get coding!

Cat writing code

Adding Dependencies

Kick-off by adding the essential dependencies to the build.gradle.kts file:

dependencies {
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

    // Serialization
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")

    // Retrofit
    api("com.squareup.okhttp3:okhttp:4.11.0")
    api("com.squareup.retrofit2:retrofit:2.9.0")
    api("com.squareup.retrofit2:converter-jackson:2.9.0")
}
Enter fullscreen mode Exit fullscreen mode

Domain Modeling

When you ping the Cat Facts API, expect a response similar to:

{"fact":"Cats have \"nine lives\" thanks to a flexible spine and powerful leg and back muscles","length":83}

Enter fullscreen mode Exit fullscreen mode

Our primary concern is the fact field. To determine the fact length, we simply utilize fact.length. This gives rise to our model:

data class Fact(val value: String)
Enter fullscreen mode Exit fullscreen mode

By leveraging Kotlin's value class, we optimize resource utilization. While we interact solely with Fact objects, these objects are substituted with String objects during compilation.

Thus, we revised our code to:

@JvmInline
value class Fact(val value: String)
Enter fullscreen mode Exit fullscreen mode

Constructing the HTTP Client

Having established our domain model, it's time to construct an HTTP client for API calls.

This would be our client's blueprint:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import retrofit2.http.GET

internal interface CatFactClient {
    @GET("fact")
    suspend fun fact(): CatFactResponse
}

@JsonIgnoreProperties(ignoreUnknown = true)
internal data class CatFactResponse(
    val fact: String,
)
Enter fullscreen mode Exit fullscreen mode

You'll observe a solitary function, fact(), geared towards API communication, yielding a CatFactResponse. Weve intentionally omitted the length field, as highlighted earlier.

Connecting everything together

With foundational pieces in place, let's merge them to manifest our core library logic.

Commence by configuring an instance of the HTTP client:

private const val API_BASE_URL = "https://catfact.ninja/"

private var client = Retrofit.Builder()
    .baseUrl(API_BASE_URL)
    .client(OkHttpClient.Builder().build())
    .addConverterFactory(JacksonConverterFactory.create(objectMapper))
    .build()
    .create<CatFactClient>()
Enter fullscreen mode Exit fullscreen mode

Now, our business logic:

override suspend fun get(numberOfFacts: Int): Set<Fact> =
    coroutineScope {
        (1..numberOfFacts).map {
            async { client.fact() }
        }.awaitAll()
            .map { Fact(it.fact) }
            .toSet()
    }
Enter fullscreen mode Exit fullscreen mode

This function concurrently dispatches numberOfFacts calls to the API, awaits all replies, translates them into the domain model, and returns a fact set. We utilize Set over List since the API doesn't assure unique responses.

Inspect the finalized version of the code here.

Happy Cat

This piece isn't tailored to guide library publishing. However, if you're inclined, relevant settings can be found here.

Our library's artifact, version 0.1.0, is available on GitHub packages and awaits your exploration. An updated version (0.2.0) offers mock implementations, bypassing internet prerequisites with a few breaking changes. Nevertheless, the core remains unaltered.


I hope you enjoyed this journey and learned something new. If you want to stay updated with my latest thoughts and ideas, feel free to register for my newsletter. You can also find me on LinkedIn or Twitter. Let's stay connected and keep the conversation going!


Conclusion

This article provides a comprehensive guide on building a client library for fetching cat facts from an API, using Kotlin, Gradle, Retrofit, and Jackson. The library is designed to handle multiple concurrent API calls and return a set of unique facts. The implementation of OpenTelemetry and service instrumentation is planned for the next parts of this series, ultimately enhancing the observability of the service.

Acknowledgments

  • Mariusz Sotysiak - for moral support, review, and suggestions while writing this series of articles.

Top comments (0)