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 true)

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.