# Телеметрия использования: usage_telemetry

*Для разработчиков. Состояние на 28.09.2026. Подробности и переменные — README репозитория `usage_telemetry`. Выкатка коллектора и дашборд лежат в репозитории `infra`, про него — статья «Мониторинг и автоматизация серверов: infra».*

## Коротко

`usage_telemetry` отвечает на вопрос, какими приложениями и какими их разделами пользуются живые люди. Цель — отличить приложение, которое висит мёртвым грузом, от приложения, которым пользуются редко, но по делу, и выключать только первое. Проект состоит из двух частей:

- **пакет** `usage-telemetry`: встраивается в приложение одной строкой и считает обращения;
- **коллектор**: принимает события, складывает их в PostgreSQL и раз в час пересчитывает суточные своды.

Результат смотрят в Grafana, на дашборде «Использование приложений».

```
приложение (middleware) ──пачками по HTTP──> коллектор ──> PostgreSQL ──> Grafana
```

## Как подключить к проекту

1. Добавить пакет в `requirements.txt` (или в `pyproject.toml`), закрепив тег: ```
    usage-telemetry @ https://github.com/Buharestskie/usage_telemetry/archive/refs/tags/v0.2.0.tar.gz
    ```
    
    Для отправки ошибок в GlitchTip нужен ещё `sentry-sdk` (extra `sentry`).
2. Повесить сбор на приложение FastAPI или Starlette: ```
    from usage_telemetry import install_telemetry
    
    app = FastAPI()
    install_telemetry(app)
    ```
    
    Если задан `SENTRY_DSN`, `install_telemetry` заодно инициализирует GlitchTip. Если в проекте SDK уже поднят своим кодом, вызывайте `install_telemetry(app, sentry=False)`.
3. Сообщить, кто пришёл. В зависимости авторизации, где сессия или токен уже разобраны: ```
    from usage_telemetry import mark_user
    
    mark_user(request, user_id=session.amocrm_user_id, auth="session", session_id=session_id)
    ```
    
    Значения `auth`: `session` — человек в браузере; `jwt` — виджет amoCRM с токеном от `widget_auth`; `telegram` — бот или Mini App; `basic` — админки; `api_key` — вызов между сервисами; `none` — без авторизации. Без `mark_user` вид входа угадывается по заголовкам, но идентификатора пользователя не будет.
4. Для бота на aiogram 3: ```
    from usage_telemetry import install_bot_telemetry
    
    install_bot_telemetry(dispatcher)
    ```
5. Добавить переменные в compose проекта и выкатить в окне обслуживания.

<table id="bkmrk-%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F%D0%9E%D0%B1%D1%8F%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD"><thead><tr><th>Переменная</th><th>Обязательна</th><th>Что означает</th></tr></thead><tbody><tr><td>`TELEMETRY_SERVICE`</td><td>да</td><td>машинный код приложения. Должен совпадать с `project` этого сервиса в `status_page_services` (`infra`)</td></tr><tr><td>`TELEMETRY_URL`</td><td>да</td><td>адрес коллектора. Приложения на одном сервере с коллектором используют `http://host.docker.internal:8130`, с других серверов — `https://donula26.ru/telemetry`</td></tr><tr><td>`TELEMETRY_ENABLED`</td><td>нет</td><td>`0` выключает сбор целиком</td></tr><tr><td>`TELEMETRY_QUEUE_SIZE`, `TELEMETRY_BATCH_SIZE`, `TELEMETRY_FLUSH_SECONDS`, `TELEMETRY_TIMEOUT`</td><td>нет</td><td>очередь (5000), размер пачки (200), интервал отправки неполной пачки (5 с), таймаут запроса (3 с)</td></tr><tr><td>`SENTRY_DSN`</td><td>нет</td><td>проект в GlitchTip; без него ошибки не отправляются</td></tr></tbody></table>

Если не задан `TELEMETRY_SERVICE` или `TELEMETRY_URL`, пакет пишет предупреждение в лог и ничего не собирает. Приложение при этом работает как обычно.

