docs: document worktime Grafana user normalization
This commit is contained in:
@@ -24,6 +24,62 @@ Version-controlled dashboard JSON находятся в каталоге `grafan
|
|||||||
|
|
||||||
По умолчанию playbook складывает их в folder `AWatch-rus` с `uid=awatch-rus`.
|
По умолчанию playbook складывает их в folder `AWatch-rus` с `uid=awatch-rus`.
|
||||||
|
|
||||||
|
## Worktime panels и canonical users
|
||||||
|
|
||||||
|
Worktime dashboard'ы читают InfluxDB measurement
|
||||||
|
`aw_rdp_worktime_daily`/`aw_rdp_worktime_hourly` и группируют данные по user
|
||||||
|
label. Старые exporter versions писали raw `username`/`userId`, поэтому в
|
||||||
|
Influx могли остаться отдельные series для `USER5/user5`,
|
||||||
|
`Администратор/администратор`, machine account `SHARKON2025$` и битых строк с
|
||||||
|
Unicode replacement char `�`.
|
||||||
|
|
||||||
|
Version-controlled dashboard JSON должны сохранять защиту от старых series:
|
||||||
|
|
||||||
|
- `grafana/detmir-rdp-user-activity-dashboard.json`;
|
||||||
|
- `grafana/detmir-aw-main-dashboard.json`.
|
||||||
|
|
||||||
|
Для affected Flux queries обязательны правила:
|
||||||
|
|
||||||
|
- фильтровать `user_id !~ /\$$/` и `user_id !~ /�/`;
|
||||||
|
- мапить текущие DetMir accounts в canonical labels:
|
||||||
|
`user1`, `user4`, `user5`, `Администратор`;
|
||||||
|
- grouping делать по `report_date,user` или `_time,user`;
|
||||||
|
- использовать `max(column: "_value")` после grouping, чтобы схлопнуть
|
||||||
|
duplicate series без удвоения часов.
|
||||||
|
|
||||||
|
После импорта проверять панель `Вчера: активность по сотрудникам`. Ожидаемые
|
||||||
|
labels: `user1`, `user4`, `user5`, `Администратор`. Bad labels list должен быть
|
||||||
|
пустым для `USER*`, `SHARKON2025$`, `администратор`, `�` и labels, начинающихся
|
||||||
|
с `\`.
|
||||||
|
|
||||||
|
## Доступ владельца из портала
|
||||||
|
|
||||||
|
На production-контуре DetMir переход из `/portal` к Grafana dashboard'ам
|
||||||
|
выполняется без второго логина Grafana. Внешняя защита при этом остается на
|
||||||
|
gateway:
|
||||||
|
|
||||||
|
- `/portal/`, `/d/...`, `/dashboards` и `/r/grafana/` закрыты nginx Basic Auth;
|
||||||
|
- nginx после успешной gateway-авторизации передает в Grafana auth-proxy
|
||||||
|
заголовки:
|
||||||
|
- `X-WEBAUTH-USER: detmir-owner`;
|
||||||
|
- `X-WEBAUTH-NAME: AWatch-rus Owner`;
|
||||||
|
- `X-WEBAUTH-EMAIL: owner@awatch-rus.local`;
|
||||||
|
- Grafana принимает auth-proxy только от gateway `10.10.10.2`;
|
||||||
|
- созданный пользователь `detmir-owner` не является Grafana admin и получает
|
||||||
|
viewer-доступ.
|
||||||
|
|
||||||
|
Основной dashboard для владельца:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/d/detmir-rdp-user-activity/detmir3a-rabota-pol-zovatelej-v-rdp?orgId=1&from=now-7d&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m
|
||||||
|
```
|
||||||
|
|
||||||
|
В портале он доступен как кнопка `Графики сотрудников`.
|
||||||
|
|
||||||
|
Не включайте `[auth.anonymous]` для решения этой задачи: это откроет Grafana на
|
||||||
|
внутреннем адресе `10.10.10.11:3000` без пользовательского контекста. Для
|
||||||
|
production используется только auth-proxy с whitelist gateway.
|
||||||
|
|
||||||
## Быстрый запуск
|
## Быстрый запуск
|
||||||
|
|
||||||
1. Подготовьте inventory и vars:
|
1. Подготовьте inventory и vars:
|
||||||
@@ -57,6 +113,30 @@ ansible-playbook -i inventory.ini deploy_grafana_dashboards.yml
|
|||||||
- перезаписывает существующие dashboard'ы при `overwrite=true`;
|
- перезаписывает существующие dashboard'ы при `overwrite=true`;
|
||||||
- верифицирует каждый dashboard по `uid` через `GET /api/dashboards/uid/<uid>`.
|
- верифицирует каждый dashboard по `uid` через `GET /api/dashboards/uid/<uid>`.
|
||||||
|
|
||||||
|
## Production fallback при 403
|
||||||
|
|
||||||
|
Если Grafana API import запрещен (`403`) или provisioning не перезаписывает уже
|
||||||
|
существующую DB-запись dashboard, не правьте JSON только в UI. Сначала
|
||||||
|
обновите version-controlled dashboard JSON в git, затем примените один из
|
||||||
|
fallback paths.
|
||||||
|
|
||||||
|
Provisioning push:
|
||||||
|
|
||||||
|
```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"'
|
||||||
|
```
|
||||||
|
|
||||||
|
DB fallback: после backup `/var/lib/grafana/grafana.db` заменить только
|
||||||
|
`dashboard.data` rows по uid нужных dashboard'ов и перезапустить
|
||||||
|
`grafana-server`. Для исправления worktime-дублей production backup был:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/grafana/grafana.db.bak.20260609T013605Z
|
||||||
|
```
|
||||||
|
|
||||||
## Переменные
|
## Переменные
|
||||||
|
|
||||||
- `grafana_url` — base URL Grafana, например `http://10.20.30.11:3000`
|
- `grafana_url` — base URL Grafana, например `http://10.20.30.11:3000`
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ curl -sS --max-time 5 http://<AW_SERVER_HOST>:5610/health | jq
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS --max-time 8 \
|
curl -sS --max-time 8 \
|
||||||
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=HOST-EXAMPLE&allow_stale=1" \
|
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=SHARKON2025&allow_stale=1" \
|
||||||
| jq '.status,.stale,.runtime'
|
| jq '.status,.stale,.runtime'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ curl -sS --max-time 12 "http://<PORTAL_HOST>/portal/api/reports?role=executive"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS --max-time 5 \
|
curl -sS --max-time 5 \
|
||||||
http://<AW_SERVER_HOST>:5600/api/0/buckets/aw-worktime-sessions_HOST-EXAMPLE \
|
http://<AW_SERVER_HOST>:5600/api/0/buckets/aw-worktime-sessions_SHARKON2025 \
|
||||||
| jq '.metadata.end'
|
| jq '.metadata.end'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,6 +76,88 @@ curl -sS --max-time 5 http://<AW_SERVER_HOST>:5600/api/0/buckets | jq 'keys'
|
|||||||
events или временная недоступность ActivityWatch API. Не запускайте повторные
|
events или временная недоступность ActivityWatch API. Не запускайте повторные
|
||||||
тяжелые запросы вручную без лимитов `--max-time`.
|
тяжелые запросы вручную без лимитов `--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`
|
||||||
|
активны.
|
||||||
|
|
||||||
## Проверка лимитов
|
## Проверка лимитов
|
||||||
|
|
||||||
Проверить системные настройки:
|
Проверить системные настройки:
|
||||||
@@ -88,6 +170,10 @@ grep '^AW_WORKTIME_' /etc/activitywatch/aw-server.env
|
|||||||
Ключевые параметры:
|
Ключевые параметры:
|
||||||
|
|
||||||
- `AW_WORKTIME_EVENTS_LIMIT` - верхний лимит чтения events из ActivityWatch.
|
- `AW_WORKTIME_EVENTS_LIMIT` - верхний лимит чтения events из ActivityWatch.
|
||||||
|
Для дневной управленческой аналитики значение должно покрывать рабочий день
|
||||||
|
по всем активным сессиям. Для пилотного контура используется `5000`; малые
|
||||||
|
значения вроде `250` допустимы только для аварийного degraded-smoke, иначе
|
||||||
|
отчет будет построен по последнему хвосту событий, а не по полному дню.
|
||||||
- `AW_WORKTIME_AW_HTTP_TIMEOUT_SECONDS` - timeout запросов к ActivityWatch API.
|
- `AW_WORKTIME_AW_HTTP_TIMEOUT_SECONDS` - timeout запросов к ActivityWatch API.
|
||||||
- `AW_WORKTIME_SOURCE_HTTP_TIMEOUT_SECONDS` - timeout внешних source-запросов.
|
- `AW_WORKTIME_SOURCE_HTTP_TIMEOUT_SECONDS` - timeout внешних source-запросов.
|
||||||
- `AW_WORKTIME_REPORT_CACHE_TTL_SECONDS` - TTL fresh report cache.
|
- `AW_WORKTIME_REPORT_CACHE_TTL_SECONDS` - TTL fresh report cache.
|
||||||
@@ -129,7 +215,7 @@ systemctl restart aw-worktime-api
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS --max-time 12 \
|
curl -sS --max-time 12 \
|
||||||
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=HOST-EXAMPLE&allow_stale=1" \
|
"http://<AW_SERVER_HOST>:5610/reports/worktime/management?format=json&host=SHARKON2025&allow_stale=1" \
|
||||||
| jq '.status,.stale,.runtime'
|
| jq '.status,.stale,.runtime'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -171,6 +257,13 @@ systemctl restart aw-worktime-api
|
|||||||
Перед rollback убедитесь, что backup-файлы действительно относятся к предыдущей
|
Перед 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.
|
- `/reports/worktime/management` отвечает HTTP 200 в bounded time.
|
||||||
|
|||||||
Reference in New Issue
Block a user