Аудитория: ИТ-администратор. Закрывает вопрос: №4 («откуда берётся активность, это не слежка?») — со стороны механики. Сценарий: S5 — «почему сегодня собралось 0 записей / почему не задвоилось?».
Сбор работает так: отдельный контейнер-планировщик по расписанию дёргает коннектор каждого источника, тот отдаёт дневные записи активности, а приём обезличивает табельный и кладёт факты в БД идемпотентно — повторный сбор того же дня не плодит строки, а перезаписывает метрики теми же значениями. Каждый запуск оборачивается в прогон коллектора (collector_run), видимый на экране /collectors. Никаких «нажатий пользователей» система не пишет — только дневные агрегаты «в системе X было N транзакций и M минут».
⏰ planlovщик (контейнер collector) api (приём) db
┌──────────────────────────┐ records ┌─────────────────────────┐ факты ┌──────────────┐
│ cron «1-59/2» (нечётн. │ ────────► │ open_collector_run │ ──────► │ activity_ │
│ минуты), JOBS = │ │ anonymize(tabel_no) │ upsert │ facts │
│ арендаторы × источники │ │ sys_code→sys_id (RLS) │ ON │ (idempotent) │
│ get_collector(src) │ │ is_active_day=session≥60│ CONFLICT│ │
│ .collect(tenant) │ │ close_collector_run │ │ collector_ │
└──────────────────────────┘ └─────────────────────────┘ │ runs │
одна транзакция: прогон + факты ──────────────────────────────────► └──────────────┘Три звена конвейера: планировщик (отдельный контейнер collector), приём (ingest_activity) и коннекторы (единый контракт Collector, см. статью 03).
Планировщик — это APScheduler `BlockingScheduler` в одиночном контейнере, а не in-process в API. Причина: при нескольких воркерах uvicorn in-process планировщик запустился бы в каждом → двойной сбор. Один контейнер = ровно один экземпляр джоб.
- Расписание: cron
1-59/2 * * * *— нечётные минуты, никогда не :00 (чтобы не
совпадать с круглыми отметками других планировщиков). На стенде это ~каждые 2 минуты; переопределяется env COLLECTOR_CRON.
- Джобы:
JOBS = COLLECTED_TENANTS × SOURCE_TYPES, то есть каждый арендатор × каждый тип
источника. Публичный demo_public не собирается.
- Прогон на старте + по тикам: живой стенд получает факты сразу, не дожидаясь первого тика.
- Защита от наложения:
max_instances=1, coalesce=True— медленный прогон не порождает
второй параллельный.
ingest_activity (вызывается и из HTTP-роута, и напрямую из планировщика):
- Резолв системы:
sys_code → sys_idпоsam.systemsпод RLS — чужой арендатор не
найдётся (изоляция на уровне СУБД). Если sys_code пуст/неизвестен — фолбэк на распознаватель (врезка ниже).
- Обезличивание:
user_id = anonymize(tenant, tabel_no)— сырой табельный не хранится
(см. статью 02).
- Вывод активного дня:
is_active_day = session_minutes ≥ ACTIVE_DAY_MIN_MINUTES (60)—
коллектор флаг не шлёт, его выводит приём.
- Идемпотентный upsert:
ON CONFLICT (tenant_id, sys_id, user_id, activity_date) DO UPDATE.
Прогон и факты пишутся в одной транзакции: успех — атомарный commit, любая ошибка — полный rollback без частичного состояния.
Зерно уникальности — (арендатор, система, пользователь, дата) — закреплено уникальным индексом (migration 005, на партиционированном родителе). Повторный тик того же дня не добавляет строки, а перезаписывает tx_count/session_minutes/is_active_day теми же значениями. Поэтому:
- число строк
activity_factsмежду тиками стабильно (можно проверить повторным запросом); - прогоны (
collector_runs) при этом копятся — каждый тик это новый run на/collectors,
даже если фактов он не изменил. Это нормально: run — это «событие сбора», а не «изменение данных».
Переменная INGEST_MODE:
- `seed` (по умолчанию):
init_dbсразу засевает факты — стенд показывает данные без
ожидания коллектора.
- `live`:
init_dbпропускает засев фактов — единственным поставщиком становится
коллектор. Так проверяется, что сбор реально работает (а не «данные были в сиде»).
Близнец Static == Http == Ingested. И сид, и фикстуры коллектора питаются из одного генератора телеметрии. Поэтому собранные коллектором факты совпадают с засеянными — расхождению неоткуда взяться. Для админа это означает: цифры на стенде в режимах seed и live одинаковы (например, у artek covered=6, avg I_eff_ops=0.6585).### Врезка: распознавание ПО (фолбэк, если источник шлёт сырое имя) Реальные источники часто отдают не наш код системы, а сырое имя: «winword.exe», «1С:Зарплата и кадры, ред. 3». Еслиsys_codeпуст, приём вызывает распознавательrecognition.recognize(raw_product)— regex/substring-распознаватель приводит сырое имя к каноническому коду системы (счётчикrecognizedViaParser). Несанкционированное ПО (облака, мессенджеры), не распознанное как система реестра, отправляется в отдельный инвентарь Shadow IT (/risks) — оно не переписывает оценки риска санкционированных систем. Это демонстрационный срез; отдельная статья появится при выходе из демо-режима.
/collectors и диагностировать S5| Симптом | Что смотреть | Норма / действие |
|---|---|---|
| «Собралось 0 записей» | applied в прогоне | Источник без систем у арендатора (напр. 1С у РГГУ) — норма; иначе проверить фикстуру/подключение |
«Много skippedUnknownSystem» | результат приёма | Источник шлёт незнакомые коды и пустой raw_product — заполнить raw_product или добавить систему в реестр |
| «Прогоны копятся, а данные те же» | число строк activity_facts | Норма: идемпотентность; run ≠ изменение |
| «Данные не обновились после правки кода» | пересборка образа | Грабли BuildKit — собирать DOCKER_BUILDKIT=0 |
Полезные команды:
docker logs licenziar-collector-1 # события collector.run.ok (tenant, source, applied)
curl http://localhost:8001/api/artek/ops-efficiency
# чистый прогон миграций требует чистого тома:
docker compose -f docker-compose.phase1.yml down -v
ANON_SALT=test-salt docker compose -f docker-compose.phase1.yml up --build- Чистый том перед миграциями. Без
down -vфлагalready_seededпропустит новые
миграции — данные/схема останутся старыми.
- Одна `ANON_SALT` на сиде, приёме и тестах. Иначе
user_idне совпадут (на стенде —
test-salt).
- Прогоны (`collector_runs`) растут со временем — это лог событий сбора, а не утечка;
при необходимости чистится регламентно.
Связанные статьи: 03 — Коннекторы · 02 — ПДн и Vault · 06 — Чтение эффективности.