Getting started¶
Installation¶
System wide on Linux, with a systemd script:
$ curl -o - https://raw.githubusercontent.com/Tamvera/ducted/refs/heads/master/scripts/install.sh | sh
With docker:
$ docker run -i -v ./duct.yml:/duct/duct.yml ghcr.io/tamvera/ducted:latest
With pip (Requires Python 3.11 or newer):
$ pip install ducted
Quick start¶
Create a duct.yml configuration file and run ductd:
interval: 1.0
ttl: 60.0
outputs:
- output: duct.outputs.prometheus.Prometheus
port: 9100
sources:
- service: cpu
source: duct.sources.linux.basic.CPU
interval: 1.0
warning: { cpu: "> 0.5" }
critical: { cpu: "> 0.8" }
- service: memory
source: duct.sources.linux.basic.Memory
interval: 10.0
- service: disk
source: duct.sources.linux.basic.DiskFree
interval: 60.0
ductd -c duct.yml
This starts Duct with a Prometheus scrape endpoint at
http://0.0.0.0:9100/metrics. Use -v / --verbose to enable
debug logging. The daemon can also be started as a module:
python -m duct -c duct.yml
Outputs¶
Outputs define where events go. Configure one or many - events are
fanned out to all of them by default. Give each a name to enable
Routing sources.
Prometheus¶
Runs an embedded HTTP server exposing a Prometheus scrape endpoint:
outputs:
- output: duct.outputs.prometheus.Prometheus
port: 9100 # default
metric_path: metrics # default → /metrics
prefix: duct_ # prepended to every metric name
InfluxDB 3¶
Writes events via the InfluxDB 3 line-protocol HTTP API:
outputs:
- output: duct.outputs.influxdb3.InfluxDB3
url: http://localhost:8086
database: mymetrics
token: my-token
NATS¶
Publishes events to NATS subjects. Supports core NATS and JetStream,
and three serialisation formats: senml-json (default),
senml-cbor, and json:
outputs:
- output: duct.outputs.nats.Nats
servers:
- nats://localhost:4222
format: senml-cbor
jetstream: true
Elasticsearch¶
outputs:
- output: duct.outputs.elasticsearch.ElasticSearch
url: http://127.0.0.1:9200/
Graphite¶
outputs:
- output: duct.outputs.graphite.Graphite
server: localhost
port: 2003
Other supported outputs: duct.outputs.opentsdb.OpenTSDB,
duct.outputs.bosun.Bosun, duct.outputs.riemann.RiemannTCP,
duct.outputs.riemann.RiemannUDP.
Routing sources¶
Sources can be routed to specific named outputs rather than all outputs:
outputs:
- output: duct.outputs.prometheus.Prometheus
name: prom
port: 9100
- output: duct.outputs.influxdb3.InfluxDB3
name: influx
url: http://localhost:8086
database: mymetrics
token: my-token
sources:
- service: cpu
source: duct.sources.linux.basic.CPU
interval: 1.0
route: prom # single output
- service: memory
source: duct.sources.linux.basic.Memory
interval: 10.0
route: # or a list
- prom
- influx
Set default_route globally to route unspecified sources to a named
output. Set route: '*' on a source to send to all outputs regardless
of the default.
Using sources¶
Sources are Python classes that produce events on a timer. Add them to the
sources list with any source-specific config.
Common options available on every source:
service |
required |
Metric name prefix (dot-separated sub-metrics are appended automatically) |
interval |
required |
Poll interval in seconds (float) |
ttl |
optional |
Metric time-to-live in seconds |
hostname |
optional |
Override the default system FQDN |
tags |
optional |
Comma-separated list of tags |
route |
optional |
Output name or list of names to route to |
watchdog |
optional |
Restart the source if it stalls (set |
Example - CPU with thresholds:
sources:
- service: cpu
source: duct.sources.linux.basic.CPU
interval: 1.0
warning: { cpu: "> 0.5" }
critical: { cpu: "> 0.8" }
Sources that return multiple metrics append a prefix. The Ping source
returns both latency and loss:
sources:
- service: googledns
source: duct.sources.network.Ping
interval: 60.0
destination: 8.8.8.8
critical:
googledns.latency: "> 100"
googledns.loss: "> 0"
State triggers¶
warning and critical expressions can use regular expressions as
keys to match sources that produce per-device metrics:
sources:
- service: network
source: duct.sources.linux.basic.Network
interval: 5.0
critical:
network.\w+.tx_packets: "> 1000"
The expression is evaluated against event.metric. Matching events
have their state overridden.
Receiving telemetry¶
Sources are not limited to polling - they can run embedded servers and receive events pushed from other systems.
Riemann TCP server¶
Accept events from other Riemann-speaking agents:
sources:
- service: riemann-in
source: duct.sources.riemann.RiemannTCP
port: 5555
sFlow collector¶
Listen for sFlow datagrams from network switches:
sources:
- service: sflow
source: duct.sources.sflow.sFlow
port: 6343
NATS subscriber¶
Subscribe to NATS subjects and route the events onward:
sources:
- service: nats-in
source: duct.sources.nats.Nats
servers:
- nats://localhost:4222
topics:
- "metrics.>"
format: senml-json
Munin node¶
Poll a local or remote Munin node and translate its plugins into Duct events:
sources:
- service: munin
source: duct.sources.munin.MuninNode
host: localhost
interval: 60.0
SSH remote checks¶
Any source with ssh = True supports running transparently on a remote
host. SSH connections are pooled per host/user/key combination.
Set credentials globally and override per-source as needed:
ssh_username: monitor
ssh_keyfile: /etc/duct/id_ed25519
sources:
- service: load
source: duct.sources.linux.basic.LoadAverage
use_ssh: true
ssh_host: web01.example.com
interval: 30.0
ssh_knownhosts_file, ssh_username, ssh_keyfile,
ssh_password, and ssh_keypass can all be set globally or
per-source. An inline ssh_key YAML blob is also accepted.
Note
Duct does not perform host key verification by default.
Use blueprint macros to apply a toolbox of checks to many hosts without repeating config - see Blueprints and Toolboxes.
Writing a plugin¶
A source is a Python class. It can use any Python library, open any connection, or start any server:
from zope.interface import implementer
from duct.interfaces import IDuctSource
from duct.objects import Source
@implementer(IDuctSource)
class MySource(Source):
def __init__(self, *a, **kw):
Source.__init__(self, *a, **kw)
self.myopt = self.config.get('myopt', 'default')
async def get(self):
value = await fetch_something(self.myopt)
return self.createEvent('ok', 'My metric', value, prefix='mymetric')
Point to it in config by dotted import path:
sources:
- service: mything
source: mypackage.mysource.MySource
interval: 10.0
myopt: custom-value
An output follows the same pattern: subclass Output, implement
createClient for setup (start servers, open connections), and drain
the event queue - see Outputs for a walkthrough.