Preface

A substantial part of today’s modern service infrastructure involves delivering an application ( server / backend / service / micro-service / API ). The intention of this application could be anything - serving a rendered page, REST API, WebSocket stream, message queue worker etc.

While building the application is one challenge, the real boss-fight begins once the application is deployed to prod, and the team needs to support it. Supporting the application could be a simple task of figuring out which version of the service is deployed, or something more challenging such as mutating a configuration. Due to the ubiquity of these support tasks, teams often dive into building custom tooling for gaining observability and insights into said application. Building these tools has almost become a right of passage into modern application development; and after having built a few, I wanted to capture the nuances, design decisions and the lessons learned.


Spring Boot…?

In a tiny nutshell, Spring Boot is a framework to build applications in Java. Think Express in Node, Rails in Ruby, Django in Python (to an extent). The Spring project pieces together many popular Java projects into a palatable, yet extensible experience. While Spring Boot provides the base to build your applications, the rest of the Spring ‘eco-system’ provides answers for almost every possible type of integration - from Kafka to Kubernetes.

While Spring is a great solution for quickly standing-up applications, personally I’ve found it too be a bit to magical in its details and often requires some goofy workarounds if you don’t happen to agree with the ‘Spring way’. However, Spring’s ease-of-use, decent performance, and strong eco-system of integrations make it a strong candidate to build applications.


Spring Boot Actuator…?

Actuator is one such member of the previously mentioned Spring ecosystem. The module once installed into a Spring application, exposes a variety of REST endpoints, which can be programmatically queried and interacted with, in order to facilitate typical management related tasks of an application.

After setting it up, Actuator is available through the /actuator endpoint of your application. Here’s a sample…

$ curl -v localhost:8080/actuator
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+jsonF
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "_links": {
        "beans": {
            "href": "http://localhost:8080/actuator/beans",
            "templated": false
        },
        "env": {
            "href": "http://localhost:8080/actuator/env",
            "templated": false
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        "logfile": {
            "href": "http://localhost:8080/actuator/logfile",
            "templated": false
        },
        "loggers": {
            "href": "http://localhost:8080/actuator/loggers",
            "templated": false
        },
        "prometheus": {
            "href": "http://localhost:8080/actuator/prometheus",
            "templated": false
        },
        "scheduledtasks": {
            "href": "http://localhost:8080/actuator/scheduledtasks",
            "templated": false
        },
        "shutdown": {
            "href": "http://localhost:8080/actuator/shutdown",
            "templated": false
        },
        "threaddump": {
            "href": "http://localhost:8080/actuator/threaddump",
            "templated": false
        }
    }
}

The /actuator endpoint acts a ‘discovery’ endpoint and lists the available ‘Actuators’. Some of these actuators can be dead-simple read operations, such as the /health actuator which returns (you can guess)…

$ curl -v localhost:8080/actuator/health
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "status": "UP"
}

A notable aspect about the /health actuator, and to illustrate the value of Actuator in the Spring project - other components within your application can implement the API for submitting custom health indicators to the /health actuator, which in turn would show up as the updated response for /health. Various other Spring projects, such as Spring Kafka and Spring Data JPA, already implement this API thus making /health truly useful.

On the other hand, there are actuators with write operations, such as the /env actuator which lets you mutate the application’s env / configurations. This is how you would go about a totally common task of updating the db_password config in runtime -

$ curl \
    -X POST \
    -d '{"name":"db_password", "value":"hunter2"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/actuator/env

This again, is another “standard” feature that most other frameworks don’t tend to provide, leading teams to end up hacking together their own frankenstein implementations.

And /env isn’t even scratching the surface…

  • /threaddump is always handy when needed for that level of debugging.
  • /loggers allows you to mutate the logging level on a class-path level, thus letting you turn off noisy debug statements in runtime.
  • /beans for the Bean-heads (I don’t need to explain any further - you know who you are).

there is also my personal favorite: /logfile which literally streams you your log file.


All these great endpoints surely make Actuator a great debug tool… right? If you haven’t figured out the problem with Actuator as a debug tool… well it’s not - it’s an interface; and like any other interface, it needs a good client to drive it.


Spring Boot Actuator CLI…!

