Textfile collector в node_exporter — один из самых быстрых путей добавить свои метрики в Prometheus. Он читает файлы в формате Prometheus exposition из директории и подмешивает их в выдачу /metrics самого экспортера. Это удобно для скриптов на Bash/Python, одноразовых измерений и интеграций с системными утилитами, где не хочется поднимать отдельный HTTP‑сервис.
Когда уместен textfile collector
Классический экспортер — это демон с HTTP-эндпоинтом. Но часто нужно просто: «собери вывод одной команды и отдай как метрику». Для такого сценария textfile collector идеален:
- минимальные зависимости: достаточно положить файл в директорию и соблюсти формат;
- надёжность: зависшая команда не валит HTTP-стек;
- безопасность: нет нового TCP-порта, всё делает уже работающий
node_exporter; - простота деплоя: cron или systemd-timer.
Типичные кейсы: количество доступных обновлений пакетов, срок истечения SSL‑сертификата домена, число банов в Fail2ban, контроль версии деплоя, наличие backup‑файлов, кастомные бизнес‑счётчики с локальной машины.
Включаем textfile collector
Проверьте, с какими флагами запущен node_exporter. Нужны два флага:
--collector.textfile— включает коллектор;--collector.textfile.directory=/var/lib/node_exporter/textfile— директория с файлами метрик.
Создадим директорию и выдадим права пользователю, под которым работает экспортер (часто node_exporter):
sudo install -o node_exporter -g node_exporter -m 0750 -d /var/lib/node_exporter/textfile
Добавим drop-in к unit-файлу systemd, чтобы не переписывать пакетный сервис (дополните своими флагами, если они нужны):
sudo mkdir -p /etc/systemd/system/node_exporter.service.d
sudo tee /etc/systemd/system/node_exporter.service.d/textfile.conf > /dev/null << 'EOF'
[Service]
Environment=TEXTFILE_DIR=/var/lib/node_exporter/textfile
ExecStart=
ExecStart=/usr/bin/node_exporter --collector.textfile --collector.textfile.directory=${TEXTFILE_DIR}
EOF
sudo systemctl daemon-reload
sudo systemctl restart node_exporter
sudo systemctl status node_exporter
Проверьте, что коллектор активен и директория принята:
curl -s localhost:9100/metrics | grep ^node_textfile
Вы увидите метрики вида node_textfile_*, в том числе node_textfile_scrape_error и node_textfile_mtime_seconds. На собственном сервере или на наших VDS такой подход особенно удобен: минимум движущихся частей и быстрый онбординг.
Формат метрик: короткий ликбез
Файлы должны быть в формате Prometheus exposition (текстовый, одна метрика на строку). Рекомендуется расширение .prom. Пример:
# HELP sys_updates_available Количество доступных обновлений пакетов
# TYPE sys_updates_available gauge
sys_updates_available 3
# HELP cert_expiry_days Сколько дней до окончания SSL-сертификата
# TYPE cert_expiry_days gauge
cert_expiry_days{host="example.com"} 27
Базовые правила:
- имя — латиница, цифры,
_; начинать с буквы; используйте общий префикс (sys_,deploy_и т.п.); - единицы в имени:
_seconds,_bytes,_ratio,_count,_days; - метки в
{k="v"}, строки экранируйте:\\и\"; - строки
# HELPи# TYPEуказывайте один раз на имя метрики; - избегайте высокой кардинальности меток.
Метрики пишите атомарно: сначала во временный файл, затем перемещайте в рабочий. Так node_exporter никогда не прочитает «наполовину записанный» файл.
Bash: готовые рецепты
1) Количество доступных обновлений (APT/DNF)
Скрипт определяет пакетный менеджер и пишет число доступных обновлений. Работает быстро, не блокируя длительными апдейтами метаданных.
#!/usr/bin/env bash
set -euo pipefail
DIR="/var/lib/node_exporter/textfile"
OUT="${DIR}/sys_updates.prom"
TMP="$(mktemp)"
count_updates_apt() {
if command -v apt-get > /dev/null; then
apt-get -s upgrade 2>/dev/null | awk '/^[0-9]+ upgraded/{print $1}'
return 0
fi
return 1
}
count_updates_dnf() {
if command -v dnf > /dev/null; then
dnf check-update -q 2>/dev/null | awk 'BEGIN{c=0} /^[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\s/ {c++} END{print c}'
return 0
fi
if command -v yum > /dev/null; then
yum check-update -q 2>/dev/null | awk 'BEGIN{c=0} /^[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\s/ {c++} END{print c}'
return 0
fi
return 1
}
UPDATES=0
if count_updates_apt >"${TMP}.cnt"; then
UPDATES="$(cat "${TMP}.cnt")"
elif count_updates_dnf >"${TMP}.cnt"; then
UPDATES="$(cat "${TMP}.cnt")"
else
UPDATES=0
fi
{
echo "# HELP sys_updates_available Количество доступных обновлений пакетов"
echo "# TYPE sys_updates_available gauge"
echo "sys_updates_available ${UPDATES}"
} > "${TMP}"
install -o node_exporter -g node_exporter -m 0640 "${TMP}" "${OUT}"
rm -f "${TMP}" "${TMP}.cnt"
Заведите cron с блокировкой, чтобы скрипт не наслаивался:
# /etc/cron.d/sys-updates-metrics
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/15 * * * * root flock -n /run/sys-updates-metrics.lock /usr/local/bin/sys_updates_metrics.sh

