Unconstant Conjunction A personal blog

Three Useful Endpoints for Any Plumber API

The Plumber package is a popular way to make R models or other code accessible to others with an HTTP API. It’s easy to get started using Plumber, but it’s not always clear what to do after you have a basic API up and running.

This post shares three simple endpoints I’ve used on dozens of Plumber APIs to make them easier to debug and deploy in development and production environments: /_ping, /_version, and /_sessioninfo.

Most Plumber users will be familiar with the special plumber.R files that can be used to generate an API. However, for the examples in this post it is convenient to add them programmatically. Programmatic endpoints are less user-friendly, but also less magic.

The “Ping” or “Healthcheck” Endpoint

Healthcheck endpoints (often called /status, /healthz, or my personal favourite, /_ping) give an outside observer the answer to a simple question: is the API up and running?

To implement it, just return an empty “OK”:

srv <- plumber::plumb("plumber.R")
srv$handle("GET", "/_ping", function(req, res) {
  res$setHeader("Content-Type", "application/json")
  res$status <- 200L
  res$body <- ""
  res
})
# ...
srv$run()

This allows you to check if you API is up and running from a browser (by visiting http://myapi.host/_ping) or the comfort of your R console (with httr::GET(), say). It also makes your API visible to any monitoring tools used by teams in your organisation to keep tabs on running services.

A healthcheck endpoint also integrates well with many tools in the Docker ecosystem, which is a common way to deploy Plumber APIs. For example, a Dockerfile can contain a HEALTHCHECK directive. Here’s one from an internal API:

# Check the /_ping endpoint every 30 seconds.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
  CMD curl --silent --fail http://0.0.0.0:8080/_ping || exit 1

These healthcheck commands are understood by the Docker daemon and used to determine whether containers are “healthy” (or “unhealthy” and in need of a restart) – which you can see in commands like docker ps:

$ docker ps --format "table {{.ID}}\t{{.RunningFor}}\t{{.Status}}"
CONTAINER ID        CREATED             STATUS
7cedb56515a8        6 hours ago         Up 6 hours (healthy)

If you don’t want to modify the image directly, the Docker compose file format (also used by Docker Swarm) supports adding healthchecks as well:

healthcheck:
  test: curl --silent --fail http://0.0.0.0:8080/_ping || exit 1
  interval: 30s
  timeout: 5s

Both Docker Swarm and Kubernetes automatically use the healthchecks to understand if a deployment was successful, and also to implement zero-downtime rolling updates.

Over in the Kubernetes camp, you can also use the built-in liveness (and/or readiness) probes in Pods and Deployments. Here is one from ours:

livenessProbe:
  httpGet:
    path: /_ping
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 30
  timeoutSeconds: 5

Kubernetes’s built-in probes have the advantage of not requiring the Docker image to bundle curl or a Windows equivalent, too.

On a related note, Kelsey Hightower has a talk on healthcheck endpoints where he suggests that applications use them to report various kinds of “readiness” measures, for example whether they can successfully connect to an underlying database.

The “Version” Endpoint

Much like the healthcheck endpoint, which answers the question “is my API running?", it is often very useful to know “what version of my API is running?".

As with the healthcheck endpoint, this can be added programmatically to any existing Plumber API:

version <- "2.1.1"

srv$handle("GET", "/_version", function(req, res) {
  res$setHeader("Content-Type", "application/json")
  res$status <- 200L
  res$body <- sprintf('{"version":"%s"}', version)
  res
})

Of course, this requires you to version your API to begin with, but you should be doing that anyway.

I find myself checking these endpoints all the time to verify that an API has deployed correctly, particularly for rolling deployments, which might take some time to converge to the new version.

The “SessionInfo” Endpoint

The last group of questions I find myself asking about Plumber APIs are variations on “what version of R (or a package) is it using?".

R users often post the result of the sessionInfo() command when filing bugs for a package or posting on Stackoverflow, because it can help point to issues that only show up or newer or older versions of R itself or any of the packages in use. This information is similarly useful when debugging bad or inconsistent behaviour with a Plumber API.

Unfortunately, it can be hard to decipher R’s sessionInfo() results when directly serialised to JSON, because they contain deeply nested DESCRIPTION files for each loaded package.

Instead, I recommend using the sessioninfo package:

srv$handle("GET", "/_sessioninfo", function(req, res) {
  res$setHeader("Content-Type", "application/json")
  res$status <- 200L
  res$body <- jsonlite::toJSON(
    sessioninfo::session_info(), auto_unbox = TRUE, null = "null"
  )
  res
})

Coda

Implementing these three endpoints in all our internal APIs has helped diagnose and solve countless problems. But I’ve glossed over how to actually implement them in practice.

The approach used by my team is to have a common entrypoint.R script (recently reimplemented as an internal package) used across all our Plumber APIs. This ensures that even new projects will get these endpoints automatically.

Custom entrypoint support is an extremely useful but almost totally undocumented feature of Plumber. Hopefully more examples can be shared in the future to remedy this.

comments powered by Disqus