My previous job involved supporting ~12 applications written in Spring, and we had to manage them between three independent teams' sprints. Naturally, things were breaking and Actuator was used heavily to debug the cause. With 12 applications (and multiple instances of Dev / QA / Prod), in the heat of the moment, even introspecting the health of an application can a massive chore.

I would often see my co-workers wrestle with bash scripts, curl commands, jq queries and env variables to facilitate working with Actuator, however you can imagine that approach getting out of hand as the permutations of environments increase. While, there are some big name tools out in the space - REST clients such as Postman, Insomnia, Paw as the notable examples; none have hit the apex in -

  • ease of use / ease of setup
  • parsing of the responses / understanding what the responses mean
  • config storage / location of stored variables and credentials

In early 2021, I spent some time building spring-boot-actuator-cli - a command-line application to interact and visualize a Spring Boot application’s Actuator endpoint’s data.

Here’s the most basic usage - hitting the /health actuator of an application running on http://localhost:8080 -

# ./sba-cli health -U <base URL>
$ ./sba-cli health -U http://localhost:8080
┌─────────────────┐
│      HEALTH     │
├────────┬────────┤
│ status │ UP     │
└────────┴────────┘

With the arguments from the command, sba-cli figures out the right REST call to make, parses the response and prints it out - in a more human readable format.

Please excuse the text rendering on the blog; here are some screenshots of sba-cli in action


Inventory Management

To address the use-case of managing multiple applications, sba-cli allows the user to supply an Inventory. An Inventory can be defined in a config.yaml file, which sba-cli reads on init. A listing in the Inventory describes an instance of an application - defining the base URL, authorization etc. Here is a sample Inventory -

inventory:
  - name: demo-service
    baseURL: http://localhost:8080
    skipVerifySSL: true
    tags:
      - demo
      - local

  - name: demo-service-dev
    baseURL: https://demo-service-dev
    tags:
      - demo
      - dev

  - name: demo-service-prod
    baseURL: https://demo-service-prod
    tags:
      - demo
      - prod

  - name: auth-service-prod
    baseURL: https://auth-service-prod
    authorizationHeader: Basic YXJraXRzOmh1bnRlcjI=
    tags:
      - auth
      - prod

This Inventory describes 3 instances of the demo-service (running on localhost, dev, prod) and 1 instance of the auth-service (running only in prod).

After defining multiple services in your Inventory, a specific service can be referred to by passing its name rather than the URL…

# ./sba-cli info -S <name of a specific service>
$ ./sba-cli info -S demo-service
>>> demo-service
┌─────────────────────────────┐
│         SERVICE INFO        │
├──────────────┬──────────────┤
│ title        │ demo-service │
└──────────────┴──────────────┘
┌────────────────────────────────────────────────────────────┐
│                          GIT INFO                          │
├─────────────────┬──────────────────────────────────────────┤
│ branch          │ main                                     │
│ commit.time     │ 2021-03-24 01:18:38+0000                 │
│ commit.describe │ 0.0.3-6-gc6c4cdb-dirty                   │
│ commit.abbrev   │ c6c4cdb                                  │
│ commit.full     │ c6c4cdb3932d1b2f28b342fbeb1c3de1d724114e │
└─────────────────┴──────────────────────────────────────────┘

… and multiple services can be referred to as a comma-separated string. sba-cli will iterate and print the responses for each.

$ ./sba-cli health -S demo-service-dev,demo-service-prod
>>> demo-service-dev
┌─────────────────┐
│      HEALTH     │
├────────┬────────┤
│ status │ UP     │
└────────┴────────┘

>>> demo-service-prod
┌─────────────────┐
│      HEALTH     │
├────────┬────────┤
│ status │ UP     │
└────────┴────────┘

Inventory Tagging

Another usage that allows for bulk actions in complicated inventories, is with Tags. Each Inventory entry can have a list of string tags associated with it. During runtime, the user can pass a query tag (multiple as a comma-separated string) and sba-cli will match the Inventory appropriately.

For example, to query all prod services -

$ ./sba-cli health -T prod
>>> auth-service-prod
┌─────────────────┐
│      HEALTH     │
├────────┬────────┤
│ status │ DOWN   │
└────────┴────────┘

>>> demo-service-prod
┌─────────────────┐
│      HEALTH     │
├────────┬────────┤
│ status │ UP     │
└────────┴────────┘

Collaboration through Git

A key motivation for the Inventory file mechanism was for using Git to manage the file, allowing the file to be collaboratively updated. The approach would be to commit the file to a ‘secrets’ repo, and extend from there with a suitable merge-flow approach to intake changes.

It means that access control to the repo is outsourced to whatever is available, which may not be acceptable in all cases. However, sba-cli is distributed as a single binary, allowing automation to be built around it.


The next few sections dive into a few technical details of sba-cli…


Under the hood: new curl, who dis?

One of the integral pieces of sba-cli is the component that handles the HTTP calls. While that may seem banal, handling the entire HTTP lifecycle in a clean, yet customizable, manner is crucial for the effectiveness of the tooling. Similarly, corporate environments often introduce weird complications in the HTTP call (magic auth headers, uncommon proxy ports, questionable SSL certs), which must be accommodated somehow.

In the context of sba-cli, MakeHTTPCall is the central function that abstracts way the details of - setting up the HTTP client, awaiting the response, handling the errors etc. All entry-points to sba-cli are designed to gather the details and funnel them into MakeHTTPCall, with the function definition ending up being -

func MakeHTTPCall(
    requestMethod string,
    requestURL string,
    authorizationHeader string,
    rangeHeader string,
    skipVerifySSL bool,
    // ...
) (*http.Response, error) {
    // ...
}

The other side to this detail would be from the UI/UX perspective - sba-cli, being a command-line app, has to expose these configs as parameter flags for the user. Here are some of the flags returned from the mantext.

$ ./sba-cli health -h
Interface with /actuator/health

Usage:
  sba-cli health [flags]

Flags:
  -B, --actuator-base string   Base of the actuator endpoint (default "actuator")
  -H, --auth-header string     Authorization Header to use when making the HTTP call
  -h, --help                   help for health
      --skip-pretty-print      Skip any pretty printing
  -K, --skip-verify-ssl        Skip verification of SSL
  -S, --specific string        Name of a specific Inventory
  -U, --url string             URL of the target Spring Boot app
  -V, --verbose                Set whether to output verbose log

A best effort was made to align the flags with curl’s, so as to provide reasonable user experience and a “guessable” set of controls.


Under the hood: Dynamic structures? Never heard of her

After performing the HTTP call, sba-cli parses through the response and prints outs the relevant data in more more human readable format. The approach to the functionality is quite straight-forward - sba-cli traverses the response JSON structure, filtering for the relevant fields, and appending the prettified version to a Table struct which finally gets printed into stdout.

This task seemingly got complicated when dealing with GoLang’s structs - unlike JavaScript, GoLang insists on knowing the complete type definition when marshalling the HTTP response JSON into an usable GoLang struct. This makes it awkward for “dynamic” or custom keys in the struct, which is definitely possible with Actuator. For example, /env returns a dump of all environment and configurations, most of which would have custom keys.

I still wanted to solve this problem in GoLang and opted for the dynamic-struct library to traverse and manipulate the struct. An unfortunate side effect of this is that the code readability takes a huge hit. For example, here is how the /actuator response is parsed to extract all the hrefs from a map of Links (refer to the /actuator HTTP response at the beginning of the post) -

// build the dynamicstruct based on the response
reader := MakeDynamicStructReader(ActuatorInfoProperties{}, actuatorResponse)

// Extract a dictionary named "Links" map and iterate through each element
for _, link := range reader.GetField("Links").Interface().(map[string]interface{}) {

    var href string

    // Iterate through each element in the Link
    for v_k, v_v := range link.(map[string]interface{}) {

        // links[0].href
        if v_k == "href" {
            href = fmt.Sprintf("%v", v_v)
        }

    }

    t.AppendRow(table.Row{href, templated})

}

Even with my best tries at commenting this code, I still have a though time reading through it. While the code isn’t functionally doing much, there is a genuine syntactic overload, making it unwelcoming to work with. This would be one avenue that I’d love to explore further…


Closing Thoughts

sba-cli-health

With details touching various different topics of tech, the cross-cutting nature of sba-cli, and observability tooling in general, make it a great learning experience in engineering a solution.

Building tooling for humans can be challenging, exhausting, obtuse, but nonetheless - rewarding. To me, it’s satisfying to see people’s workflow improve, thus improving their effectiveness and impact.


Special thanks to @cyber_junkie 🙏