467 lines
20 KiB
Markdown
467 lines
20 KiB
Markdown
# 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.
|