# Writing Munin Plugins

## Protocol

A plugin is called with no arguments to fetch values, and with `config` to describe the graph. Optional: `autoconf` (print `yes`/`no`), `suggest` (print modes for wildcard plugins).

### Config output

Global attributes describe the graph:

| Attribute | Purpose | Example |
|---|---|---|
| `graph_title` | Title above graph | `graph_title CPU usage` |
| `graph_vlabel` | Y-axis label | `graph_vlabel percent` |
| `graph_category` | Grouping in web UI | `graph_category system` |
| `graph_args` | Passed to rrdgraph | `graph_args --base 1000 -l 0` |
| `graph_scale` | Enable SI scaling | `graph_scale no` |
| `graph_info` | Description below graph | `graph_info CPU usage by type` |

Per-field attributes:

| Attribute | Purpose | Example |
|---|---|---|
| `field.label` | Legend label (required) | `cpu.label CPU` |
| `field.type` | GAUGE, COUNTER, DERIVE | `cpu.type DERIVE` |
| `field.draw` | LINE1, LINE2, AREA, AREASTACK | `mem.draw AREASTACK` |
| `field.min` | Minimum valid value | `cpu.min 0` |
| `field.max` | Maximum valid value | `cpu.max 100` |
| `field.warning` | Warning threshold | `cpu.warning 80` |
| `field.critical` | Critical threshold | `cpu.critical 95` |
| `field.info` | Field description | `cpu.info Percent CPU used` |
| `field.cdef` | RPN expression transform | `bytes.cdef bytes,8,*` |
| `field.negative` | Mirror another field below axis | `up.negative down` |

### Value output

```
fieldname.value 42
otherfield.value 3.14
```

Use `U` for unknown: `fieldname.value U`

## Wildcard plugins

A single plugin script handles multiple instances via its symlink name. The plugin parses `basename $0` to determine what to monitor.

Example: `if_` plugin symlinked as `if_eth0`, `if_wlan0`. The script strips its prefix to get the interface name.

Wildcard plugins should implement `suggest` to list valid instances:

```sh
if [ "${1:-}" = "suggest" ]; then
    ls /sys/class/net/ | grep -v '^lo$'
    exit 0
fi
```

## Multigraph plugins

Emit `multigraph <graph_id>` before each graph's output:

```sh
#!/bin/sh
if [ "${1:-}" = "config" ]; then
    echo "multigraph service_users"
    echo "graph_title Users"
    echo "graph_category myservice"
    echo "graph_vlabel count"
    echo "total.label Total users"

    echo "multigraph service_uptime"
    echo "graph_title Uptime"
    echo "graph_category myservice"
    echo "graph_vlabel days"
    echo "uptime.label Uptime"
    exit 0
fi

echo "multigraph service_users"
echo "total.value $(get_user_count)"

echo "multigraph service_uptime"
echo "uptime.value $(get_uptime_days)"
```

The master must negotiate `cap multigraph` before `list` will show these plugins. This happens automatically during normal polling.

## Graph args reference

Common `graph_args` values:

- `--base 1000`: decimal SI units (1k = 1000)
- `--base 1024`: binary units (1Ki = 1024), use for bytes
- `-l 0`: lower limit 0 (graph won't go below)
- `--upper-limit 100`: upper limit (for percentages)

## Magic markers

Add these comments for `munin-node-configure` auto-detection:

```sh
#%# family=auto          # or contrib, manual
#%# capabilities=autoconf suggest
```

## Testing during development

```bash
# Set MUNIN_LIBDIR if plugin uses plugin.sh helpers
export MUNIN_LIBDIR=/usr/share/munin   # or /usr/lib/munin on Arch

# Test directly
chmod +x ./myplugin
./myplugin config
./myplugin

# Test through munin-run (sets up full environment)
cp myplugin /etc/munin/plugins/
munin-run myplugin config
munin-run myplugin
```

## Common patterns

### Monitoring a CLI tool's output

```sh
#!/bin/sh
case ${1:-} in
    config)
        echo "graph_title My Service Stats"
        echo "graph_category myservice"
        echo "graph_vlabel count"
        echo "connections.label Active connections"
        exit 0 ;;
esac
echo "connections.value $(myservice-ctl status | awk '/connections:/ {print $2}')"
```

### Monitoring an API endpoint

```sh
#!/bin/sh
case ${1:-} in
    config)
        echo "graph_title API Response Time"
        echo "graph_category network"
        echo "graph_vlabel ms"
        echo "response.label Response time"
        exit 0 ;;
esac
ms=$(curl -s -o /dev/null -w '%{time_total}' http://localhost:8080/health | awk '{printf "%.0f", $1 * 1000}')
echo "response.value $ms"
```

### Monitoring container resource usage (Incus/LXD)

Query the Incus API via `incus query` for per-container stats. Run as `user root` in plugin-conf.d. Use DERIVE for CPU (cumulative nanoseconds → rate), AREASTACK for memory.
