FreeBSD: Home NAS, part 11 – extended моніторинг з додатковими експортерами
0 (0)

Автор |  09/02/2026
Click to rate this post!
[Total: 0 Average: 0]

В попередньому пості FreeBSD: Home NAS, part 10 – моніторинг з VictoriaMetrics та Grafana налаштували VictoriaMetrics, node_exporter, Grafana, зробили базовий дашборд і базові алерти.

Тепер до цього хочеться додати  трохи більше моніторингу – бачити дані по CPU/RAM процесів, інформацію по SMART та ZFS.

Все, що тут написав – додав в репозиторій setevoy2/nas-monitoring: там і скрипти, і Grafana dashboard.

Установка кастомних експортерів

Не всі експортери мають порти або є в репозиторії – тому опишу, як зробив власний “pseudo FreeBSD port”.

Установка ZFS exporter

Репозиторій експортеру – zfs_exporter.

Взагалі в портах є інший експортер – py-prometheus-zfs, але я вже зробив з цим, і вийшло наче непогане – і цікаве – рішення, тому збережу в блог те, як це налаштував.

І те саме рішення використав для go-ecoflow-exporter.

Отже, у нас є GitHub репозиторій експортеру, в репозиторії є релізи, де можна скачати готовий білд – але не всі підтримують готові файли для FreeBSD.

Зато у всіх є код, і більшість сервісі на Go – тому їх легко зібрати самому.

Ідея доволі проста:

  • скрипт build.sh: завантажити або оновити код експортеру
  • Makefile: для запуску build.sh та копіювання файлу самого експортеру і його rc.d скрипта

Створюємо структуру каталогів:

# mkdir -p /opt/exporters/zfs_exporter/{rc.d,src}

Створення build.sh

Додаємо скрипт – він буде клонити репозиторій і виконувати go build.

Не став заморачуватись з файлом VERSION – просто беремо master бранч, і білдимо з нього.

Пишемо /opt/exporters/zfs_exporter/build.sh:

#!/bin/sh

# stop on first error
set -e

BASE_DIR="/opt/exporters/zfs_exporter"
SRC_DIR="${BASE_DIR}/src/zfs_exporter"
BIN_NAME="zfs_exporter"
REPO_URL="https://github.com/pdf/zfs_exporter.git"

# ensure src dir exists
mkdir -p "${BASE_DIR}/src"

# clone repo if it does not exist
if [ ! -d "${SRC_DIR}" ]; then
    git clone "${REPO_URL}" "${SRC_DIR}"
fi

cd "${SRC_DIR}"

# always update sources
git pull

# build binary into BASE_DIR
go build -o "${BASE_DIR}/${BIN_NAME}"

Задаємо права на запуск:

# chmod +x /opt/exporters/zfs_exporter/build.sh

Запускаємо:

# /opt/exporters/zfs_exporter/build.sh

Перевіряємо:

# ll /opt/exporters/zfs_exporter/src/
total 8
drwxr-xr-x  6 root setevoy  512B Feb  9 13:32 zfs_exporter

І бінарний файл:

# file /opt/exporters/zfs_exporter/zfs_exporter 
/opt/exporters/zfs_exporter/zfs_exporter: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD) ...

Можна запустити його для перевірки:

# /opt/exporters/zfs_exporter/zfs_exporter
time=2026-02-09T13:42:30.119+02:00 level=INFO source=zfs_exporter.go:40 msg="Starting zfs_exporter" version="(version=, branch=, revision=7af698c8844864eb1e724ed08c47e5a7b4bbcc53)"
time=2026-02-09T13:42:30.120+02:00 level=INFO source=zfs_exporter.go:41 msg="Build context" context="(go=go1.24.12, platform=freebsd/amd64, user=, date=, tags=unknown)"
...
time=2026-02-09T13:42:30.120+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

Тепер додаємо Makefile, аби простіше буде робити установку і апдейти.

Створення Makefile

Вся логіка апдейту і білда буде в build.sh, а в Makefile просто викликаємо сам скрипт і виконуємо установку файлів в систему:

# simple makefile for zfs_exporter

PREFIX=/usr/local
BIN_NAME=zfs_exporter
BASE_DIR=/opt/exporters/zfs_exporter

.PHONY: build install clean

build:
  $(BASE_DIR)/build.sh

