This manual documents sakana version 0.1.0, a lightweight monitor for GNU Taler exchanges.
Copyright © 2026 Taler Systems SA.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Affero General Public License, version 3 or any later version published by the Free Software Foundation.
sakana periodically asks every configured GNU Taler exchange for its /keys endpoint and reports anything that looks wrong: an unreachable host, a refused port, a broken TLS handshake, a TLS certificate that should have been renewed, an unexpected HTTP status, a timeout, a /keys response that does not parse, key material that has expired or is about to expire, and fees that are not configured for the period they must cover.
Reports are not sent anywhere by sakana itself. Instead, sakana forks and executes a command from its configuration and writes the report to the standard input of that command. Whether a report becomes a mail, a page, a line in a log or an entry in an incident tracker is therefore entirely up to the operator.
Failure counters live in memory. A restart of sakana starts every counter from zero, which means the first check after a restart reports an existing problem immediately.
Every error class of every exchange counts its failures separately. A denomination warning that has been pending for weeks can therefore not delay the report of an exchange that has just become unreachable.
While an exchange cannot be reached, nothing can be said about its keys. The key classes are then neither escalated nor reset, so a pending warning survives an outage and does not produce a second alert when the exchange comes back.
An exchange keeps publishing keys whose validity has ended. sakana reports expiry only when no usable key remains, which is the situation in which key rotation has actually stopped. Signing keys and denominations have separate warning horizons, since exchanges provision them months and days ahead respectively.
sakana is written in Go and uses only the standard library, including for TLS, HTTP and JSON.
sakana needs Go 1.22 or newer and nothing else. The optional makeinfo
program from texinfo renders this manual as info and html.
./bootstrap ./configure --prefix=/usr/local make make check sudo make install
./bootstrap checks the toolchain and installs a gofmt
pre-commit hook. ./configure is a hand-written script in the style
used throughout the GNU Taler code base; it accepts the usual directory
variables (--prefix, --bindir, --sysconfdir,
--mandir, …) and tool overrides such as GO=/opt/go/bin/go.
Run ./configure --help for the list.
make install installs
the binary,
the configuration, from the example file, if it does not exist yet,
the manual pages,
the systemd unit,
an example reporting command, and
this manual, the example configuration and the license.
make check runs the whole test suite. It needs neither network
access nor an installation. make check-live additionally checks the
public exchanges and does need network access. make help lists all
targets.
The configuration is an INI file. The manual page sakana.conf(5) documents every
option in reference form; this chapter explains how the options fit together.
A minimal configuration looks like this:
[sakana] FREQUENCY = 15 m TOLERANCE = 3 10 300 COMMAND = /usr/share/sakana/sakana-notify-mail ops@example.com [exchange-taler-ops] BASE_URL = https://exchange.taler-ops.ch/
The optional [sakana] section holds defaults. Every other section
describes one exchange, and its name — exchange-taler-ops here
— appears in every report and log line about that exchange. Each
exchange needs four values, either in its own section or as a default:
BASE_URL, FREQUENCY, COMMAND and TOLERANCE.
Durations use the Taler and GNUnet spelling: 5 s, 10m,
1 h, 1 d, 4d 32s. The units are us, ms,
s, m, h, d, w and y, with the long
forms (seconds, minutes, …) and the abbreviations
" and ' accepted as well.
The delay between two checks of one exchange is FREQUENCY randomized by
±5%. Several monitors watching the same exchange therefore drift apart
instead of hitting it in lockstep. The first check of every exchange happens
immediately when sakana starts.
TLS_RENEWAL_HORIZON (five days by default) is the point at which a TLS
certificate that has not been renewed becomes a report. Five days is a
deliberate compromise: certbot renews at 30 days remaining and retries twice a
day, so a certificate with less than five days left means that renewal has
been failing for weeks.
The key material has two horizons, because exchanges provision its two kinds on entirely different scales.
SIGNKEY_EXPIRATION_HORIZON (30 days by default) is the point at which
expiring online signing keys become a report. Exchanges sign those months
ahead—the two public exchanges are currently at two months and ten months
respectively—so 30 days of notice is both ample and easy to satisfy.
DENOMINATION_EXPIRATION_HORIZON (14 days by default) is the point at
which denominations that can no longer be withdrawn become a report. It is
shorter on purpose: denominations are rotated within days, and an exchange
publishes them only as far ahead as its LOOKAHEAD_SIGN reaches. The
demo exchange at https://exchange.demo.taler.net/ publishes them about
30 days ahead and dips to roughly three weeks between signing runs, so a
30 day horizon would warn about an exchange that rotates exactly as intended.
A horizon below the lookahead of the deployment still catches the real
failure, because a signing pipeline that has stopped lets the remaining
horizon shrink to zero.
Raise the denomination horizon if the exchange you monitor signs further ahead
and you want earlier warning; KEY_EXPIRATION_HORIZON sets both at once
if you want one knob.
Every check is one GET request to BASE_URLkeys. Along the
way, sakana evaluates each of its error classes and arrives at one of three
verdicts for it: ok, fail or unknown, the last meaning
that this check could not say anything about the class.
Name resolution, the TCP connection and the TLS handshake are performed by sakana itself rather than left to the HTTP client, so that each failure can be classified precisely:
dns-failureThe host name does not resolve. The report distinguishes a name that does not exist from a resolver that did not answer.
host-unreachableThe connection attempt was answered with neither a SYN-ACK nor a RST, or an ICMP host or network unreachable arrived.
port-unreachableThe connection was refused with a RST: something is listening on the host, but not on this port.
sakana validates the certificate chain itself, with the system trust store plus every root given with --trust. Because validation happens after the handshake, with the chain in hand, the report can name the certificate that is at fault even when it is rejected:
tls-handshakeThe handshake failed, or validation rejected the chain. The detail names the reason: a certificate that is not valid at this time, an unknown certification authority, a host name mismatch, a peer that did not speak TLS at all, or the TLS alert the peer sent.
tls-expiringThe handshake succeeded, but the certificate expires within
TLS_RENEWAL_HORIZON.
http-statusThe status was not 200 OK. The report includes the Taler error code and hint if the body carries them, and adds what a 502 or a 503 from /keys usually means.
http-timeoutThe connection was established, but no complete response arrived within
TIMEOUT.
keys-parseThe body is not a JSON object, or not an exchange /keys object: a
missing version, currency, master_public_key,
signkeys, denominations, global_fees or wire_fees
member, or a malformed timestamp, all land here.
Both key checks ask whether anything usable is left. Keys whose validity has ended stay in the response and are not by themselves a problem.
signkeys-expiredNo entry of signkeys has a stamp_expire in the future. The
exchange cannot sign anything and is effectively out of service.
signkeys-expiringNo entry of signkeys is valid beyond
SIGNKEY_EXPIRATION_HORIZON.
denoms-expiredNo denomination has a stamp_expire_withdraw in the future, so no coin
can be withdrawn any more. Denominations marked lost are not counted
as usable.
denoms-expiringNo denomination can be withdrawn beyond
DENOMINATION_EXPIRATION_HORIZON.
When the expired class fires, the expiring class of the same kind is left unknown instead of being reported as well: the worse report subsumes the milder one, and the milder counter neither escalates nor resets.
Fees must cover the period from now until the end of the current year, and, during December, until the end of the next year as well — by December an exchange has to have the fees of the coming year signed. sakana sorts the fee periods, walks them and reports the first moment that is not covered. Periods that end exactly when the next one starts are contiguous, which is how consecutive years are configured.
global-fees-missingglobal_fees does not cover the required period.
wire-fees-missingThe wire fees of at least one wire method in wire_fees do not cover it.
The report names the methods and their configured periods.
TOLERANCE is a sequence of counts. With 3 10 300:
Any check in which the class is ok resets the sequence to its beginning, so the next problem is again reported at the fourth consecutive failure. A check in which the class is unknown changes nothing.
Warnings use the same machinery as outages. A certificate that certbot failed to renew is therefore reported once, then on an increasingly sparse schedule, rather than on every single check.
When a check finds every class in order — the response was fetched and parsed and nothing failed — and at least one report had been made since the last restoration, sakana delivers one brief service restored report naming the classes that had been reported, and forgets them. Failures that were tolerated and never reported produce no restoration report, because nobody was told about them in the first place.
COMMAND is split into an argument vector the way a shell would split
it — single quotes, double quotes and backslashes work — and is
then executed directly. There is no shell, so a redirection in COMMAND
does nothing; write /bin/sh -c '…' if a shell is wanted.
The report is written to the standard input of the command. A command that
does not read its input is not an error: sakana ignores the resulting broken
pipe. The command runs in its own process group and is killed, with its
children, if it exceeds COMMAND_TIMEOUT.
The first line is a one-line summary suitable as a mail subject:
sakana ALERT https://exchange.example.com/: TLS certificate expiring
A blank line follows, then the headers:
Exchange: https://exchange.example.com/ Endpoint: https://exchange.example.com/keys Host: exchange.example.com Error class: tls-expiring Severity: warning Detected: 2026-11-03T21:30:00Z Occurrences: 4 consecutive failures of this class; 3 tolerated before this report Monitor: sakana 0.1.0 on monitor.example.com, configuration section [exchange-demo]
Then the detail of the class — for a certificate that is expiring, its subject, issuer, names and validity, and what to check about certbot — and finally a footer that says after how many further checks the class would be reported again.
The restoration report follows the same shape, with the summary
sakana RESTORED https://exchange.example.com/: service restored
For every report sakana writes one line to standard error:
2026-11-03T21:30:00Z ERROR ERROR https://exchange.example.com/ unexpected HTTP status [http-status] -- command ok after 7ms, output: "accepted"
It carries the severity, the exchange URL, the error class in two or three words, the class identifier and the result of the reporting command, including its exit status and its output. Output of the command is collapsed to one line and truncated, so that one event stays one line.
sakana [-c FILE] [-R FILE]... [-L LEVEL] [-1] [-t] [-v] [-h]
The options are documented in the manual page sakana(1). The two that matter
most in daily use are --test, which prints the schedule that results
from a configuration without contacting anything, and --once, which
checks every exchange once, reports as usual and exits non-zero if anything
was wrong.
The shipped unit runs sakana as the unprivileged user sakana with a
tight sandbox and logs to the journal:
systemctl enable --now sakana.service journalctl -u sakana -f
Note that the sandbox applies to the reporting command as well, since it is a child of sakana. A command that needs more than outgoing network access and a read-only file system needs the unit relaxed accordingly, which is why the unit is a plain file in /usr/lib/systemd/system and meant to be overridden with a drop-in.
A one-off check fits a cron job:
*/15 * * * * sakana --once --config /etc/sakana/sakana.conf
Keep in mind that each invocation starts with empty counters, so a tolerance
sequence cannot build up across invocations; use TOLERANCE = 0 in this
mode, or run sakana as a daemon.
make check runs unit tests for the configuration parser, the
duration parser, the tolerance state machine, the /keys analysis, the
error classification and the reporting command, and an integration suite that
drives the real sakana binary against a fake exchange process.
The fake exchange, tests/fakeexchange, serves a synthetic
/keys response for a named scenario. It mints its own certification
authority at start-up, writes the CA certificate to a file and issues its
server certificate from it, so the integration tests exercise real TLS without
touching the system trust store: sakana is told to trust that file with
--trust. The scenario can be changed while the process runs, by
writing another scenario name into the file named by -mode-file,
which is how the suite exercises failure sequences and the restoration report.
tests/fakeexchange -list-modes lists the scenarios.
A few failures cannot be produced with a real socket in a sandbox without becoming flaky — a name that does not resolve, a host that drops packets. Those are covered by unit tests that substitute the dialer and return the operating system error in question, so the classification is still tested end to end from the error to the report text.
make check-live checks the public exchanges
https://exchange.taler-ops.ch/ and
https://exchange.demo.taler.net/ and expects them to pass without a
report, with no horizon configured at all: defaults that warn about a
correctly operated exchange would be useless defaults. It needs network
access and is therefore not part of make check. The same
expectation is pinned down offline by a unit test that analyzes an exchange of
the demo exchange’s shape—denominations with a seven day window published
about 30 days ahead, signing keys two months ahead—and requires silence.
make deb builds the package with dpkg-buildpackage. The
packaging aims at a bit-identical result from the same source:
debian/rules exports SOURCE_DATE_EPOCH as the commit date of the
release, hard-coded in the file. contrib/update-source-date-epoch.sh
sets it to the date of the current HEAD, which is how it is refreshed at
release time.
CGO_ENABLED=0 keeps the C toolchain out, and -trimpath and
-buildvcs=false keep the build directory and the git state out of the
binary. The build cache is redirected into the build tree.
TZ=UTC and LC_ALL=C fix the environment, and
dh_builddeb clamps the member timestamps to
SOURCE_DATE_EPOCH.
make deb-reproducible builds the package twice in separate trees and
compares the checksums, which is the only real proof.
| Class | Severity | Reported when |
|---|---|---|
dns-failure | error | the host name does not resolve |
host-unreachable | error | no SYN-ACK and no RST, or ICMP unreachable |
port-unreachable | error | the connection was refused with a RST |
tls-handshake | error | the handshake or the certificate validation failed |
tls-expiring | warning | the certificate expires within TLS_RENEWAL_HORIZON |
http-status | error | /keys did not return 200 OK |
http-timeout | error | no complete response within TIMEOUT |
keys-parse | error | the response is not an exchange /keys object |
signkeys-expired | error | no signing key is valid any more |
signkeys-expiring | warning | no signing key is valid beyond SIGNKEY_EXPIRATION_HORIZON |
denoms-expired | error | no denomination can be withdrawn any more |
denoms-expiring | warning | no denomination can be withdrawn beyond DENOMINATION_EXPIRATION_HORIZON |
global-fees-missing | warning | global fees do not cover the required period |
wire-fees-missing | warning | wire fees of a method do not cover it |
The severity only affects the wording of the report and the level of the log line. Warnings and errors use the same failure counters and both block the restoration report.
| Jump to: | B C D F H I J R S T |
|---|
| Jump to: | B C D F H I J R S T |
|---|