Recording HikariCP Metrics in Clojure
Tags: database, analytics, clojure, hikaricp
Published: 2022-03-01 01:49:44

[HikariCP](https://github.com/brettwooldridge/HikariCP) is a popular JDBC connection pool, battle-tested and exhibiting good performance.  A Clojure wrapper does the things discussed in this post [here](https://github.com/tomekw/hikari-cp).  However, our project uses HikariCP directly.

Last year I needed to give us some additional insight into our application performance, and this turned out to be a short, fun exercise.  So, after I dug into HikariCP, it looked like these two interfaces must be implemented.

- [IMetricsTracker](https://github.com/brettwooldridge/HikariCP/blob/dev/src/main/java/com/zaxxer/hikari/metrics/IMetricsTracker.java)
- [MetricsTrackerFactory](https://github.com/brettwooldridge/HikariCP/blob/dev/src/main/java/com/zaxxer/hikari/metrics/MetricsTrackerFactory.java)

## Implementing the interfaces

Using the incredibly simple [clj-statsd](https://github.com/pyr/clj-statsd) library, we can write a small record to implement the `IMetricsTracker`.

```clojure
(def pool-metrics-prefix "my-pool")

(defn make-key [db-name key] (clojure.string/join \. [pool-metrics-prefix db-name key]))

(defrecord StatsdMetricsTracker [db-name pool-name pool-stats]
  IMetricsTracker
  (recordConnectionCreatedMillis
    [_ connection-created-millis]
    (statsd/gauge (make-key db-name "wait-millis") connection-created-millis))
  (recordConnectionAcquiredNanos
    [_ elapsed-acquired-nanos]
    (statsd/gauge (make-key db-name "wait-nanos") elapsed-acquired-nanos))
  (recordConnectionUsageMillis
    [_ elaspsed-borrowed-millis]
    (statsd/gauge (make-key db-name "usage") elaspsed-borrowed-millis))
  (recordConnectionTimeout [_]
    (statsd/increment (make-key db-name "timeout"))))
```

The previous code is the core of the implementation, with the `close` function omitted.  We will come back to that.

The most straightforward implementation of `MetricsTrackerFactory` would instantiate StatsdMetricsTracker and return it.

```clojure
(defrecord StatsdMetricsTrackerFactory [db-name]
  MetricsTrackerFactory
  (create [_ pool-name pool-stats]
    (->StatsdMetricsTracker db-name pool-name pool-stats)))
```

At this point, we have enough code to glue to our application and report the following stats:

- milliseconds required to create a connection
- nanoseconds needed to acquire a connection
- milliseconds a connection has been borrowed
- cumulative number of connections timed out

I will leave the application glue to the reader but feel free to take the following code as a starting point.

```clojure
(.setMetricsTrackerFactory datasource (->StatsdMetricsTrackerFactory db-name))
```

Gluing all the code in this section to your app should be sufficient to give you some helpful graphs:


![example of a helpful chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kd8lwvky5q7rme1kyouy.png)

There are more metrics we can grab.

## Polling for additional metrics

HikariCP will "push" the metrics discussed in the last section, but there are some additional metrics that you can access.  You will have to poll for these, however.  Unfortunately, these metrics are less valuable and more challenging to report than I expected at first.

Starting is easy enough.  Let's write a function to report these metrics.

```clojure
(defn log-pool-stats [db-name pool-stats]
  (try
    (statsd/gauge (make-key db-name "total-connnections") (.getTotalConnections pool-stats))
    (statsd/gauge (make-key db-name "idle-connections") (.getIdleConnections pool-stats))
    (statsd/gauge (make-key db-name "active-connections") (.getActiveConnections pool-stats))
    (statsd/gauge (make-key db-name "pending-connections") (.getPendingThreads pool-stats))
    (catch Exception e
      (log/warn e "the database pool closed while we were logging metrics"))))
```

`pool-stats` needs to be an instance of `StatsdMetricsTrackerFactory`.

The most frustrating novelty I discovered about HikariCP is that it does not call `create` on the metrics tracker factory until it performs a query.  Your solution to this problem may be different than mine - I made a `promise` to force `log-pool-stats` to wait.  It looks something like this.

```clojure
(def trackers (atom {}))

(defn log-pool-stats
  ([db-name]
  ;; The tracker is wrapped in a promise, so we do a blocking de-ref until it becomes available.
   (when-let [tracker (get @trackers db-name)]
     (when-let [pool-stats (:pool-stats @tracker)]
       (log-pool-stats db-name pool-stats))))
  ([db-name pool-stats]
   ...))

(defrecord StatsdMetricsTrackerFactory [db-name]
  MetricsTrackerFactory
  (create [_ pool-name pool-stats]
    (let [tracker (->StatsdMetricsTracker db-name pool-name pool-stats)]
      (deliver (get @trackers db-name) tracker)
      tracker)))
```

I have modified the `create` function to inject the `StatsdMetricsTracker` instance into `trackers`.  I have again omitted some code for brevity, but clearly, you will need to baby `trackers` a bit before `create` is called.  That is pretty easy to do with your glue code.

## Takeaways

This project took an evening, and I enjoyed working on it.  However, there turned out to be some funny state management problems related to closing all the resources cleanly and removing old data so that metrics tracking would play nice with our reloaded workflow.

We will be removing this code shortly because it provides little benefit compared to the APM metrics we get through datadog.

If someone has used `tomekw/hikari-cp` I would be happy to hear about your experience.  It's doubtless a better solution than this home-grown thing anyway.