install:
  install -m 0755 $(BASE_DIR)/$(BIN_NAME) $(PREFIX)/bin/$(BIN_NAME)

clean:
  rm -f $(BASE_DIR)/$(BIN_NAME)

Можемо використовувати його як:

# cd /opt/exporters/zfs_exporter
# make build

// або
# make -C /opt/exporters/zfs_exporter build

// або
# make -f /opt/exporters/zfs_exporter/Makefile build

Тепер у нас така структура експортеру:

# tree /opt/exporters/zfs_exporter
/opt/exporters/zfs_exporter
├── Makefile
├── build.sh
├── rc.d
├── src
│   └── zfs_exporter
│       ├── CHANGELOG.md
│       ├── LICENSE
...
│       └── zfs_exporter.go
└── zfs_exporter

Запускаємо для перевірки make build:

# make build 
/opt/exporters/zfs_exporter/build.sh
Already up to date.

І make install:

# make install 
install -m 0755 /opt/exporters/zfs_exporter/zfs_exporter /usr/local/bin/zfs_exporter

Перевіряємо ще раз, вже з /usr/local/bin:

# /usr/local/bin/zfs_exporter
...
time=2026-02-09T13:44:59.651+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

Створення rc.d скрипта

Пишемо файл /opt/exporters/zfs_exporter/rc.d/zfs_exporter:

#!/bin/sh

# PROVIDE: zfs_exporter
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name="zfs_exporter"
rcvar="zfs_exporter_enable"

command="/usr/local/bin/zfs_exporter"
pidfile="/var/run/${name}.pid"

# defaults (override in rc.conf)
: ${zfs_exporter_enable:=no}
: ${zfs_exporter_listen_address:=":9134"}
: ${zfs_exporter_extra_flags:=""}
: ${zfs_exporter_log_file:="/var/log/zfs_exporter.log"}

start_cmd="${name}_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"

zfs_exporter_start()
{
    echo "Starting ${name}"
    /bin/sh -c "${command} --web.listen-address=${zfs_exporter_listen_address} ${zfs_exporter_extra_flags} > ${zfs_exporter_log_file} 2>&1 & echo \$! > ${pidfile}"
}

zfs_exporter_stop()
{
    if [ -f "${pidfile}" ]; then
        kill "$(cat ${pidfile})"
        rm -f "${pidfile}"
        echo "Stopped ${name}"
    else
        echo "${name} is not running"
    fi
}

zfs_exporter_status()
{
    if [ -f "${pidfile}" ] && kill -0 "$(cat ${pidfile})" 2>/dev/null; then
        echo "${name} is running (pid $(cat ${pidfile}))"
    else
        echo "${name} is not running"
        return 1
    fi
}

load_rc_config $name
run_rc_command "$1"

Задаємо права на запуск:

# chmod +x /opt/exporters/zfs_exporter/rc.d/zfs_exporter

Додаємо його копіювання до /usr/local/etc/rc.d в таргет install нашого Makefile:

# simple makefile for zfs_exporter

PREFIX=/usr/local
BIN_NAME=zfs_exporter
BASE_DIR=/opt/exporters/zfs_exporter
RC_DIR=$(PREFIX)/etc/rc.d

.PHONY: build install clean

build:
  $(BASE_DIR)/build.sh

install:
  install -m 0755 $(BASE_DIR)/$(BIN_NAME) $(PREFIX)/bin/$(BIN_NAME)
  install -m 0755 $(BASE_DIR)/rc.d/$(BIN_NAME) $(RC_DIR)/$(BIN_NAME)

clean:
  rm -f $(BASE_DIR)/$(BIN_NAME)

Запускаємо:

# make install 
install -m 0755 /opt/exporters/zfs_exporter/zfs_exporter /usr/local/bin/zfs_exporter
install -m 0755 /opt/exporters/zfs_exporter/rc.d/zfs_exporter /usr/local/etc/rc.d/zfs_exporter

Додаємо до /etc/rc.conf:

# sysrc zfs_exporter_enable="YES"
zfs_exporter_enable:  -> YES

Запускаємо сервіс:

# service zfs_exporter start
Starting zfs_exporter

Перевіряємо статус:

# service zfs_exporter status
zfs_exporter is running (pid 91712)

Перевіряємо лог:

# tail -f /var/log/zfs_exporter.log 
time=2026-02-09T13:52:07.033+02:00 level=INFO source=zfs_exporter.go:40 msg="Starting zfs_exporter" version="(version=, branch=, revision=7af698c8844864eb1e724ed08c47e5a7b4bbcc53)"
...
time=2026-02-09T13:52:07.033+02:00 level=INFO source=tls_config.go:354 msg="Listening on" address=[::]:9134
...

І метрики:

# curl -s localhost:9134/metrics | grep zfs_ | head -5
# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children.
# TYPE zfs_dataset_available_bytes gauge
zfs_dataset_available_bytes{name="nas",pool="nas",type="filesystem"} 2.723599372288e+12
zfs_dataset_available_bytes{name="nas/backups",pool="nas",type="filesystem"} 2.723599372288e+12
zfs_dataset_available_bytes{name="nas/media",pool="nas",type="filesystem"} 2.723599372288e+12

VMAgent описував в попередньому пості в частині Установка VMAgent – додаємо до нього збір метрик з нового експортеру.

Редагуємо /usr/local/etc/prometheus/prometheus.yml, додаємо новий таргет:

...
  - job_name: "zfs_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:9134"
...

Рестартимо vmagent і перевіряємо метрики в VictoriaMetrics:

Exporter upgrade з make

Тут у нас просто кілька кроків:

# service zfs_exporter stop

# make -C /opt/exporters/zfs_exporter build

# make -C /opt/exporters/zfs_exporter install

# service zfs_exporter start

Без VERSION чи тегів або релізів – все максимально просто.

Можна додати до Makefile:

...

upgrade:
        service zfs_exporter stop || true
        $(MAKE) build
        $(MAKE) install
        service zfs_exporter start

Установка go-ecoflow-exporter

Аналогічно зробив для go-ecoflow-exporter, але тут є пару відмінностей, бо треба передати пачку змінних оточення.

Для цього в скрипті /opt/exporters/ecoflow_exporter/rc.d/ecoflow_exporter додаємо export:

...
ecoflow_exporter_start()
{
    echo "Starting ${name}"

    export \
        EXPORTER_TYPE \
        ECOFLOW_EMAIL \
        ECOFLOW_PASSWORD \
        ECOFLOW_DEVICES \
        MQTT_DEVICE_OFFLINE_THRESHOLD_SECONDS \
        DEBUG_ENABLED \
        METRIC_PREFIX \
        SCRAPING_INTERVAL \
        PROMETHEUS_ENABLED \
        PROMETHEUS_PORT

    /bin/sh -c "${command} --listen ${ecoflow_exporter_listen_address} ${ecoflow_exporter_extra_flags} > ${ecoflow_exporter_log_file} 2>&1 & echo \$! > ${pidfile}"
}
...

А значення – через /etc/rc.conf.d і файл /etc/rc.conf.d/ecoflow_exporter:

# cat /etc/rc.conf.d/ecoflow_exporter
ecoflow_exporter_enable="YES"

EXPORTER_TYPE="mqtt"
...
PROMETHEUS_PORT="2112"

Ім’я файлу в rc.conf.d має збігатись з іменем в rc.d, тобто:

# cat /usr/local/etc/rc.d/ecoflow_exporter | grep name=
name="ecoflow_exporter"

Додаємо новий таргет до VMAgent:

...
  - job_name: "ecoflow_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:2112"
...

Установка smartctl_exporter

Він є в портах – smartctl_exporter і в репозиторії FreeBSD, тому просто встановлюємо з pkg:

# pkg install -y smartctl_exporter

Додаємо запуск:

# sysrc smartctl_exporter_enable="YES"

Запускаємо сервіс:

# service smartctl_exporter start

Перевіряємо метрики:

# curl -s 127.0.0.1:9633/metrics | grep smart | grep -v \# | head -5
smartctl_device{ata_additional_product_id="unknown",ata_version="ACS-4 T13/BSR INCITS 529 revision 5",device="ada0" ...

Додаємо VMAgent таргет:

...
  - job_name: smartctl_exporter
    static_configs:
      - targets:
          - 127.0.0.1:9633
...

Але smartctl_exporter по дефолту збирає інформацію тільки для /dev/ada* – а в мене ще є NVMe.

В rc.d скрипті експортеру є glob:

...
smartctl_exporter_devices (string):           Shell glob (like /dev/ada[0-9]) for all devices
...

Задаємо диски в /etc/rc..conf:

...
smartctl_exporter_devices="/dev/ada* /dev/nvme0"
...

NVMe просто ім’я явно, без glob, бо буде тягнути розділи типу /dev/nvme0ns1.

Node Exporter та Textfile

Скільки років користуюсь node_exporter в Kubernetes – не знав, що у нього є така цікава можливість як Textfile, є навіть цілі колекції, див. node-exporter-textfile-collector-scripts.

Що хотілось бачити у себе – це інформацію по CPU/RAM процесів, і спочатку думав просто взяти process_exporter, як це робив в Kubernetes (див. Kubernetes: моніторинг процесів з process-exporter).

Але process_exporter не працює з FreeBSD, бо він всю інформацію збирає з каталогу /proc, який включити можна – але все одно буде не Linux-proc.

Тому зробив інакше – через node_exporter та textfiles, через які збираю температуру CPU та інформацію по процесам.

Перевіряємо, що директорію, з якої директорі node_exporter читає файли з метриками:

root@setevoy-nas:~ # ps aux | grep node_exporter
nobody            2511   0.0  0.2   1264028  13012  -  S    Fri17       1:17.17 /usr/local/bin/node_exporter --web.listen-address=:9100 --collector.textfile.directory=/var/tmp/node_exporter

--collector.textfile.directory=/var/tmp/node_exporter – Окей, включено, можна додавати дані.

Скрипт для CPU temperature, CPU та RAM процесами

Пишемо скрипт /usr/local/bin/process_resources_exporter.sh:

#!/bin/sh

OUT="/var/tmp/node_exporter/process_resources.prom"

# reset file once
{
  echo "# HELP local_process_memory_bytes resident memory size per process"
  echo "# TYPE local_process_memory_bytes gauge"

  echo "# HELP local_process_cpu_percent cpu usage percent per process"
  echo "# TYPE local_process_cpu_percent gauge"

  echo "# HELP node_cpu_temperature_celsius CPU/system temperature via ACPI"
  echo "# TYPE node_cpu_temperature_celsius gauge"
} > "$OUT"

# ----
# top processes by memory (aggregate by process name)
# ----
ps -axo comm,rss | grep -vE '^(idle|pagezero|kernel)' | awk '
{
  gsub(/ /,"_",$1)
  mem[$1] += $2
}
END {
  for (p in mem)
    printf "local_process_memory_bytes{process=\"%s\"} %d\n", p, mem[p] * 1024
}
' | sort -k2 -nr | head -10 >> "$OUT"

# ----
# top processes by cpu (aggregate by process name)
# ----
ps -axo comm,%cpu | grep -vE '^(idle|pagezero|kernel)' | awk '
{
  gsub(/ /,"_",$1)
  cpu[$1] += $2
}
END {
  for (p in cpu)
    printf "local_process_cpu_percent{process=\"%s\"} %.2f\n", p, cpu[p]
}
' | sort -k2 -nr | head -10 >> "$OUT"

# ----
# cpu temperature via ACPI
# ----
TEMP=$(sysctl -n hw.acpi.thermal.tz0.temperature 2>/dev/null | tr -d 'C')

if [ -n "$TEMP" ]; then
  echo "node_cpu_temperature_celsius $TEMP" >> "$OUT"
fi

Запускаємо для тесту:

# chmod +x /usr/local/bin/process_resources_exporter.sh

# /usr/local/bin/process_resources_exporter.sh

Перевіряємо файл /var/tmp/node_exporter/process_resources.prom  з метриками:

# cat /var/tmp/node_exporter/process_resources.prom 
# HELP local_process_memory_bytes resident memory size per process
# TYPE local_process_memory_bytes gauge
# HELP local_process_cpu_percent cpu usage percent per process
# TYPE local_process_cpu_percent gauge
# HELP node_cpu_temperature_celsius CPU/system temperature via ACPI
# TYPE node_cpu_temperature_celsius gauge
local_process_memory_bytes{process="jellyfin"} 456441856
...
local_process_cpu_percent{process="syslogd"} 0.00
node_cpu_temperature_celsius 27.9

І експортер з нього імпортує метрики:

Додаємо скрипт в cron, раз на хвилину буде достатньо:

* * * * * /usr/local/bin/process_resources_exporter.sh

Скрипт для freebsd_update та pkg updates

Аналогічно можна додати інформацію по доступним апдейтам.

Скрипт /usr/local/bin/updates_exporter.sh:

#!/bin/sh

OUT="/var/tmp/node_exporter/updates.prom"

# header
{
  echo "# HELP node_freebsd_update_available FreeBSD base system updates available (1=yes, 0=no)"
  echo "# TYPE node_freebsd_update_available gauge"

  echo "# HELP node_pkg_updates_available Number of pkg updates available"
  echo "# TYPE node_pkg_updates_available gauge"
} > "$OUT"

# --------
# freebsd-update
# --------
FREEBSD_UPDATES=0

# freebsd-update fetch returns:
# - exit 0 even if no updates
# - but prints "No updates needed to update system"
if freebsd-update fetch | grep -q "No updates needed"; then
  FREEBSD_UPDATES=0
else
  FREEBSD_UPDATES=1
fi

echo "node_freebsd_update_available $FREEBSD_UPDATES" >> "$OUT"

# --------
# pkg updates
# --------
# pkg version -l "<" lists outdated packages
PKG_UPDATES=$(pkg version -l "<" 2>/dev/null | wc -l | tr -d ' ')

# fallback safety
PKG_UPDATES=${PKG_UPDATES:-0}

echo "node_pkg_updates_available $PKG_UPDATES" >> "$OUT"

Робимо chmod, додаємо в cron – але тут раз на годину, і перевіряємо метрики в VictoriaMetrics:

Скрипт для Services health

Тут перевіряємо, що сервіси запущені, якщо все ОК – пишемо в метрику service_up значення 1, якщо проблеми – то 0.

Скрипт /usr/local/bin/service_status_exporter.sh:

#!/bin/sh

DIR="/var/tmp/node_exporter"
OUT="$DIR/service_status.prom"
TMP="$DIR/service_status.prom.tmp"

# ------------
# helpers
# ------------

check_proc() {
  pgrep -f "$1" >/dev/null 2>&1
}

check_port() {
  host="$1"
  port="$2"
  nc -z "$host" "$port" >/dev/null 2>&1
}

# ----------------
# write metrics (atomic)
# ----------------

cat <<EOF > "$TMP"
# HELP service_up Service availability status (1 = up, 0 = down)
# TYPE service_up gauge
EOF

# jellyfin
if check_port 127.0.0.1 8096; then
  echo 'service_up{name="jellyfin"} 1' >> "$TMP"
else
  echo 'service_up{name="jellyfin"} 0' >> "$TMP"
fi

# filebrowser
if check_port 127.0.0.1 8080; then
  echo 'service_up{name="filebrowser"} 1' >> "$TMP"
else
  echo 'service_up{name="filebrowser"} 0' >> "$TMP"
fi

# grafana
if check_port 127.0.0.1 3000; then
  echo 'service_up{name="grafana"} 1' >> "$TMP"
else
  echo 'service_up{name="grafana"} 0' >> "$TMP"
fi

# victoria-metrics
if check_port 127.0.0.1 8428; then
  echo 'service_up{name="victoria-metrics"} 1' >> "$TMP"
else
  echo 'service_up{name="victoria-metrics"} 0' >> "$TMP"
fi

# sshd (port only)
if check_port 127.0.0.1 22; then
  echo 'service_up{name="sshd"} 1' >> "$TMP"
else
  echo 'service_up{name="sshd"} 0' >> "$TMP"
fi

# nfsd (process only)
if check_proc nfsd; then
  echo 'service_up{name="nfsd"} 1' >> "$TMP"
else
  echo 'service_up{name="nfsd"} 0' >> "$TMP"
fi

# atomic replace
mv "$TMP" "$OUT"

І метрики:

Grafana дашборд – нові графіки

А тепер це все додаємо в Grafana.

Графік CPU процесами:

topk(5, local_process_cpu_percent)

Аналогічно для пам’яті:

Статус дисків зі SMART:

smartctl_device_smart_status

Стан сервісів:

І все разом тепер:

Залишилось додати алерти – і, в принципі, моніторинг готовий.

Loading