## Что собирается

Одно событие на одно обращение к приложению:

<table id="bkmrk-%D0%9F%D0%BE%D0%BB%D0%B5%D0%A7%D1%82%D0%BE-%D0%B2-%D0%BD%D1%91%D0%BC-at%2C-se"><thead><tr><th>Поле</th><th>Что в нём</th></tr></thead><tbody><tr><td>`at`, `service`, `source`</td><td>время, код приложения, источник: `http` или `bot`</td></tr><tr><td>`method`, `path`</td><td>метод и **шаблон** пути из маршрута (`/api/deals/{id}`, а не `/api/deals/451`). Если маршрута нет, путь нормализуется: числа заменяются на `{id}`, UUID — на `{uuid}`, длинные непрозрачные сегменты — на `{token}`</td></tr><tr><td>`status`, `duration_ms`</td><td>код ответа и длительность</td></tr><tr><td>`auth`, `user_id`, `session_id`</td><td>чем запрос аутентифицирован и чей идентификатор из уже существующей авторизации: `amocrm_user_id`, id из JWT, `telegram_id`</td></tr><tr><td>`user_agent`</td><td>первые 200 символов</td></tr><tr><td>`client_ip`</td><td>доходит только до коллектора. Там из него, user-agent и общей соли считается отпечаток посетителя, а сам адрес не сохраняется</td></tr></tbody></table>

- **Не пишется никогда:** строка запроса, тело, referer, имена и почта. В строке запроса ходят разовые токены, а хранилище не должно превращаться в базу утёкших секретов.
- **Отсекается на входе:** `OPTIONS`, `/health`, `/healthz`, `/metrics`, `/ping`, `/favicon.ico`, `/robots.txt`, пути под `/static/`, `/assets/`, `/_next/`, `/_app/` и файлы статики по расширению. Свои исключения добавляются параметрами `skip_paths` и `skip_prefixes`.
- **В ботах** считаются только входящие от людей: сообщения (команда с короткой меткой вроде `/start beeline` или тип вложения) и нажатия кнопок. Данные кнопки сводятся к шаблону: `service:1842` превращается в `service:{id}`. Рассылки не считаются, прочтения Telegram ботам не отдаёт.

## Как устроено внутри

- **Приложение ничего не ждёт.** Middleware кладёт событие в очередь в памяти, фоновая задача отправляет пачки в `POST /events`. Если коллектор недоступен, пачка выбрасывается без повторов. Если очередь переполнилась, она сбрасывается целиком. При остановке приложения остаток отправляется.
- **Коллектор** — FastAPI с PostgreSQL. Сырые события хранятся в таблице `events` 90 дней. Раз в час коллектор пересчитывает своды за последние трое суток и чистит устаревшее сырьё. Своды хранятся бессрочно: 
    - `daily_users` — человеко-дни: пара «день + человек». В этот свод попадают только обращения с `auth` `session`, `jwt`, `telegram` или `basic` и с известным `user_id`. Вызовы между сервисами сюда не входят;
    - `daily_paths` — разделы по дням: число обращений, людей, посетителей по отпечатку, ошибок 5xx и 95-й перцентиль длительности.
- **Справочник приложений** (таблица `services`) собирается из `status_page_services` и `telemetry_extra_services` в инвентаре `infra`. Коллектор читает его только при старте. По отметке первого события видно, раскатан ли сбор в приложение.
- **Ручки коллектора:** `POST /events` (ответ `{"accepted": N}`), `GET /health`, `GET /stats` — сколько накоплено и когда был последний свод. Коллектор слушает только внутренний интерфейс сервера. С других серверов события идут через путь `/telemetry/` на donula26.ru, куда пускают только адреса из списка в роли `web_proxy`.

## Где смотреть

Grafana, grafana.donula26.ru → папка «Инфраструктура» → дашборд «Использование приложений». Период по умолчанию — 30 дней.

<table id="bkmrk-%D0%9F%D0%B0%D0%BD%D0%B5%D0%BB%D1%8C%D0%A7%D1%82%D0%BE-%D0%BF%D0%BE%D0%BA%D0%B0%D0%B7%D1%8B%D0%B2%D0%B0%D0%B5%D1%82"><thead><tr><th>Панель</th><th>Что показывает</th></tr></thead><tbody><tr><td>Приложений под наблюдением</td><td>сколько приложений в справочнике</td></tr><tr><td>Сбор не раскатан</td><td>приложения, от которых не пришло ни одного события. Пока здесь не ноль, выводы о неиспользуемых делать рано</td></tr><tr><td>Без единого обращения за период</td><td>сбор работает, а обращений не было — кандидаты на выключение</td></tr><tr><td>Приложения по живости</td><td>главная таблица: обращения, люди, посетители (оценка), человеко-дни, последний заход и замечание: «сбор не подключён», «никто не заходил», «люди не опознаются» или «только оценка по отпечатку»</td></tr><tr><td>Человеко-дни по приложениям</td><td>график по дням</td></tr><tr><td>Разделы и данные</td><td>разделы выбранного приложения (переменная «Приложение»)</td></tr><tr><td>Кто пользуется</td><td>идентификаторы пользователей выбранного приложения, без имён: нужны, чтобы предупредить людей перед выключением</td></tr><tr><td>Медленное и ошибающееся</td><td>разделы с ошибками или с 95-м перцентилем больше 3 секунд</td></tr></tbody></table>

Дашборд генерируется скриптом `roles/monitoring_server/files/dashboards/generate_usage.py` в `infra`. В браузере он не редактируется.

## Выкатка

- **Коллектор** выкатывается из репозитория `infra`: `ansible-playbook playbooks/telemetry_collector.yml`. Роль забирает код из ветки `main` этого репозитория, выпускает самоподписанный сертификат для базы, один раз создаёт соль отпечатка, раскладывает справочник приложений и поднимает compose. Пароль базы берётся из Vault (`telemetry_collector_db_password`), без него роль останавливается. База входит в ежедневные бэкапы.
- **Пакет** приезжает в приложения тегом в зависимостях. Новая версия пакета — это новый тег здесь и правка строки в каждом проекте.
- **Проверки:** `python tests/test_telemetry.py` проверяет пакет без базы и сети. `COLLECTOR_TEST_DSN=… python collector/tests/test_collector.py` проверяет коллектор и требует настоящий PostgreSQL: тест работает в отдельной схеме и удаляет её за собой.

## На что наступают

- **`TELEMETRY_SERVICE` не совпал с `project` в `status_page_services`.** События пишутся, но приложение не попадает в список ожидаемых и на дашборде в таблице живости не видно. Приложениям без пробы на статус-странице код прописывают в `telemetry_extra_services`.
- **После правки справочника в инвентаре** коллектор нужно перезапустить. Роль `telemetry_collector` делает это сама.
- **События теряются осознанно.** Коллектор лежал — в данных дыра. Приложение, которое молчит, и приложение, которым не пользуются, на сводах выглядят одинаково. Различают их по колонке «Замечание».
- **Отпечаток — оценка, а не счёт людей.** Сотрудники из офиса сидят за одним адресом, и различает их только user-agent: одинаковые браузеры схлопнутся в одного посетителя.
- **Приложение на другом сервере шлёт в пустоту,** если адреса этого сервера нет в списке для `/telemetry/` в роли `web_proxy`: nginx отвечает 403, пакет молча выбрасывает события.
- **Без привязки к завершению приложения** последняя пачка теряется при каждом перезапуске. `install_telemetry` привязывается к `lifespan`, а если не получилось, пишет предупреждение в лог. Его не игнорируйте.
- **Не у каждого бота есть `Dispatcher`.** У односторонних ботов, которые только рассылают, входящих от людей нет, и мерить там нечего.