В попередньому пості 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
Стан сервісів:
І все разом тепер:
Залишилось додати алерти – і, в принципі, моніторинг готовий.
![]()








