Files
AWatch-rus/docs/OPERATIONS_RUNBOOK_WORKTIME_RU.md
T

467 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Runbook восстановления worktime reports
Документ описывает безопасную диагностику и восстановление цепочки
`ActivityWatch -> aw-worktime-api -> portal executive reports`.
ClickHouse не является обязательной зависимостью worktime reports. Не
перезапускайте ClickHouse для восстановления отчетов рабочего времени, если нет
отдельного подтвержденного отказа ClickHouse.
## Stable host id
Worktime reports используют stable logical host id, а не обязательно текущее
Windows `COMPUTERNAME`. Для DetMir production текущий logical id:
```text
SHARKON2025
```
При переименовании RDP-сервера не меняйте `awHostname` автоматически. Сначала
обновите Windows account domain для задач, затем проверьте, что collectors
продолжают писать в bucket-и `*_SHARKON2025`. Подробный порядок:
`docs/WINDOWS_LOGICAL_HOST_ID_RU.md`.
## Симптомы перегруза
- `/portal/api/reports?role=executive` открывается медленно или отвечает
degraded/stale.
- `/portal/api/health` показывает degraded-состояние worktime source.
- `/reports/worktime/management` на `aw-worktime-api` возвращает
`status=DEGRADED`.
- В журнале `aw-worktime-api` растут `aw_query_timeout_count` или
`report_build_error_count`.
- ActivityWatch HTTP API отвечает медленно, не отвечает или держит SQLite под
высокой нагрузкой.
## Быстрая диагностика
Проверить состояние сервисов:
```bash
systemctl status activitywatch-server aw-worktime-api --no-pager
systemctl status aw-worktime-ui-bridge.timer aw-worktime-autoheal.timer aw-rus-healthd.timer --no-pager
```
Проверить последние журналы:
```bash
journalctl -u aw-worktime-api -n 80 --no-pager
journalctl -u activitywatch-server -n 80 --no-pager
```
Проверить health worktime API:
```bash
curl -sS --max-time 5 http://<AW_SERVER_HOST>:5610/health | jq
```
Проверить отчет в безопасном bounded-режиме:
```bash
curl -sS --max-time 8 \
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=SHARKON2025&allow_stale=1" \
| jq '.status,.stale,.runtime'
```
Проверить portal health:
```bash
curl -sS --max-time 8 http://<PORTAL_HOST>/portal/api/health | jq
curl -sS --max-time 12 "http://<PORTAL_HOST>/portal/api/reports?role=executive" | jq '.status,.sources'
```
## Проверка свежести bucket
Проверить metadata конкретного worktime bucket:
```bash
curl -sS --max-time 5 \
http://<AW_SERVER_HOST>:5600/api/0/buckets/aw-worktime-sessions_SHARKON2025 \
| jq '.metadata.end'
```
Проверить список bucket без чтения тяжелых событий:
```bash
curl -sS --max-time 5 http://<AW_SERVER_HOST>:5600/api/0/buckets | jq 'keys'
```
Если metadata свежая, а report degraded, вероятная причина - перегрузка чтения
events или временная недоступность ActivityWatch API. Не запускайте повторные
тяжелые запросы вручную без лимитов `--max-time`.
## Дубли пользователей в Grafana/Influx
Симптом: панели Grafana показывают одного сотрудника несколькими строками,
например `USER5` и `user5`, `Администратор` и `администратор`, или показывают
служебные/битые метки вроде `SHARKON2025$` и строк с ``.
Причина: старые версии worktime exporter писали raw `username`/`userId` в tag
`user`, а Grafana группировала Influx series по этому сырому tag. Поэтому
варианты регистра, machine account и поврежденная OEM/Unicode строка становились
разными series. После исправления exporter пишет canonical tags, но старые
series остаются в диапазоне Grafana до истечения retention/range, поэтому Flux
queries должны фильтровать и схлопывать их.
Текущая canonical policy для DetMir RDP host:
- `USER1/user1`, `USER4/user4`, `USER5/user5` -> `user1`, `user4`, `user5`;
- `администратор` -> `Администратор`;
- users с suffix `$` исключаются;
- users, содержащие Unicode replacement char ``, исключаются.
Кодовые точки, где должна сохраняться одинаковая нормализация:
- `adk-rust/crates/worktime-api/src/main.rs`;
- `adk-rust/crates/worktime-influx-exporter/src/main.rs`.
Проверка перед deploy:
```bash
cd <REPO_ROOT>/adk-rust
cargo fmt --all --check
cargo test -p worktime-api -p worktime-influx-exporter
cargo build --release -p worktime-api -p worktime-influx-exporter
```
Минимальный deploy с backup бинарников:
```bash
cd <REPO_ROOT>/ansible
export no_proxy="localhost,127.0.0.1,10.10.10.13,10.10.10.2,10.10.10.0/24"
export NO_PROXY="$no_proxy"
ts=$(date -u +%Y%m%dT%H%M%SZ)
ansible -i inventory.ini aw_server -m shell -a "set -e; sudo cp -a /usr/local/bin/aw-worktime-api-rust /usr/local/bin/aw-worktime-api-rust.bak.${ts}; sudo cp -a /usr/local/bin/aw-worktime-influx-exporter-rust /usr/local/bin/aw-worktime-influx-exporter-rust.bak.${ts}"
ansible -i inventory.ini aw_server -m copy -a "src=/home/igor/.cache/detmir-adk-rust-target/release/worktime-api dest=/tmp/aw-worktime-api-rust.new mode=0755"
ansible -i inventory.ini aw_server -m copy -a "src=/home/igor/.cache/detmir-adk-rust-target/release/worktime-influx-exporter dest=/tmp/aw-worktime-influx-exporter-rust.new mode=0755"
ansible -i inventory.ini aw_server -m shell -a 'set -e; sudo install -o root -g root -m 0755 /tmp/aw-worktime-api-rust.new /usr/local/bin/aw-worktime-api-rust; sudo install -o root -g root -m 0755 /tmp/aw-worktime-influx-exporter-rust.new /usr/local/bin/aw-worktime-influx-exporter-rust'
ansible -i inventory.ini aw_server -m shell -a 'set -e; sudo systemctl restart aw-worktime-api; sudo systemctl start aw-worktime-influx-exporter.service; systemctl is-active aw-worktime-api'
```
Grafana cleanup для старых Influx series:
- dashboard JSON: `grafana/detmir-rdp-user-activity-dashboard.json` и
`grafana/detmir-aw-main-dashboard.json`;
- Flux должен фильтровать `user_id !~ /\$$/` и `user_id !~ //`;
- известные текущие accounts должны мапиться в canonical labels до grouping;
- grouping должен быть по `report_date,user` или `_time,user`;
- для схлопывания duplicate series использовать `max(column: "_value")`, чтобы
не удваивать часы.
Если Grafana API import возвращает `403`, используйте provisioning/DB fallback:
```bash
scp grafana/detmir-aw-main-dashboard.json grafana/detmir-rdp-user-activity-dashboard.json igor@10.10.10.2:~/codex-dashboard-import/
ssh igor@10.10.10.2 'sudo pct push 201 /home/igor/codex-dashboard-import/detmir-aw-main-dashboard.json /etc/grafana/provisioning/dashboards/aw/detmir-aw-main.json --perms 0644'
ssh igor@10.10.10.2 'sudo pct push 201 /home/igor/codex-dashboard-import/detmir-rdp-user-activity-dashboard.json /etc/grafana/provisioning/dashboards/aw/detmir-rdp-user-activity.json --perms 0644'
ssh igor@10.10.10.2 'sudo pct exec 201 -- bash -lc "cp -a /var/lib/grafana/grafana.db /var/lib/grafana/grafana.db.bak.$(date -u +%Y%m%dT%H%M%SZ); systemctl restart grafana-server"'
```
Если provisioning не перезаписал существующие DB dashboards, перед изменением
сделать backup `/var/lib/grafana/grafana.db`, затем заменить только
`dashboard.data` rows по uid `detmir-aw-main` и `detmir-rdp-user-activity`.
Проверка после deploy:
- live panel `Вчера: активность по сотрудникам` возвращает только labels
`user1`, `user4`, `user5`, `Администратор`;
- bad labels list пуст для `USER*`, `SHARKON2025$`, `администратор`, `` и
labels, начинающихся с `\`;
- Grafana dashboard открывается с HTTP `200`, HTML title содержит `Grafana`;
- `aw-worktime-api`, `grafana-server` и `aw-worktime-influx-exporter.timer`
активны.
## Проверка лимитов
Проверить системные настройки:
```bash
systemctl cat aw-worktime-api
grep '^AW_WORKTIME_' /etc/activitywatch/aw-server.env
```
Ключевые параметры:
- `AW_WORKTIME_EVENTS_LIMIT` - верхний лимит чтения events из ActivityWatch.
Для дневной управленческой аналитики значение должно покрывать рабочий день
по всем активным сессиям. Для пилотного контура используется `5000`; малые
значения вроде `250` допустимы только для аварийного degraded-smoke, иначе
отчет будет построен по последнему хвосту событий, а не по полному дню.
- `AW_WORKTIME_AW_HTTP_TIMEOUT_SECONDS` - timeout запросов к ActivityWatch API.
- `AW_WORKTIME_SOURCE_HTTP_TIMEOUT_SECONDS` - timeout внешних source-запросов.
- `AW_WORKTIME_REPORT_CACHE_TTL_SECONDS` - TTL fresh report cache.
- `AW_WORKTIME_REPORT_STALE_TTL_SECONDS` - TTL stale cache для degraded path.
Нормальная production-политика: bounded timeouts, ограниченный events limit,
stale cache включен. Нулевой stale TTL допустим только для специальных тестов,
но не для демонстрации или промышленного пилота.
## Безопасный restart
1. Зафиксировать текущий сигнал:
```bash
systemctl status aw-worktime-api activitywatch-server --no-pager
journalctl -u aw-worktime-api -n 120 --no-pager
curl -sS --max-time 5 http://<AW_SERVER_HOST>:5610/health | jq
```
2. Перезапустить только `aw-worktime-api`, если ActivityWatch отвечает, но
портал получает degraded report:
```bash
systemctl restart aw-worktime-api
sleep 3
curl -sS --max-time 5 http://<AW_SERVER_HOST>:5610/health | jq
```
3. Перезапустить `activitywatch-server` только если ActivityWatch API не
отвечает или SQLite явно перегружен:
```bash
systemctl restart activitywatch-server
sleep 5
systemctl restart aw-worktime-api
```
4. Прогреть отчет один раз:
```bash
curl -sS --max-time 12 \
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=SHARKON2025&allow_stale=1" \
| jq '.status,.stale,.runtime'
```
5. Проверить портал:
```bash
curl -sS --max-time 8 http://<PORTAL_HOST>/portal/api/health | jq
curl -sS --max-time 12 "http://<PORTAL_HOST>/portal/api/reports?role=executive" | jq '.status'
```
## Production repair: AW SQLite hot path, 2026-06-30
Симптомы:
- `activitywatch-server` отвечает `503` на bucket API;
- журнал содержит `poisoned lock` / `database is locked`;
- `aw-worktime-api` уходит в bounded `DEGRADED`;
- `/buckets/aw-worktime-sessions_<HOST>/events?limit=...` тайм-аутится даже
при малом лимите;
- RDP browser/category collector пишет `bucket create failed` или timeout.
Порядок безопасного восстановления:
1. Остановить RDP guard и процессы `aw-windows-telemetry`, чтобы не продолжать
штурмовать AW API.
2. Остановить `aw-worktime-*` timers/services и другие локальные потребители AW
API.
3. Перезапустить `activitywatch-server` отдельно и проверить `/api/0/info`.
4. Если bucket metadata отвечает, но `/events` медленный, проверить SQLite plan:
```sql
EXPLAIN QUERY PLAN
SELECT id,starttime,endtime,data
FROM events
WHERE bucketrow=(SELECT id FROM buckets WHERE name='aw-worktime-sessions_<HOST>')
ORDER BY starttime DESC
LIMIT 100;
```
Если план строит `TEMP B-TREE FOR ORDER BY`, нужен составной индекс:
```sql
CREATE INDEX IF NOT EXISTS events_bucketrow_starttime_desc_index
ON events(bucketrow, starttime DESC);
ANALYZE;
PRAGMA optimize;
PRAGMA integrity_check;
```
Индекс добавлять только в controlled window:
- остановить `activitywatch-server`;
- сделать rollback backup SQLite DB;
- создать индекс;
- проверить `PRAGMA integrity_check = ok`;
- запустить `activitywatch-server`;
- проверить, что `/events?limit=100` больше не тайм-аутится.
Production DetMir repair 2026-06-30:
- оставлены две свежие ежедневные SQLite VACUUM backup-копии, старые backup-и
ротированы для освобождения места;
- создан rollback backup:
`/var/lib/activitywatch/backups/db/aw-sqlite-before-hotpath-index-20260630T035032Z.db`;
- добавлен индекс `events_bucketrow_starttime_desc_index`;
- `ROCKET_WORKERS=8` добавлен в `/etc/activitywatch/aw-server.env`;
- для `aw-worktime-api.service` добавлен stabilization drop-in:
`AW_WORKTIME_EVENTS_LIMIT=100`,
`AW_WORKTIME_AW_HTTP_TIMEOUT_SECONDS=25`,
`AW_WORKTIME_EVENTS_CACHE_TTL_SECONDS=600`,
`AW_WORKTIME_REPORT_STALE_TTL_SECONDS=7200`.
После ремонта проверить:
```bash
curl -sS --max-time 10 http://127.0.0.1:5600/api/0/info
curl -sS --max-time 15 \
'http://127.0.0.1:5600/api/0/buckets/aw-worktime-sessions_SHARKON2025/events?limit=100'
curl -sS --max-time 15 \
'http://127.0.0.1:5610/reports/worktime/today?format=json' | jq '{rows:(.rows|length),degraded,runtime}'
```
Также проверить отсутствие новых `poisoned lock` после финального старта:
```bash
journalctl -u activitywatch-server --since '<FINAL_START_TIME>' --no-pager |
grep -E 'poisoned lock|Taking datastore lock failed|database is locked'
```
## Crash/readiness test after AW repair
Цель: проверить, что контур выдерживает restart и короткую параллельную
нагрузку, а проверки не путают `systemctl active` с готовым API.
Порядок:
1. Зафиксировать baseline:
```bash
./check-aw-full.sh
```
2. На AW server проверить readiness, hot-path и Worktime API:
```bash
AW_API=http://127.0.0.1:5600 \
AW_WORKTIME_API=http://127.0.0.1:5610 \
AW_LOGICAL_HOST_ID=SHARKON2025 \
scripts/detmir_resilience_check.sh --live
```
Если скрипт запускается с ноутбука, live-mode нужно выполнять на самом
AW-сервере через SSH/Ansible, потому что он проверяет local systemd и SQLite.
3. Controlled restart:
```bash
systemctl restart aw-worktime-api
curl -sS --max-time 12 \
'http://127.0.0.1:5610/reports/worktime/today?format=json&host=SHARKON2025&allow_stale=1' |
jq '{rows:(.rows|length),degraded}'
systemctl restart activitywatch-server
# Не считать "active" готовностью: дождаться HTTP readiness.
timeout 90 bash -c 'until curl -fsS --max-time 8 http://127.0.0.1:5600/api/0/info >/dev/null; do sleep 2; done'
curl -sS --max-time 15 \
'http://127.0.0.1:5600/api/0/buckets/aw-worktime-sessions_SHARKON2025/events?limit=100' >/dev/null
```
4. Проверить, что после рестарта нет новых lock/503:
```bash
journalctl -u activitywatch-server --since '<RESTART_TIME>' --no-pager |
grep -E 'poisoned lock|Taking datastore lock failed|database is locked|503'
```
5. Проверить RDP guard restart отдельно:
```powershell
Restart-Service AWatchRusCollectorGuard -Force
Start-Sleep -Seconds 75
Get-Service AWatchRusCollectorGuard
Get-Process aw-windows-telemetry -ErrorAction SilentlyContinue | Measure-Object
```
Ожидаемый результат для DetMir после ремонта 2026-06-30:
- `check-aw-full.sh`: `FRESH=8`, `STALE=0`, `DEAD=0`;
- `/events?limit=100` отвечает за bounded time и использует
`events_bucketrow_starttime_desc_index`;
- `aw-rus-healthd.service` завершается `status=0/SUCCESS`;
- server-side TCP до RDP может быть `warn`, если
`AW_RUS_HEALTH_RDP_TCP_REQUIRED=false`, но bucket freshness и WinRM/SSH
через admin path должны оставаться зелёными;
- optional DLP/Loki heavy runtime units должны быть inactive в экономном
production profile.
## Rollback
Rollback нужен, если после обновления бинарника или env-настроек:
- fresh report не собирается;
- stale cache не отдается;
- `/health` не отражает degraded-состояние;
- портал зависает вместо bounded degraded response.
Порядок:
```bash
systemctl stop aw-worktime-api
cp /usr/local/bin/aw-worktime-api.prev /usr/local/bin/aw-worktime-api
systemctl daemon-reload
systemctl start aw-worktime-api
curl -sS --max-time 5 http://<AW_SERVER_HOST>:5610/health | jq
```
Если rollback касается env/drop-in:
```bash
cp /etc/systemd/system/aw-worktime-api.service.d/override.conf.prev \
/etc/systemd/system/aw-worktime-api.service.d/override.conf
systemctl daemon-reload
systemctl restart aw-worktime-api
```
Перед rollback убедитесь, что backup-файлы действительно относятся к предыдущей
рабочей версии.
Последний production rollback set после исправления canonical users
`2026-06-09`:
- `/var/lib/grafana/grafana.db.bak.20260609T013605Z`;
- `/usr/local/bin/aw-worktime-api-rust.bak.20260609T011956Z`;
- `/usr/local/bin/aw-worktime-influx-exporter-rust.bak.20260609T011956Z`.
## Признаки успешного восстановления
- `/reports/worktime/management` отвечает HTTP 200 в bounded time.
- При свежей сборке `status` отсутствует или равен `OK`, `stale=false`.
- При временном отказе ActivityWatch API отдается `status=DEGRADED`, а не
timeout.
- Если stale cache доступен, response содержит `stale=true` и
`runtime.report_stale_served=true`.
- Если stale cache недоступен, response компактный, `stale=false`,
`reason=report_unavailable`.
- `/health` у `aw-worktime-api` и `/portal/api/health` не маркируют систему как
fully healthy при degraded reports.
- Счетчики `aw_query_timeout_count` и `report_build_error_count` перестают
расти после восстановления ActivityWatch API.
## Smoke-тест degraded path
Локально, без обращения к рабочему контуру:
```bash
cd <REPO_ROOT>
cd adk-rust && cargo build -p worktime-api
cd ..
node scripts/worktime-degraded-smoke.mjs
```
Ожидаемый результат:
```text
worktime degraded smoke OK
```
Smoke проверяет:
- fresh report успевает построиться и прогреть cache;
- при недоступном ActivityWatch API отдается stale degraded response;
- при отсутствии stale cache отдается компактный degraded response;
- health отражает degraded-состояние;
- runtime-поля присутствуют в JSON.