Skip to content
StolyarovmnPublic

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Pulse

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 --workspace

Windows + 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 --workspace

Стоимость агента

scripts/measure-cost.sh            # сбор: CPU, RSS, серии, выход по SIGTERM
scripts/measure-cost.sh --tui 15   # render-цикл живого TUI

Снимок кадра 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 или защищённый туннель.

Управление TUI

Четыре 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, неверные диапазоны и сломанная гистерезисная конфигурация приводят к отказу старта.

Архитектура и безопасность

Ограничения MVP

  • история хранится только в памяти текущего процесса;
  • 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.

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages