Overview
iocaine can be configured in a number of dialects: TOML, JSON, YAML, and KDL. The other formats are primarily kept as a way for programmatic configuration, for the simple reason that those formats are easier to generate than KDL. Nevertheless, serialized formats are also documented - just not here, we’re dealing only with KDL here, because that’s the most human-friendly.
Out of the box, iocaine has a built-in configuration, one you can view by running iocaine show config. This configuration can be augmented or replaced, and iocaine can assemble its configuration from multiple files and directories. See the Configuration merging section for details!
The configuration is not hot-reloaded! If it is changed, iocaine has to be restarted for the new configuration to take effect.
Syntax
The syntax described in this documentation is based on KDL, and we aim to document the syntax in enough detail, through examples, that you won’t need to read the KDL spec. Lets dive in!
The main purpose of the configuration is to define “servers” (any number of them), each of which will act (mostly) independently. Apart from servers, the configuration lets us declare “handlers” (again: any number of them) - handlers are used by some of the server types to serve requests. It also lets us configure an initial seed.
There are multiple server types: HTTP servers, HAProxy SPOA servers, and Prometheus servers. The first two require a handler to serve request, and can use a Prometheus server for their metrics. All of these have a name, but the names live in separate namespaces: we can have a http-server, a prometheus-server, a haproxy-spoa-server, and a handler all share the same name, without conflicts. We’ll show an example at the end of this document.
Handlers are not shared between servers, but Prometheus servers can be. We’ll talk about these specifics later.
The format also recognizes comments: start a comment with //, and the rest of the line will be ignored. It is also possible to comment out entire nodes by prefixing them with /-. We’ll show a few examples below.
Because configuration can be split into multiple files, it is possible and allowed for partial configurations to have incomplete nodes, as long as the final, merged configuration has all required parts. This too will be shown in the examples section.
initial-seed or initial-seed-file
The “initial seed” controls the initial seed used by iocaine’s random number generator. The random number generator will be seeded by this value, and certain properties of the request: the requested host, the requested path, and all query parameters serialized.
As long as the same seeds are in play, iocaine will generate the same random numbers for any given request. As such, if we wish to change the generated garbage from time to time, changing the initial seed is the easiest way to accomplish that. We can either set a static string (which we’d change every so often):
initial-seed "Trans Rights are Human Rights"
Or we can point it to a file, to externalize the randomization. For example, on a NixOS system, setting it to /run/current-system/boot.json will result in the initial seed changing every time a new configuration is deployed.
// Set the initial seed to the contents of our system's boot configuration.
initial-seed-file "/run/current-system/boot.json"
These nodes can be repeated, but repeats will override previous declarations, and only the last one will be effective. There is no priority difference between initial-seed and initial-seed-file: whichever has been declared last will be used.
If not set, defaults to the empty string.
instance-id & state-directory
Important
Added in iocaine 3.1.0.
instance-id is an identifier for this particular instance of iocaine. This can be - and is used as such by the built-in script - used to generate URLs or other data that poisons them, and can be easily identified later. If not specified, iocaine will generate a random UUID, and store it in a file in state-directory, so that it persists across restarts.
The state-directory is where iocaine can store persistent state. It defaults to the current working directory.
state-directory "/var/lib/iocaine"
instance-id "CVsfEhWMnA4umxJfJ9VQ"
The built-in script will derive the poison ID from this, using the instance-id and the name of the server it was instantiated for.
http-server
An HTTP server is one where iocaine will serve HTTP requests, and use a handler to decide their fate. The handler shall return a HTTP 421 response if the request should be served normally. Any other status indicates that iocaine’s response should be served instead of the real content. We can have any number of HTTP servers, listening on different ports, with their own initial seed (if so desired), and using metrics provided by a particular Prometheus server.
For each HTTP server we wish to run, we need to declare them with the http-server stanza. This requires a single positional argument: the name of the HTTP server. It also requires a bind address, and a handler. We’ll look at those in detail in a bit, but first, the minimal boilerplate required for a HTTP server declaration:
http-server "name" {
bind "127.0.0.1:42069"
use handler-from="name"
}
This declares that we want to start a HTTP server bound to port 42069 on 127.0.0.1, using a handler named “name”.
Available settings
Each http-server can be configured independently, and they support the following settings:
initial-seed or initial-seed-file
These work exactly the same as the global initial-seed, but the seed is bound to this server only. If not set, the server will use the global seed.
bind
The bind setting controls where iocaine will bind this server to. It can be an IP address and a port (both IPv4 and IPv6 are supported, of course), a UNIX domain socket, or - on Linux - an Abstract Unix Domain socket or a systemd-provided socket file descriptor. The address to bind to must be the first argument to this setting, and the setting is required for each http-server declaration.
The setting accepts a unix-socket-access named property, but it is only used if the bind address is a unix domain socket. It’s value can be any one of: “owner”, “group”, or “everyone”. This property controls the access restrictions on the unix domain socket. If unset, the permissions on the socket will not be explicitly set, and will inherit it from umask.
Lets see a few examples!
http-server "tcp" {
bind "127.0.0.1:42069" // binds to TCP port 42069 on 127.0.0.1
}
http-server "uds" {
// Binds to /run/iocaine/uds.sock, and sets permissions to 0660.
bind "/run/iocaine/uds.sock" unix-socket-access="group"
}
http-server "auds" {
// Binds to the "iocaine.auds.sock" Abstract Unix Socket.
// This feature is only available under Linux.
bind "@iocaine.auds.sock"
}
http-server "systemd" {
// "binds" to a socket provided by systemd's socket activation feature
bind "sd-listen:iocaine-systemd"
}
use
The use setting allows us to connect our http-server with a prometheus-server, and it is also used to set the configuration of the handler our server will use. It accepts only named properties, and does not allow positional arguments.
Two named properties are available: metrics, the name of a prometheus-server to connect to, and send metrics to (this property is optional), and handler-from, a handler declaration to instantiate our handler from (this property is required).
Each http-server will run its own handler, but the handler declaration can be shared. On the other hand, metrics servers are shared between http-server instances. Lets see a few examples!
http-server "default" {
use handler-from="default"
}
This will result in a HTTP server using the default handler. Unless otherwise declared, that’s going to be iocaine’s built-in request handler.
http-server "default" {
use metrics="default" handler-from="default"
}
haproxy-spoa-server "default" {
use metrics="default" handler-from="default"
}
prometheus-server "default"
Now this is a more interesting declaration! It sets up three servers: a http-server, a haproxy-spoa-server, and a prometheus-server. Both the http-server and the haproxy-spoa-server will use same prometheus-server, but they’ll each have their own instance of the request handler, instantiated from the "default" declaration.
haproxy-spoa-server
An haproxy-spoa-server starts an agent that can be used with HAProxy’s Stream Processing Offload Engine. How to configure HAProxy to use it falls outside of the scope of this documentation, and will be explored elsewhere. We’re only dealing with the iocaine-side of it here.
Similar to http-server, we can have any number of declared haproxy-spoa-servers, and the declaration can be configured exactly the same as http-server. See its documentation for details.
prometheus-server
The purpose of a prometheus-server is two-fold: it provides an endpoint for Prometheus to scrape, and also provides a metrics registry for http-serverss and haproxy-spoa-servers to use.
We can have any number of prometheus-servers, each with a different name, and a different bind address, both of which are required. As such, the minimal declaration necessary for a prometheus-server is this:
prometheus-server "name" {
bind "address"
}
A more complete example would look like the following:
prometheus-server "main" {
bind "127.0.0.1:42042"
persist-path "/var/lib/iocaine/main.metrics.json"
persist-interval "10min"
}
This declares a server to listen on port 42042 of 127.0.0.1, persist its metrics to /var/lib/iocaine/main.metrics.json every 10 minutes (and during shutdown).
Available settings
Apart from the bind setting, which has the same syntax as in the http-server’s case, two other options are available: persist-path and persist-interval.
persist-path
If set, persist-path should be the path to a file the prometheus-server being declared can save its metrics to. Saving the metrics allow handlers to load the saved metrics, so they aren’t lost between restarts. If not set (which is the default), metrics will not be saved.
If a path is set, metrics will be persisted periodically, at persist-interval intervals, and when shutting iocaine down (or restarting it).
persist-interval
The persist-interval setting controls how often the declared prometheus-server will save its metrics to the file given through persist-path. If no path is configured, this setting is ignored. Unless configured otherwise, it defaults to one hour.
The interval can be specified in a human-readable format, like “1h” (1 hour, which is also a valid and recognised interval itself), “10m” (10 minutes). See this list of units for the list of recognised units.
Periodic saving cannot be turned off at this time, only set to an unreasonably long interval like a thousand years.
declare-handler
Handlers are at the core of iocaine, they’re responsible for deciding what to do with any incoming request. iocaine does not function without them. How they work, and what they can do, is outside of the scope of this document, we’re only dealing with their declarations here.
Handlers are used by http-servers and haproxy-spoa-servers, but each server will use its own instance of a handler. They can share the template the handler is instantiated from, but each will run its own copy.
To declare such a template, we use the declare-handler stanza. Like most other things in iocaine’s configuration, it requires a single positional argument: its name.
declare-handler "name" {
// configuration
}
Available properties
Apart from the name, the declare-handler stanza - unlike other settings - accepts a number of properties:
path
If specified, path must point to a directory containing the request handler. If not specified, iocaine will use the default handler.
language
The programming language the handler is written in. Can be either of roto (the default), lua, or fennel. The default handler, used when no path is set is only available in roto and lua.
If using fennel as the language, compiler must be set too.
compiler
When using a handler written in Fennel, a compiler must be specified. This must be a path to Fennel’s compiler, not to the command-line tool!
Configuration for the handler
The declare-handler stanza allows setting custom options, which will be made available to the handler. This lets us configure the handler from within iocaine’s own configuration files! Unlike other parts of the configuration, handler configuration is limited to key-value pairs, and does not allow named properties. A short example demonstrates this best:
declare-handler "test" path="/usr/share/iocaine/handlers/test" language="roto" {
should-do-something #true
data-file-path "/some/path"
template {
min-words 10
max-words 42
}
// The following, however, are not allowed:
/-props foo=bar
/-something prop=name {
another-setting
}
}
Configuration merging
iocaine comes with a limited, but sensible default configuration out of the box. This configuration can be augmented, or replaced. iocaine can be pointed to configuration files, or directories via the --config-path command-line option. This option can be specified multiple times, and can point to either a file, or a directory.
If pointed at a file, then all previous configuration (including the built-in configuration) will be discarded, and the file will serve as the new base. If pointed at a directory, iocaine will look for configuration files within (recursing into subdirectories), in any of the supported formats (TOML, JSON, YAML, and KDL), and will load them in alphabetical order.
For example, to replace the built-in configuration, and augment that with snippets, we can do:
iocaine --config-path /etc/iocaine/config.kdl \
--config-path /etc/iocaine/defaults/config.d \
--config-path /etc/iocaine/config.d
Configuration parts are merged in the same order as given on the command-line. Taking the example above, iocaine will first load /etc/iocaine/config.kdl, then it will merge all files under /etc/iocaine/defaults/config.d into it (in alphabetical order within that directory), then it will merge the files under /etc/iocaine/config.d (again, in alphabetical order within that directory).
When merging configuration, dictionaries are recursively merged, arrays and other types will use the incoming value. To demonstrate this with an example, lets say we have two files, /etc/iocaine/config.kdl, and /etc/iocaine/config.d/00-demo.kdl, with the following contents:
// /etc/iocaine/config.kdl
http-server default {
bind "127.0.0.1:42069"
use handler-from=default
}
declare-handler default {
ai-robots-txt-path "data/robots.json"
unwanted-visitors Perplexity
sources {
training-corpus "data/corpus/1984.txt"
}
}
// /etc/iocaine/config.d/00-demo.kdl
http-server default {
bind "0.0.0.0:42069"
}
declare-handler default language=lua {
ai-robots-txt-path "data/ai.robots.txt-robots.json"
unwanted-visitors Googlebot ClaudeBot
sources {
wordlists "data/words.txt"
}
}
In this case, http-server default will have its properties merged, because it is a dictionary. It’s bind setting will get replaced, because it’s a string type. Similarly, declare-handler default also merges! The language=lua property will replace the implicit default of roto (because properties are dictionaries), ai-robots-txt-path and unwanted-visitors will also be replaced, because one’s a string, and the other is an array. The sources setting, on the other hand, is a dictionary, and will get merged.
Thus, we’ll end up with the following merged configuration:
http-server default {
bind "127.0.0.1:42069"
use handler-from=default
}
declare-handler default language=lua {
ai-robots-txt-path "data/ai.robots.txt-robots.json"
sources {
training-corpus "data/corpus/1984.txt"
wordlists "data/words.txt"
}
unwanted-visitors Googlebot ClaudeBot
}
This sounds more confusing than it really is. Hopefully.
Examples
While the syntax described above is hopefully detailed and comprehensive enough, nothing beats trial and error, and experimenting. To get you up and running, in this section, we’ll provide a number of examples to get started with.
Default handler with HTTP & Prometheus servers
Lets create a complete configuration, all in one file, with every required parameter set. Save it somewhere, and run iocaine --config-path /path/to/this/file.kdl
http-server default {
bind "127.0.0.1:42069"
use handler-from=default metrics=default
}
prometheus-server default {
bind "127.0.0.1:42042"
persist-path "/var/lib/iocaine/default.metrics.json"
persist-interval "10m"
}
declare-handler default {
/-ai-robots-txt-path "/opt/iocaine/share/data/ai.robots.txt-robots.json"
}
This sets up a HTTP server to listen on port 42069 of 127.0.0.1, use the default handler, and the Prometheus server set up to listen on port 42042 of 127.0.0.1, with metrics persisted to a file every ten minutes.
While it also has a handler declared, that handler is the default one, and no additional configuration is applied to it, because the ai-robots-txt-path setting is commented out. We do need it declared, however, because this configuration overrides the built-in configuration, and a handler must be defined. We just defined it without a path property, to make iocaine use the built-in handler.
Splitting the configuration file
It’s also possible to split the configuration into multiple files. This is useful both for organizing a larger configuration, and when you’re packaging iocaine (for example) and wish to ship a default that users of the package can augment.
Like in the previous example, we’ll start our configuration with a file that’ll override the built-in configuration. However, we will omit some of the required parts from it, and fill them out in separate snippets.
Lets write the following to a file, say, to config.kdl:
// config.kdl
http-server default {
use handler-from=default metrics=default
}
prometheus-server default
declare-handler default
There are no binds set up for neither the http-server, nor the prometheus-server! How will that work?
Lets create a directory, config.d, and a file within it: 00-binds.kdl.
// config.d/00-binds.kdl
http-server default {
bind "127.0.0.1:42069"
}
prometheus-server default {
bind "127.0.0.1:42042"
}
We’re also going to want those metrics persisted, so lets create config.d/01-persist-metrics.kdl:
// config.d/01-persist-metrics.kdl
prometheus-server default {
persist-path "/var/lib/iocaine/default.metrics.json"
persist-interval "10m"
}
Now, lets see how iocaine merges these!
# iocaine --config-path config.kdl --config-path config.d show config
initial-seed ""
http-server default {
bind "127.0.0.1:42069"
use handler-from=default metrics=default
}
prometheus-server default {
bind "127.0.0.1:42042"
persist-path "/var/lib/iocaine/default.metrics.json"
persist-interval "10m"
}
declare-handler default language=roto
Wonderful!