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.