2) Сколько дней до окончания SSL‑сертификата
Проверим публичный сертификат домена через openssl и дадим метрику _days. Скрипт работает для TCP:443; список доменов хранится рядом. Если вы управляете сертификатами для сайтов — посмотрите наши SSL-сертификаты для автоматизации и единого управления.
#!/usr/bin/env bash
set -euo pipefail
DIR="/var/lib/node_exporter/textfile"
OUT="${DIR}/cert_expiry.prom"
TMP="$(mktemp)"
LIST="/etc/ssl/domains.txt"
now() { date +%s; }
get_expiry_days() {
local host="$1"
local exp_ts
exp_ts="$(echo | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null | openssl x509 -noout -enddate | sed 's/notAfter=//')"
if ; then
echo -1
return 0
fi
local exp_epoch
exp_epoch="$(date -d "$exp_ts" +%s 2>/dev/null || true)"
if ; then
echo -1
return 0
fi
local diff
diff=$(( (exp_epoch - $(now)) / 86400 ))
echo "$diff"
}
{
echo "# HELP cert_expiry_days Сколько дней до окончания SSL-сертификата"
echo "# TYPE cert_expiry_days gauge"
while read -r h; do
&& continue
d="$(get_expiry_days "$h" || echo -1)"
echo "cert_expiry_days{host=\"$h\"} $d"
done < "$LIST"
} > "$TMP"
install -o node_exporter -g node_exporter -m 0640 "$TMP" "$OUT"
rm -f "$TMP"
Пример /etc/ssl/domains.txt:
# один домен на строку
example.com
api.example.org
Планируйте обновления чаще раза в сутки, если хотите алерты с запасом времени.
3) Суммарное количество активных банов Fail2ban
Метрика показывает общее число IP в бане во всех jails. Можно добавить метку jail, но следите за кардинальностью. Ниже — агрегированная версия. Если настраиваете почтовые jails, пригодится разбор в материале Fail2ban для Postfix/Dovecot.
#!/usr/bin/env bash
set -euo pipefail
DIR="/var/lib/node_exporter/textfile"
OUT="${DIR}/fail2ban_bans.prom"
TMP="$(mktemp)"
get_total_bans() {
local total=0
while read -r jail; do
&& continue
cnt="$(fail2ban-client status "$jail" 2>/dev/null | awk -F: '/Currently banned/ {gsub(/ /, "", $2); print $2}')"
cnt="${cnt:-0}"
total=$(( total + cnt ))
done < <(fail2ban-client status 2>/dev/null | awk -F: '/Jail list/ {print $2}' | tr "," "\n" | sed 's/ //g')
echo "$total"
}
TOTAL="$(get_total_bans || echo 0)"
{
echo "# HELP fail2ban_bans_total Общее число активных банов во всех jails"
echo "# TYPE fail2ban_bans_total gauge"
echo "fail2ban_bans_total ${TOTAL}"
} > "$TMP"
install -o node_exporter -g node_exporter -m 0640 "$TMP" "$OUT"
rm -f "$TMP"
Cron: как запускать безопасно
- используйте
flockдля эксклюзивного запуска; - настраивайте переменную
PATHв crontab или скрипте; - выполняйте от пользователя, который может писать в директорию textfile (или используйте
install -o -gпри атомарном перемещении); - разводите скрипты по разным .prom-файлам — не мешайте несвязанные метрики.
Шаблон записи в /etc/cron.d/:
*/5 * * * * root flock -n /run/<name>.lock /usr/local/bin/<script>.sh
Если предпочитаете systemd timers — логика та же: один сервис на скрипт, таймер с интервалом, блокировка внутри скрипта.
Python: аккуратно и «правильно»
В Python удобно использовать официальный пакет prometheus_client, у него есть функция write_to_textfile. Она пишет атомарно и правильно формирует HELP/TYPE.
#!/usr/bin/env python3
import argparse
import os
from prometheus_client import CollectorRegistry, Gauge, Counter, write_to_textfile
parser = argparse.ArgumentParser()
parser.add_argument('--out', default='/var/lib/node_exporter/textfile/app_metrics.prom')
args = parser.parse_args()
registry = CollectorRegistry()
# Гейдж: версия релиза и билд-номер как метки
release_info = Gauge('deploy_release_info', 'Текущий релиз приложения', ['project', 'env', 'version'], registry=registry)
release_info.labels(project='site', env='prod', version=os.getenv('APP_VERSION', 'unknown')).set(1)
# Счётчик: миграции БД (пример фикса завершённых миграций)
migrations = Counter('db_migrations_total', 'Сколько миграций БД выполнено', ['project'], registry=registry)
migrations.labels(project='site').inc(0)
write_to_textfile(args.out, registry)

