Pulse — local-first observability node для Linux с диагностическим TUI. Он собирает данные из procfs, sysfs и cgroup v2, строит temporal entity graph, хранит короткую локальную историю и показывает не только загрузку, но и владельца ресурса, открытые проблемы и изменения между двумя моментами.
Текущий статус: рабочий Linux MVP. Дисковая история, eBPF, OTLP, Prometheus Remote Read/Write и Kubernetes/CRI API пока не реализованы.
- сущности
host,cgroup,systemd unit,process,disk,network interface,container,pod; - устойчивая идентичность процессов
(pid, start_ticks)и cgroup по inode; - CPU, memory, PSI, disk, network, process и cgroup telemetry;
- восемь семейств диагностических правил с гистерезисом и явными доказательствами;
- semantic A/B diff структуры, метрик и событий без недоказанных причинных утверждений;
- spawn ancestry по
ppidиpulse why <pid>: источник запуска выводится только как эвристика с ancestor-доказательством; обрыв цепочки из-за бюджета/churn/прав помечается явно и не смешивается с ownership-графом; - четыре экрана Overview, Problems, Entities, Timeline плюс контекстный Inspector и overlay'и Help/Search/Palette;
- семантический конвейер событий: рутинный churn ядра подавляется, повторы
сворачиваются в группы, полный поток остаётся доступен через
Raw events(Все событияв ru); - жёсткий предел памяти истории с гарантией прогресса вытеснения и bounded
завершением по
SIGTERM/SIGINT; - OpenMetrics на
/metrics, проверка состояния на/healthz; - безопасные значения по умолчанию: loopback bind, process metrics выключены для экспорта, redaction включена, действия выключены.
Инструмент диагностики нужен там, где ставить ничего нельзя: чужой сервер, инцидент, нет пакетного менеджера и прав root. Поэтому поставка — один статический файл (musl, около 3 МиБ), без зависимостей и без установки в систему.
curl -fsSL https://raw.githubusercontent.com/Stolyarovmn/Pulse/main/scripts/install.sh | bashСкрипт сверяет контрольную сумму, проверяет запуск бинарника и кладёт его в
/usr/local/bin либо в ~/.local/bin, если прав на первый нет. Установка без
подтверждённой суммы невозможна: несовпадение и отсутствие SHA256SUMS
прерывают её (scripts/verify-install.sh проверяет все три исхода).
Вручную, без скрипта:
curl -fLO https://github.com/Stolyarovmn/Pulse/releases/latest/download/pulse-linux-x86_64
curl -fLO https://github.com/Stolyarovmn/Pulse/releases/latest/download/SHA256SUMS
sha256sum -c SHA256SUMS
chmod +x pulse-linux-x86_64 && ./pulse-linux-x86_64 runПубликуется только Linux x86_64: на остальных таргетах сборку нечем проверить прогоном, а непроверенный артефакт выкладывать нельзя.
- Linux с cgroup v2;
- Rust 1.85 или новее;
- терминал с UTF-8; для ограниченных терминалов доступен
ui.ascii = true.
На Windows разработка и запуск выполняются внутри WSL2. Репозиторий может находиться на диске Windows; вспомогательные скрипты держат target в Linux-файловой системе.
Linux:
cargo build --release -p pulse-cli
cargo test --workspaceWindows + WSL2 Ubuntu 24.04:
wsl -d Ubuntu-24.04 -- bash /mnt/c/Users/maxim/project/pulse/scripts/wsl-cargo.sh build --release -p pulse-cli
wsl -d Ubuntu-24.04 -- bash /mnt/c/Users/maxim/project/pulse/scripts/wsl-cargo.sh test --workspacescripts/measure-cost.sh # сбор: CPU, RSS, серии, выход по SIGTERM
scripts/measure-cost.sh --tui 15 # render-цикл живого TUIПроверять раскладку глазами по скриншоту дорого и невоспроизводимо. Кадр снимается как текстовая сетка нужного размера:
scripts/wsl-capture.sh -c 180 -r 40 # Overview на 180x40
scripts/wsl-capture.sh -c 120 -r 30 -k 4 # Timeline
scripts/wsl-capture.sh -c 120 -r 40 -k '?' # Help с легендой состоянийСетку держит tmux, поэтому позиционирование курсора не теряется, а агент
гарантированно останавливается по PID панели и не занимает порт 9099.
Детерминированные буферы без запуска процесса даёт
PULSE_DUMP_SNAPSHOTS=1 cargo test -p pulse-tui --lib dump_v09 -- --nocapture.
Цепочка расследования проходится одной командой: экран Entities, поиск, заданное число спусков, кадр каждого шага.
scripts/capture-inspector.sh angie 3 # unit → cgroup → process
scripts/capture-inspector.sh angie 3 -a # кадр после каждого шага
scripts/capture-inspector.sh angie 2 -t # проверить боковой переходЕсли агент не отвечает или занимает ядро, нужно знать, какой поток и на чём стоит, а не гадать:
scripts/diag-stuck.sh <pid> 2 # состояние потоков и их тики ядра
scripts/repro-terminal-loss.sh # воспроизводит потерю терминала под TUIВторой скрипт входит в scripts/wsl-verify.sh: тест на pty потерю терминала
не воспроизводит, а живой дефект «100% ядра и игнор kill» давала только
смерть сервера tmux под работающим интерфейсом.
Обычная сборка динамически линкуется с glibc узла сборки: собранный на
Ubuntu 24.04 бинарник требует GLIBC_2.39 и на 22.04 падает с
version 'GLIBC_2.39' not found. Для переноса на произвольный хост есть
статическая сборка на musl - внешний toolchain не нужен:
bash scripts/build-portable.shСкрипт проверяет статическую линковку, отсутствие ссылок на glibc и прогоняет
pulse check, после чего кладёт результат в dist/pulse (около 2,7 МБ).
Перенос:
scp dist/pulse user@host:~/pulse
ssh user@host 'chmod +x ~/pulse && ~/pulse run'# TUI и локальный OpenMetrics endpoint
cargo run -p pulse-cli -- run
# Только агент и HTTP endpoint
cargo run -p pulse-cli -- serve
# Однократный текстовый снимок
cargo run -p pulse-cli -- top --limit 20
# Почему PID существует: spawn ancestry отдельно от ownership
cargo run -p pulse-cli -- why 1234
# A/B diff: собрать короткое окно и сравнить секунду назад с текущим моментом
cargo run -p pulse-cli -- diff --from 1s --to now
# Измерить стоимость агента
cargo run -p pulse-cli -- scorecard --seconds 10
# Проверить конфигурацию и реальный такт сбора
cargo run -p pulse-cli -- check
# Получить эффективную конфигурацию
cargo run -p pulse-cli -- config printПо умолчанию exporter слушает только 127.0.0.1:9099:
curl http://127.0.0.1:9099/healthz
curl http://127.0.0.1:9099/metricsНа здоровом хосте диагностику показать нечем: правила молчат, Timeline пуст,
A/B diff нечего сравнивать. Флаг --demo подставляет сценарий вместо
реального хоста:
cargo run -p pulse-cli -- --demo run
cargo run -p pulse-cli -- --demo topПодменяется только нижний слой чтения файлов (FsSource). Дальше идёт
тот же конвейер: те же коллекторы, тот же граф сущностей, те же правила
с гистерезисом, та же история. Демо доказывает работу движка, а не рисует
картинку поверх него, и потому не имеет права быть незаметным — в шапке
каждого кадра стоит плашка DEMO.
Сценарий цикличен, такт равен general.interval_ms:
| Такты | Что происходит |
|---|---|
| 0–29 | покой |
| 30–74 | деградация api.service: память идёт к лимиту cgroup, CPU упирается в квоту, растёт давление PSI |
| 75–149 | восстановление, проблема закрывается по своему условию снятия |
Числа сценария выбраны по порогам Rules::default, а не на глаз: пик обязан
пересекать memory_util_crit (0.95), throttle_warn (0.10) и psi_cpu_crit
(0.50), а фаза покоя — быть длиннее clear_after_ticks (15 тактов), иначе
проблема не успевает закрыться. Оба свойства закреплены тестами
(pulse-collect/src/demo.rs, pulse-cli/tests/demo_scenario.rs).
Для bind не на loopback требуется export.token_file с токеном длиной не менее 16 байт. Встроенного TLS нет: для внешней публикации нужен TLS reverse proxy или защищённый туннель.
Четыре top-level экрана; Inspector — контекстная сессия, палитра команд,
справка и Search — overlay'и. Tab никогда не меняет экран: он циклит только
видимые панели текущего экрана. Текущее место подсвечено плашкой в футере,
фокус панели — цветом и маркером ▌. Футер контекстный: в Inspector и
overlay он освобождает место от глобальных вкладок и показывает действия
текущего режима (Enter Follow, Tab Panel, Esc Back).
| Клавиша | Действие |
|---|---|
1 / 2 / 3 / 4 |
Overview / Problems / Entities / Timeline |
P / E,e / T |
псевдонимы Problems / Entities / Timeline |
↑/↓, j/k |
выбор строки в focused панели |
Enter |
открыть выбранный видимый объект |
Esc |
назад по следу Inspector, затем к origin |
Tab / Shift+Tab |
вне Inspector: видимые панели; в Inspector: INSIDE → LEADS → AROUND |
/ |
видимый modal поиска |
: |
палитра команд текущего места: ↑↓ выбор, Enter выполнить, набор фильтрует |
? / F1 |
справка — та же палитра со всеми командами всех экранов и алфавитом состояний |
, |
Settings: язык Русский / English и иконки off / unicode / nerd; применяется сразу |
m / s / f |
Entities: вид, сортировка, фильтр |
←/→, +/-, F |
Timeline: scrub, масштаб, возврат в LIVE |
A, B, D |
маркеры и semantic diff |
p |
пауза отображения; сбор продолжается |
q, Ctrl+C |
выход |
Команда палитры нажимает ту же клавишу, что и в таблице, поэтому палитра не
расходится с горячими клавишами. Команда другого экрана из справки сначала
переводит на этот экран. Своей клавиши нет только у Raw events (Все события)
и Incident story на Timeline.
Язык и набор иконок можно задать постоянно в конфигурации:
[ui]
language = "ru" # ru | en
icons = "nerd" # off | unicode | nerdОкно Settings меняет значения только для текущей сессии и показывает preview.
Pulse выбирает глифы Nerd Font, но не может сменить шрифт терминала: в Windows
Terminal выберите вариант с суффиксом Nerd Font Mono. В ASCII-режиме язык
временно отображается по-английски, а иконки скрываются, чтобы кадр оставался
строго ASCII.
Локаль переводит пояснения и действия, но не меняет общепринятый
observability-словарь: LIVE, up, OVERVIEW, PROBLEMS, ENTITIES,
TIMELINE, CPU, MEM, PSI, IO WAIT, max, avg. Так кадр совпадает
с btop/below/Grafana и не заставляет заново расшифровывать аббревиатуры.
Inspector отвечает на вопросы следователя, каждый блок — на один:
| Блок | Вопрос |
|---|---|
TRAIL |
где я и как сюда пришёл: пронумерованный след, текущий шаг выделен |
CASE |
что здесь не так: открытые проблемы объекта и его cgroup-двойника, доказательства, форма величины за минуту |
LEADS |
где интересное: проблема или ненормальное состояние внутри, доля CPU/памяти больше половины, перезапуск или OOM, самый активный сосед по диску |
INSIDE |
из чего объект состоит: каждый объект отдельной строкой, сначала ненормальные, затем нагруженные, с долей CPU |
AROUND |
кто рядом: родитель (owner — только unit/контейнер/pod), соседи с дисковым потоком, ресурсы |
DOSSIER |
кто это: путь, доли хоста, CPU за 60 с, лимиты; у процесса — источник запуска и детали |
RECENT |
что с объектом происходило |
Зацепки — измеренные факты («84% of CPU here (2.10c of 2.50c)»), а не
догадки о причине. Enter по INSIDE и LEADS продолжает след, Esc
возвращает к месту, откуда за зацепкой пошли. Переход из AROUND начинает
новое расследование. Enter открывает строку, показанную на экране, а курсор
держится за объект, когда список пересортировался за такт.
Диски и интерфейсы — ресурсы, а не звенья: диском пользуются десятки несвязанных сервисов, и переход в него превращал бы его в пересадочный узел.
На процессе дело заканчивается PROCESS DETAILS: пользователь, исполняемый
файл, рабочий каталог, слушающие порты и открытые файлы. Они читаются по
требованию только для открытого процесса. Без прав на чужой процесс блок
честно сообщает restricted: run as root to read, а не выглядит как «файлов
нет».
В TUI-режиме диагностика не печатается в терминал: ratatui владеет экраном, и
строка журнала оставалась бы висеть посреди кадра. Журнал включается переменной
PULSE_LOG=/path/to/pulse.log; состояние потолка памяти и ошибки сбора видны в
самом интерфейсе и в pulse scorecard.
Создайте файл из эффективной конфигурации и передайте его глобальным параметром:
pulse config print > pulse.toml
pulse --config pulse.toml check
pulse --config pulse.toml runНеизвестные поля, небезопасный внешний bind без token file, неверные диапазоны и сломанная гистерезисная конфигурация приводят к отказу старта.
- история хранится только в памяти текущего процесса;
pulse diffс относительным временем сам наполняет окно и ограничен пятью минутами;- контейнеры и pod определяются эвристически по cgroup path, без обращения к runtime API;
- exporter не реализует TLS;
serveи TUI завершаются поSIGTERM/SIGINT/SIGHUP/SIGQUIT, но HTTP-соединения не дренируются: запрос, начатый в момент остановки, обрывается;- глубина истории сокращается при достижении
store.max_bytes(64 МиБ по умолчанию): сначала warm, затем hot, но не ниже двух тактов. Если два такта сами по себе тяжелее бюджета (тысячи серий и очень малыйmax_bytes), соблюсти его невозможно: агент не обнуляет историю, а заявляет отказ вытеснения — счётчикpulse_agent_history_eviction_no_progressиpulse_agent_history_peak_bytesв/metrics, те же величины вpulse scorecard. Проверяется живым прогономscripts/measure-memory-bound.sh; - длинное окно доступно только для 22 метрик списка
LONG_WINDOW; остальные метрики живут только в горячем окне; - namespace pod не определяется без Kubernetes API и остаётся
unknown; - наблюдаемость процессов ограничивается правами пользователя и настройками
/proc; - детали процесса просматривают не более 1024 дескрипторов: разыменование
каждой ссылки в
/proc/<pid>/fd— отдельный системный вызов, и у прокси со ста тысячами соединений одно нажатиеEnterстоило бы ста тысяч вызовов. При превышении бюджета список файлов и портов помечается неполным (обход оборван бюджетом), а не выдаётся за полный; - дорожки метрик Timeline рисуют форму только по реальным точкам горячего окна
(
History::series_points); до появления второй точки честно пишутcollecting history, а не подделывают кривую; - сама линия времени Timeline не размечает события по оси: разметка требует биннинга истории, поэтому события живут в State River и Story;
- блоки
FILESиSOCKETSинспектора не реализованы: файловых дескрипторов и сокетов как сущностей в модели пока нет; - зависимость
cgroup -> diskстроится по/sys/dev/block/<major>:<minor>, а без этого каталога (WSL2, урезанный контейнер) - поmajor:minorцелых устройств из/proc/diskstats; раздел, упомянутый вio.statна таком хосте, до диска не поднимается, и связь честно не создаётся.
Apache License 2.0. См. LICENSE.