Linkerd is built on a modular plugin system so that individual components may be swapped out without needing to recompile. This also allows anyone to build custom plugins that implement functionality specific to their needs. This guide will show you how to write your own custom Linkerd plugin, how to package it, and how to install it in Linkerd.
In this guide we will write a custom HTTP response classifier. However, the ideas in this guide apply just as well for writing custom identifiers, namers, name interpreters, protocols, or any other Linkerd plugin.
All the code for the plugin in this guide is available on GitHub. I recommend keeping the code open in another window while you read this guide so that you can follow along.
We will write a custom HTTP response classifier that classifies responses based on a special response header instead of the HTTP response code. If the header's value is “success” we will treat the response as a success, if it is “retry” we will treat it as a retryable failure, and otherwise we will treat it as a non-retryable failure.
We will describe how to write the plugin, how to build and package it, and how to install it in Linkerd.
While Linkerd itself is written in Scala, its plugins can be written in Scala or Java. To demonstrate this, we will write our plugin in Java. For our plugin to be functional, we will need 3 things: the response classifier itself, a config class, and a config initializer. All plugins follow this pattern of having the class that implements the business logic, a config class, and a config initializer.
is the response classifier itself. Response classifiers must extend
PartialFunction[ReqRep, ResponseClass]. Each plugin type has a different
interface that it must implement. For example, namer plugins must implement
and identifier plugins must implement
Next we need a class that defines the structure of the config block for this
plugin and constructs the response classifier. We will call this
HeaderClassifierConfig must implement
ResponseClassifierConfigs are deserialized from the response classifier section
of the Linkerd config by Jackson and its
public members are populated by the corresponding JSON (or YAML) properties. (In
Scala this would be a case class.) In our case we have one public member called
headerName, which defines the name of the response header to use.
ResponseClassifierConfig we must also implement a method called
mk() which constructs the response classifier.
A config initializer is a special class that Linkerd loads at startup. It tells
Linkerd about config classes it can use. We create a config initializer called
This class must define a
configId and a
configClass. When Linkerd parses a
config block with a
kind property, it looks for a config initializer with that
configId and attempts to deserialize the block as an instance of the
HeaderClassifierInitializer tells Linkerd that when it finds a
responseClassifier block with
kind: io.buoyant.headerClassifier, it should
deserialize that block as a
Finally, in order for Linkerd to be able to dynamically load the config
initializer at startup, we must register it with the service loader. To do this
simply create a resource file called
and add the fully qualified class name of the config initializer to that file.
Build & package
We use sbt to build our plugin and the assembly sbt plugin to package it into a
jar. Here is the
file for the project. Note that we can mark any Linkerd dependencies as
“provided”. This means that those dependencies will be provided by Linkerd and
do not need to be included in the plugin jar. Similarly, Linkerd will provide
the Scala standard libraries, so we can exclude those from the jar as well by
includeScala = false.
Build the plugin jar by running:
To install this plugin with Linkerd, simply move the plugin jar into Linkerd's
plugin directory (
$L5D_HOME/plugins). Then add a classifier block to the
router in your Linkerd config:
routers: - ... responseClassifier: kind: io.buoyant.headerClassifier headerName: status
If you run Linkerd with
-log.level=DEBUG then you should see a line printed
at startup that indicates the HeaderClassifierInitializer has been loaded:
LoadService: loaded instance of class io.buoyant.http.classifiers.HeaderClassifierInitializer for requested service io.buoyant.linkerd.ResponseClassifierInitializer
Trying it out
Now that we have our plugin in the plugins directory, let's try it out. Start
Linkerd with this simple config that sends all requests to
routers: - protocol: http dtab: /svc/* => /$/inet/localhost/8888 responseClassifier: kind: io.buoyant.headerClassifier headerName: status servers: - ip: 0.0.0.0 port: 4140
Then we'll start a simple server on port 8888 that responds with the status header indicating success:
while true; do echo -e "HTTP/1.1 200 OK\r\nstatus: success\r\n" | nc -i 1 -l 8888; done
Now let's issue a request:
curl -v localhost:4140
By checking Linkerd's metrics, we can see that this request was classified as a success:
curl -s localhost:9990/admin/metrics.json?pretty=1 | grep -E 'srv.*(success|failure)' "rt/http/srv/0.0.0.0/4140/success" : 1,
Now let's restart our server and have it set the header to “failure”:
while true; do echo -e "HTTP/1.1 200 OK\r\nstatus: failure\r\n" | nc -i 1 -l 8888; done
And issue another request:
curl -v localhost:4140
Now when we check Linkerd's metrics, we'll see that this request was classified as a failure:
curl -s localhost:9990/admin/metrics.json?pretty=1 | grep -E 'srv.*(success|failure)' "rt/http/srv/0.0.0.0/4140/failures" : 1, "rt/http/srv/0.0.0.0/4140/failures/com.twitter.finagle.service.ResponseClassificationSyntheticException" : 1, "rt/http/srv/0.0.0.0/4140/success" : 1,
If you have any questions about using or developing Linkerd plugins, or would like to share what you've created, please drop into the Linkerd public Slack. We hope to see you soon!