Если зависимости ставить нельзя, можно писать «сырое» содержимое вручную:
#!/usr/bin/env python3
import os
import time
out = '/var/lib/node_exporter/textfile/simple.prom'
content = []
content.append('# HELP app_heartbeat_seconds Время генерации метрик, epoch')
content.append('# TYPE app_heartbeat_seconds gauge')
content.append('app_heartbeat_seconds {}'.format(int(time.time())))
tmp = out + '.tmp'
with open(tmp, 'w', encoding='utf-8') as f:
f.write('\n'.join(content) + '\n')
os.replace(tmp, out)
Советы для Python-метрик:
- одна директория — несколько файлов. Придерживайтесь префикса и тематики;
- группируйте метрики одной темы в одном файле, чтобы HELP/TYPE не дублировались несколькими скриптами;
- добавляйте таймауты сетевым вызовам и ограничивайте время работы.
Права доступа и безопасность
Рекомендуемый вариант: скрипты запускаются от root, но финальную запись выполняют в файл с владельцем node_exporter и режимом 0640. Так экспортер прочитает метрики, а посторонние — нет.
- директория
/var/lib/node_exporter/textfile:0750, владелец и группа — пользователь экспортера; - файлы
.prom:0640; - используйте
install -o -g -mилиchown/chmodпосле атомарного перемещения.
SELinux: если включён, дайте контекст на директорию аналогично остальным данным экспортера (через semanage fcontext и restorecon в вашей политике). Это за пределами типовой инструкции, но важно для RHEL‑подобных систем.
Атомарность и свежесть файлов
Почему атомарно? node_exporter читает файл целиком. Если вы пишете прямо в целевой путь и в этот момент случился контекстный переключатель, экспортер может прочитать половину файла и выбросить ошибку формата; метрики пропадут, node_textfile_scrape_error станет 1.
Пишите всегда в mktemp, затем install или mv. Для «жизненного цикла» добавьте чистильщик старых файлов, если метрики должны исчезать при остановке скрипта:
# удалять .stale.prom старше 10 минут
find /var/lib/node_exporter/textfile -name '*.stale.prom' -mmin +10 -type f -delete
Вместо удаления можно держать «индикатор свежести» — метрику времени генерации в секундах. В алертах условие: «если индикатор устарел > X минут — тревога».
Типичные ошибки
- Дублируете
# HELP/# TYPEдля одного имени в разных файлах. Решение: один файл — одна группа HELP/TYPE, или разные имена/префиксы. - Пишете метки с пробелами и без экранирования кавычек. Экранируйте
\\и\". - Слишком много меток (кардинальность растёт). Оставляйте стабильные значения:
env,project,instance. - Скрипт виснет, cron запускает следующий — конфликт. Используйте
flockи таймауты у команд. - Неверные права на директорию/файлы — экспортер не читает. Смотрите
node_textfile_scrape_error, логи сервиса и права.
Отладка и валидация
Проверьте файлы руками:
ls -l /var/lib/node_exporter/textfile
sed -n '1,120p' /var/lib/node_exporter/textfile/sys_updates.prom
Проверьте формат метрик promtool (утилита из Prometheus):
promtool check metrics /var/lib/node_exporter/textfile/sys_updates.prom
Посмотрите в выдачу экспортера, появились ли метрики:
curl -s localhost:9100/metrics | grep -E 'sys_updates_available|cert_expiry_days|fail2ban_bans_total'
Практика именования и единицы измерения
- Счётчики —
*_totalи инкрементируйте, а не устанавливайте; - Гейджи — текущие значения (температуры, количество банов, апдейты);
- Единицы — в имени:
_seconds,_milliseconds,_bytes,_days; - Не добавляйте
histogram/summaryвручную — textfile не агрегирует наблюдения, используйте gauge/counter.
Алерты для кастомных метрик
Как только метрики появились, добавляйте алерты в правила Prometheus. Вдохновиться можно и нашими примерами правил для Node и Nginx. Простейшие шаблоны:
groups:
- name: custom-textfile-alerts
rules:
- alert: ManyPendingUpdates
expr: sys_updates_available > 50
for: 2h
labels:
severity: warning
annotations:
summary: "Много обновлений на хосте"
description: "Доступно {{ $value }} обновлений. Проверьте репозитории и план обновления."
- alert: CertificateExpiringSoon
expr: cert_expiry_days{host!~""} >= 0 and cert_expiry_days <= 14
for: 1h
labels:
severity: critical
annotations:
summary: "Сертификат истекает скоро ({{ $labels.host }})"
description: "Срок истекает через {{ $value }} дней. Обновите сертификат заранее."
- alert: Fail2banEmpty
expr: fail2ban_bans_total == 0
for: 24h
labels:
severity: info
annotations:
summary: "Fail2ban не банит никого"
description: "Проверьте, работает ли Fail2ban и актуальны ли jails. Возможно, всё тихо — и это хорошо."
Правила даны как ориентир; адаптируйте пороги и метки под свою инфраструктуру.
Организационные практики
- Один репозиторий с каталогом
scripts/metrics/, стандартизируйте шаблон: логер, таймауты, flock, атомарная запись. - Код-ревью даже на «маленькие скрипты» — метрики влияют на алерты, а алерты на ваш сон.
- Единый стиль имён:
<domain>_<subject>_<unit>, напримерsys_updates_available,cert_expiry_days,deploy_release_info. - Версионируйте HELP/TYPE и договоритесь, в каком файле живёт какая группа метрик.
Итог
Textfile collector — «карманный нож» инженера: быстро, надёжно, предсказуемо. Включили коллектор, настроили права, завели пару скриптов — и у вас уже есть полезные сигналы для алертов: апдейты пакетов, срок SSL, статус Fail2ban. Дальше — стандартизируйте подход, добавьте Python там, где хочется типовой сериализации, и не забывайте про атомарную запись и кардинальность меток.


