Compare commits
6
Commits
1fccaf8599
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39f613a01 | ||
|
|
335e5e7612 | ||
|
|
ffd467bf43 | ||
|
|
dda4f3aeb5 | ||
|
|
05e4cdea4d | ||
|
|
9d3a8053d3 |
No files matched your search
@@ -1,78 +0,0 @@
|
|||||||
# Handoff: спека «Боевой реализм стенда (v2)» — ревью и приёмка (#17)
|
|
||||||
|
|
||||||
Дата: 2026-07-30 10:03. Сессия работала над issue #17 (Gitea, CLI `tea`):
|
|
||||||
сборка спеки из резолюций карты #10.
|
|
||||||
|
|
||||||
## Состояние
|
|
||||||
|
|
||||||
- Спека: `docs/specs/2026-07-30-stand-v2-realism.md` (Draft).
|
|
||||||
**Файл untracked** — при сбое машины git его не защитит; владельцу
|
|
||||||
предложен WIP-коммит.
|
|
||||||
- Источники истины: резолюции в комментариях issues #14, #15, #16, #18;
|
|
||||||
карта #10 (там же решение: v2 — новый пустой репозиторий, рабочее имя
|
|
||||||
`clickstream-data-platform`, v1 замораживается). Фактура:
|
|
||||||
`docs/research/2026-07-26-yandex-clickstream-format.md`.
|
|
||||||
|
|
||||||
## Что сделано
|
|
||||||
|
|
||||||
1. Спека собрана и прошла четыре волны правок (всё уже в файле):
|
|
||||||
владельца (v2 — новый репозиторий; доки переосмыслить, не копировать;
|
|
||||||
лабы с нуля, вне скоупа; генератор с нуля), вычитки (термины, `K` только
|
|
||||||
как окно), сверки с резолюциями (12 правок: бюджет 16 ГБ, «ландшафт
|
|
||||||
итогом», потерянные отклонения, помеченные отступления) и грилинга
|
|
||||||
(17 решений: расщеплённый дедуп заказов, пересборка K+1 партиций,
|
|
||||||
сессии по границе суток, обязательный заказ с каждой куки двухкукового,
|
|
||||||
сверка по сравнимой базе, строгий приём JSON, `dds.v_event`,
|
|
||||||
`SAMPLE BY`, «схема как контракт», объём ~80–110 файлов, этапы слиты
|
|
||||||
и дополнены этапом Airflow).
|
|
||||||
2. Решение владельца по генератору: Python с производительной
|
|
||||||
архитектурой; компилируемый язык — только если замеры не пройдут
|
|
||||||
(в спеке, раздел 11).
|
|
||||||
|
|
||||||
## Открытые вопросы владельцу (спека их пометила как отступления)
|
|
||||||
|
|
||||||
- Порядок страховочных срезов: #15 предписывал 1→2→3, спека применяет
|
|
||||||
3 и 2, срез 1 (статусы) держит в резерве.
|
|
||||||
- `Sign` на хитах: исследование советовало взять колонку, спека не берёт.
|
|
||||||
- `VisitID` оставлен в событии как эталон самопроверки лабы сессий.
|
|
||||||
|
|
||||||
## В полёте на момент записки
|
|
||||||
|
|
||||||
- Sonnet-агент довносил 17 правок грилинга: маркеры всех правок в файле
|
|
||||||
уже есть; свежей сессии — убедиться, что файл цел (маркеры:
|
|
||||||
`skip_unknown_fields`, `_ingested_at`, `dds.v_event`, `SAMPLE BY`,
|
|
||||||
«сравнимой базе», «80–110», «Схема как контракт», «каждой куки»).
|
|
||||||
- Слепое ревью спеки Кодексом подготовлено, НЕ запущено. Промпт собран в
|
|
||||||
`$SCRATCH/spec-review/review-prompt.md` (скретчпад сессии в /tmp — мог
|
|
||||||
не пережить сбой). Пересборка: параметрический блок (роль: ревью
|
|
||||||
постановки, не реализации; префикс находок SOL; вне скоупа — стиль и
|
|
||||||
вкусовщина) + `references/prompts/review-boilerplate.md` из скилла
|
|
||||||
`claude-subagent-playbook`. Запуск — по ветке «ревьюер»
|
|
||||||
`references/codex-exec.md`: `codex exec`, модель `gpt-5.6-sol`, effort
|
|
||||||
high, фоновым Bash, вердикт в `-o`-файл. Песочница проверена — жива.
|
|
||||||
Предупреждение владельца: у Сола плохой вкус и тяга к деталям
|
|
||||||
реализации раньше времени — рамка в промпте это прижимает, остаток
|
|
||||||
фильтровать на триаже.
|
|
||||||
|
|
||||||
## Дальше (по порядку)
|
|
||||||
|
|
||||||
1. Проверить целостность 17 правок; финальное сквозное чтение спеки.
|
|
||||||
2. Запустить ревью Сола, триаж находок (вкусовые — отклонять), правки.
|
|
||||||
3. Вердикт владельца по трём открытым вопросам → статус Accepted.
|
|
||||||
4. Коммит (`/conventional-commits`), комментарий-резолюция в #17.
|
|
||||||
5. Разбиение через /to-tickets — уже отдельным заходом (спека и карта
|
|
||||||
переедут в v2-репозиторий после его создания).
|
|
||||||
|
|
||||||
## Грабли сессии
|
|
||||||
|
|
||||||
- Дважды 529 Overloaded на Opus-субагентах: правки терялись ДО записи.
|
|
||||||
Рецепт: проверять маркеры в файле, затем возобновлять агента
|
|
||||||
SendMessage'ем; после второго падения — свежий Sonnet-агент с
|
|
||||||
самодостаточным списком правок (сработало).
|
|
||||||
|
|
||||||
## Suggested skills
|
|
||||||
|
|
||||||
- `/claude-subagent-playbook` — продолжить конвейер ревью (этапы 4–6).
|
|
||||||
- `/conventional-commits` — при коммите спеки.
|
|
||||||
- `/grilling` — если владелец захочет добить три открытых вопроса.
|
|
||||||
- Контракт трекера — `docs/agents/issue-tracker.md` (Gitea, `tea`).
|
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
[](./docker-compose.yml)
|
[](./docker-compose.yml)
|
||||||
[](./docs/ARCHITECTURE.md)
|
[](./docs/ARCHITECTURE.md)
|
||||||
|
|
||||||
|
> **Стенд заморожен для новых фич.** Он остаётся стабильным учебным стендом:
|
||||||
|
> что здесь работает, то работает и дальше — курс и лабы живут тут.
|
||||||
|
> Развитие переехало в
|
||||||
|
> [clickstream-data-platform](https://git.dementev.space/ddmitry/clickstream-data-platform):
|
||||||
|
> там одно широкое событие кликстрима вместо четырёх топиков, заказы бэкенда
|
||||||
|
> вторым источником и ClickHouse кластером.
|
||||||
|
|
||||||
Живой стек для работы с кликстримом: Kafka, ClickHouse, Airflow, Superset и мониторинг
|
Живой стек для работы с кликстримом: Kafka, ClickHouse, Airflow, Superset и мониторинг
|
||||||
(Prometheus с Grafana) поднимаются в Docker одной командой. На этом стенде можно учиться
|
(Prometheus с Grafana) поднимаются в Docker одной командой. На этом стенде можно учиться
|
||||||
по курсу или просто поднять его у себя и поэкспериментировать с потоковой загрузкой и
|
по курсу или просто поднять его у себя и поэкспериментировать с потоковой загрузкой и
|
||||||
|
|||||||
@@ -33,9 +33,8 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 0.1
|
type: gt
|
||||||
type: lt
|
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
@@ -51,16 +50,17 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 0.1
|
||||||
type: gt
|
type: lt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
@@ -114,8 +114,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 50
|
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -132,15 +131,16 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 50
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -164,7 +164,7 @@ groups:
|
|||||||
for: 5m
|
for: 5m
|
||||||
annotations:
|
annotations:
|
||||||
summary: "В очереди Airflow > 50 задач"
|
summary: "В очереди Airflow > 50 задач"
|
||||||
description: "airflow_executor_queued_tasks = {{ $values.A }} (превышает 50 в течение 5 минут) — возможна перегрузка"
|
description: "airflow_executor_queued_tasks = {{ $values.B.Value }} (превышает 50 в течение 5 минут) — возможна перегрузка"
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
|
|
||||||
@@ -195,8 +195,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 0.1
|
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -213,15 +212,16 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 0.1
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -245,7 +245,7 @@ groups:
|
|||||||
for: 3m
|
for: 3m
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Высокий rate падений тасков"
|
summary: "Высокий rate падений тасков"
|
||||||
description: "rate(airflow_task_failures_total) = {{ $values.A }} (превышает 0.1 в течение 3 минут) — проверьте логи DAG"
|
description: "rate(airflow_task_failures_total) = {{ $values.B.Value }} (превышает 0.1 в течение 3 минут) — проверьте логи DAG"
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
|
|
||||||
@@ -276,8 +276,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 30
|
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -294,15 +293,16 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 30
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -326,6 +326,6 @@ groups:
|
|||||||
for: 5m
|
for: 5m
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Парсинг DAG занимает > 30 секунд"
|
summary: "Парсинг DAG занимает > 30 секунд"
|
||||||
description: "airflow_dag_processing_total_parse_time = {{ $values.A }}s — возможно, есть тяжелые DAG"
|
description: "airflow_dag_processing_total_parse_time = {{ $values.B.Value }}s — возможно, есть тяжелые DAG"
|
||||||
labels:
|
labels:
|
||||||
severity: info
|
severity: info
|
||||||
@@ -33,9 +33,8 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 1
|
type: gt
|
||||||
type: lt
|
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
@@ -51,16 +50,17 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 1
|
||||||
type: gt
|
type: lt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
@@ -83,7 +83,7 @@ groups:
|
|||||||
for: 1m
|
for: 1m
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Kafka broker недоступен"
|
summary: "Kafka broker недоступен"
|
||||||
description: "Количество доступных брокеров: {{ $values.A }} (ожидается ≥ 1)"
|
description: "Количество доступных брокеров: {{ $values.B.Value }} (ожидается ≥ 1)"
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
|
|
||||||
@@ -114,8 +114,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 10000
|
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -132,15 +131,16 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 10000
|
||||||
type: gt
|
type: gt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
@@ -164,7 +164,7 @@ groups:
|
|||||||
for: 5m
|
for: 5m
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Высокий лаг консьюмера {{ $labels.consumergroup }}"
|
summary: "Высокий лаг консьюмера {{ $labels.consumergroup }}"
|
||||||
description: "Lag для {{ $labels.topic }}:{{ $labels.partition }} составляет {{ $values.A }} сообщений (порог: 10000)"
|
description: "Lag для {{ $labels.topic }}:{{ $labels.partition }} составляет {{ $values.B.Value }} сообщений (порог: 10000)"
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
|
|
||||||
@@ -195,9 +195,8 @@ groups:
|
|||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params: []
|
||||||
- 0.1
|
type: gt
|
||||||
type: lt
|
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
@@ -213,16 +212,17 @@ groups:
|
|||||||
expression: A
|
expression: A
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
maxDataPoints: 43200
|
maxDataPoints: 43200
|
||||||
|
reducer: last
|
||||||
refId: B
|
refId: B
|
||||||
type: threshold
|
type: reduce
|
||||||
- refId: C
|
- refId: C
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model:
|
model:
|
||||||
conditions:
|
conditions:
|
||||||
- evaluator:
|
- evaluator:
|
||||||
params:
|
params:
|
||||||
- 0
|
- 0.1
|
||||||
type: gt
|
type: lt
|
||||||
operator:
|
operator:
|
||||||
type: and
|
type: and
|
||||||
query:
|
query:
|
||||||
|
|||||||
@@ -304,9 +304,10 @@ docker compose ps airflow-scheduler
|
|||||||
docker compose stop airflow-scheduler
|
docker compose stop airflow-scheduler
|
||||||
```
|
```
|
||||||
|
|
||||||
Подожди 2-3 минуты. У правила `Airflow Scheduler Down` стоит окно `for: 2m`, поэтому алерт не
|
Подожди около 5 минут. После остановки scheduler значение `rate(...[5m])` снижается постепенно.
|
||||||
обязан покраснеть мгновенно. Это нормальное поведение: мониторинг защищается от коротких
|
При интервале heartbeat в 5 секунд порог будет пройден примерно через 2,5 минуты. Затем Grafana
|
||||||
миганий.
|
ещё 2 минуты держит правило в состоянии `Pending` из-за `for: 2m`. Точное время зависит от
|
||||||
|
моментов сбора метрик и вычисления правила.
|
||||||
|
|
||||||
Что ожидаем:
|
Что ожидаем:
|
||||||
|
|
||||||
@@ -326,7 +327,8 @@ docker compose stop airflow-scheduler
|
|||||||
docker compose up -d airflow-scheduler
|
docker compose up -d airflow-scheduler
|
||||||
```
|
```
|
||||||
|
|
||||||
Подожди ещё 2-3 минуты и проверь:
|
Подожди ещё около 3 минут: пятиминутному окну `rate` нужно накопить достаточно новых сигналов
|
||||||
|
heartbeat. Затем проверь:
|
||||||
|
|
||||||
- `docker compose ps airflow-scheduler` показывает running/up;
|
- `docker compose ps airflow-scheduler` показывает running/up;
|
||||||
- **Scheduler Heartbeat Rate** снова растёт;
|
- **Scheduler Heartbeat Rate** снова растёт;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
# Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность
|
# Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность
|
||||||
|
|
||||||
Статус: Draft — на приёмку владельцу.
|
Статус: Accepted (2026-07-30). Три помеченных отступления подтверждены
|
||||||
|
владельцем на приёмке: порядок страховочных срезов (раздел 9), `Sign` как
|
||||||
|
колонка без механики (раздел 1.1), `VisitID` как эталон самопроверки (1.2).
|
||||||
Дата: 2026-07-30. Тикет: #17 (сборка карты #10).
|
Дата: 2026-07-30. Тикет: #17 (сборка карты #10).
|
||||||
|
Источник истины переехал в v2:
|
||||||
|
https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/specs/2026-07-30-stand-v2-realism.md
|
||||||
Источники: резолюции #18 (модель данных), #15 (`purchase` и заказы),
|
Источники: резолюции #18 (модель данных), #15 (`purchase` и заказы),
|
||||||
#14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование
|
#14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование
|
||||||
[формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md);
|
[формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md);
|
||||||
@@ -64,10 +68,13 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
|
|||||||
тоже не берём, но это наш выбор, а не запрет источника: исследование
|
тоже не берём, но это наш выбор, а не запрет источника: исследование
|
||||||
запрещало только выдавать это поле за формат Яндекса, оставить разрешало.
|
запрещало только выдавать это поле за формат Яндекса, оставить разрешало.
|
||||||
Разбор строки user agent — не урок этого стенда.
|
Разбор строки user agent — не урок этого стенда.
|
||||||
- **Механику версий записи (`Sign`/`HitVersion`) не берём сейчас**: по #15 это
|
- **`Sign` берём как колонку формата, без механики** (решение владельца на
|
||||||
кандидат на потом, и её дом — клиентская сторона (поток визитов Метрики
|
приёмке спеки): генератор всегда пишет `Sign = 1`, исправлений записей не
|
||||||
Про). Это помеченное отступление от рекомендации исследования («`Sign` на
|
шлёт — движки и запросы не меняются. Сама механика версий
|
||||||
хитах полезен»): колонка без механики — мёртвый вес.
|
(CollapsingMergeTree, пара `HitVersion`) — кандидат на потом, по #15.
|
||||||
|
Честность: комментарий в DDL и абзац в документе о реализме («в бою здесь
|
||||||
|
бывают −1/+1, считают через `sum(Sign)`»); в лекции — крючок про
|
||||||
|
CollapsingMergeTree (частый вопрос на собеседованиях).
|
||||||
- **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки;
|
- **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки;
|
||||||
расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не
|
расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не
|
||||||
нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`,
|
нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`,
|
||||||
@@ -78,7 +85,7 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
|
|||||||
- **Наша честная добавка** — `EventType`: у Метрики такого поля нет
|
- **Наша честная добавка** — `EventType`: у Метрики такого поля нет
|
||||||
(там `isPageView` + `productEventType`), стенду таксономия нужна явно.
|
(там `isPageView` + `productEventType`), стенду таксономия нужна явно.
|
||||||
|
|
||||||
### 1.2 Состав полей (46 колонок)
|
### 1.2 Состав полей (47 колонок)
|
||||||
|
|
||||||
Идентификаторы и время:
|
Идентификаторы и время:
|
||||||
|
|
||||||
@@ -92,6 +99,7 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
|
|||||||
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
|
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
|
||||||
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
||||||
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
||||||
|
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
|
||||||
|
|
||||||
Правила резки визитов в генераторе документируются и совпадают с лабной
|
Правила резки визитов в генераторе документируются и совпадают с лабной
|
||||||
логикой (30-минутный таймаут).
|
логикой (30-минутный таймаут).
|
||||||
@@ -155,7 +163,7 @@ Ecommerce (заполнены только у торговых событий):
|
|||||||
### 1.4 Схема как контракт
|
### 1.4 Схема как контракт
|
||||||
|
|
||||||
Одно машинное описание схемы события (python-модуль или YAML) — источник
|
Одно машинное описание схемы события (python-модуль или YAML) — источник
|
||||||
истины: из него выводятся DDL и валидация генератора, а не наоборот. 46
|
истины: из него выводятся DDL и валидация генератора, а не наоборот. 47
|
||||||
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью,
|
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью,
|
||||||
трансформации, витрины, манифест, доки) — без контракта они расходятся
|
трансформации, витрины, манифест, доки) — без контракта они расходятся
|
||||||
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое
|
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое
|
||||||
@@ -196,7 +204,9 @@ Ecommerce (заполнены только у торговых событий):
|
|||||||
|
|
||||||
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
||||||
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
|
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
|
||||||
витрины работают с плоскими массивами. Это единственный носитель навыка
|
витрины работают с плоскими массивами `dds.order`: `item_sku`
|
||||||
|
Array(String), `item_qty` Array(UInt64), `item_price` Array(Decimal(18,2))
|
||||||
|
— одной длины, порядок как в JSON. Это единственный носитель навыка
|
||||||
«вложенный JSON в ClickHouse» на стенде.
|
«вложенный JSON в ClickHouse» на стенде.
|
||||||
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
||||||
выручки внутри окна; сужение до двух — резервный срез 1.
|
выручки внутри окна; сужение до двух — резервный срез 1.
|
||||||
@@ -212,7 +222,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
|||||||
## 4. Сверка `purchase` против заказов
|
## 4. Сверка `purchase` против заказов
|
||||||
|
|
||||||
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
|
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
|
||||||
заказа на `/confirmation`). Расхождения — перечислимый список,
|
заказа на `/confirmation`). У события `purchase` массив `purchaseID` несёт
|
||||||
|
ровно один элемент (одно подтверждение — один заказ), сверка соединяет по
|
||||||
|
`purchaseID[1]`; правило зафиксировать комментарием в SQL сверки.
|
||||||
|
Расхождения — перечислимый список,
|
||||||
детерминированный от seed, не хаос:
|
детерминированный от seed, не хаос:
|
||||||
|
|
||||||
| | Расхождение | Механика в генераторе | Ориентир доли |
|
| | Расхождение | Механика в генераторе | Ориентир доли |
|
||||||
@@ -235,7 +248,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
|||||||
механически дублирует B.
|
механически дублирует B.
|
||||||
|
|
||||||
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
||||||
бэкенду**. Оно выучивается на конфликте: суммы не сойдутся, менти сам
|
бэкенду**. Единственное разрешённое исключение — клиентская оценка выручки
|
||||||
|
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
|
||||||
|
слово `declared` в имени — сигнал «это заявка клиента, не деньги
|
||||||
|
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
|
||||||
раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
|
раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
|
||||||
|
|
||||||
## 5. Анонимность и склейка идентичностей
|
## 5. Анонимность и склейка идентичностей
|
||||||
@@ -286,7 +302,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
|||||||
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
|
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
|
||||||
прогонами, куда легли данные — нет».
|
прогонами, куда легли данные — нет».
|
||||||
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
|
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
|
||||||
поля — без значений по умолчанию. Несовпадение имени поля — громкая
|
поля — без значений по умолчанию. Контракт присутствия: генератор выдаёт
|
||||||
|
**все 47 полей в каждом событии**; «пусто» — пустой массив, пустая строка
|
||||||
|
или 0, а не отсутствие ключа в JSON. Так строгий приём уживается с
|
||||||
|
полями, пустыми по смыслу (ecommerce у `pageview`, UTM у прямого захода). Несовпадение имени поля — громкая
|
||||||
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
|
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
|
||||||
опечатка иначе не падает.
|
опечатка иначе не падает.
|
||||||
- **Политика соединений**: по ключу ко-локации — обычное соединение с
|
- **Политика соединений**: по ключу ко-локации — обычное соединение с
|
||||||
@@ -370,8 +389,11 @@ Kafka переобработка возможна только из эталон
|
|||||||
в порядке приоритета — классы пересекаются, побеждает более ранний).
|
в порядке приоритета — классы пересекаются, побеждает более ранний).
|
||||||
`match` — большинство строк; `amount_delta` — только необъяснённый остаток
|
`match` — большинство строк; `amount_delta` — только необъяснённый остаток
|
||||||
после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
|
после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
|
||||||
Строка «`purchase` без заказа» внутри живого окна — это опоздание, ждущее
|
Строка «`purchase` без заказа» внутри живого окна — опоздание, ждущее
|
||||||
слепка, а не отдельный класс расхождения.
|
слепка, а не расхождение: она получает служебный класс `awaiting_order`
|
||||||
|
(шестое значение `mismatch_class`, вне приоритетов расхождений). После
|
||||||
|
закрытия окна K таких строк не остаётся — сироты исключены построением
|
||||||
|
(раздел 4).
|
||||||
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
|
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
|
||||||
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
||||||
`declared_revenue` по UTM.
|
`declared_revenue` по UTM.
|
||||||
@@ -398,7 +420,7 @@ Kafka переобработка возможна только из эталон
|
|||||||
Манифест расширяется контрольными числами:
|
Манифест расширяется контрольными числами:
|
||||||
|
|
||||||
- заказная сторона: заказы и выручка по дням; манифест хранит точные
|
- заказная сторона: заказы и выручка по дням; манифест хранит точные
|
||||||
счётчики по каждому классу расхождений (отмены, потери, дубли) —
|
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
|
||||||
самопроверка лабы сверки;
|
самопроверка лабы сверки;
|
||||||
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
|
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
|
||||||
покупателей — лаба склейки получает самопроверку.
|
покупателей — лаба склейки получает самопроверку.
|
||||||
@@ -469,7 +491,8 @@ v2 стартует пустым, поэтому объём ниже — это
|
|||||||
9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
|
9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
|
||||||
|
|
||||||
Критерий приёмки этапа — честный: `make up` работает и проходят
|
Критерий приёмки этапа — честный: `make up` работает и проходят
|
||||||
smoke-проверки, а не «дашборд зелёный». Документация правится в PR этапа
|
smoke-проверки, а не «дашборд зелёный». Это минимальная планка; свои
|
||||||
|
наблюдаемые критерии каждый этап получает при разбиении в /to-tickets. Документация правится в PR этапа
|
||||||
(правило AGENTS.md).
|
(правило AGENTS.md).
|
||||||
|
|
||||||
## 10. Границы: чего не делаем
|
## 10. Границы: чего не делаем
|
||||||
@@ -486,8 +509,8 @@ smoke-проверки, а не «дашборд зелёный». Докуме
|
|||||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||||
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
||||||
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
||||||
- `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток визитов
|
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
|
||||||
Метрики Про).
|
визитов Метрики Про); сама колонка `Sign` уже в схеме, статикой (см. 1.1).
|
||||||
- Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки,
|
- Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки,
|
||||||
отдельный поток возвратов — отклонены в #15/#18.
|
отдельный поток возвратов — отклонены в #15/#18.
|
||||||
- Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас
|
- Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас
|
||||||
@@ -532,7 +555,8 @@ smoke-проверки, а не «дашборд зелёный». Докуме
|
|||||||
Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок
|
Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок
|
||||||
надёжнее» + сноска про compacted topic; identity stitching перестаёт быть
|
надёжнее» + сноска про compacted topic; identity stitching перестаёт быть
|
||||||
чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или
|
чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или
|
||||||
выгрузка»; полный словарь торговых событий — теория.
|
выгрузка»; колонка `Sign` без механики — честное ограничение стенда
|
||||||
|
(в бою −1/+1 и `sum(Sign)`); полный словарь торговых событий — теория.
|
||||||
|
|
||||||
В v1 при заморозке — указатель на v2 в README (форму решить при рождении
|
В v1 при заморозке — указатель на v2 в README (форму решить при рождении
|
||||||
v2, этап 0).
|
v2, этап 0).
|
||||||
@@ -549,6 +573,11 @@ v2, этап 0).
|
|||||||
- сцена «разные consumer groups → дубли»;
|
- сцена «разные consumer groups → дубли»;
|
||||||
- словарь регионов из CSV той же машинерией, что каталог товаров (оживляет
|
- словарь регионов из CSV той же машинерией, что каталог товаров (оживляет
|
||||||
`RegionCityID`);
|
`RegionCityID`);
|
||||||
|
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
|
||||||
|
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
|
||||||
|
синтетическая постановка — осознанный приём;
|
||||||
|
- лекция «`Sign` и CollapsingMergeTree»: почему на стенде `sum(Sign)` =
|
||||||
|
`count()`, а в бою — нет; частый вопрос на собеседованиях;
|
||||||
- лекция про идентичность «как в бою»: `setUserID` и first-party id,
|
- лекция про идентичность «как в бою»: `setUserID` и first-party id,
|
||||||
детерминированная против вероятностной склейки, identity graph,
|
детерминированная против вероятностной склейки, identity graph,
|
||||||
кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер
|
кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ prometheus-client==0.21.1
|
|||||||
|
|
||||||
# Тестирование
|
# Тестирование
|
||||||
pytest==8.3.4
|
pytest==8.3.4
|
||||||
|
PyYAML==6.0.2
|
||||||
|
|
||||||
# Утилиты
|
# Утилиты
|
||||||
python-json-logger==2.0.7
|
python-json-logger==2.0.7
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ALERTING_DIR = REPO_ROOT / "configs" / "grafana" / "provisioning" / "alerting"
|
||||||
|
|
||||||
|
EXPECTED_THRESHOLDS = {
|
||||||
|
"airflow-alert-rules.yml": {
|
||||||
|
"airflow_scheduler_down": ("lt", [0.1]),
|
||||||
|
"airflow_queue_high": ("gt", [50]),
|
||||||
|
"airflow_task_failures_high": ("gt", [0.1]),
|
||||||
|
"airflow_parse_time_high": ("gt", [30]),
|
||||||
|
},
|
||||||
|
"kafka-alert-rules.yml": {
|
||||||
|
"kafka_broker_down": ("lt", [1]),
|
||||||
|
"kafka_consumer_lag_high": ("gt", [10000]),
|
||||||
|
"kafka_no_messages_produced": ("lt", [0.1]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _rules_by_uid(path):
|
||||||
|
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
return {
|
||||||
|
rule["uid"]: rule
|
||||||
|
for group in document["groups"]
|
||||||
|
for rule in group["rules"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_range_alerts_reduce_series_before_threshold():
|
||||||
|
"""Диапазонный запрос сводится к числу до сравнения с порогом."""
|
||||||
|
for filename, expected_thresholds in EXPECTED_THRESHOLDS.items():
|
||||||
|
rules = _rules_by_uid(ALERTING_DIR / filename)
|
||||||
|
|
||||||
|
assert set(rules) == set(expected_thresholds)
|
||||||
|
|
||||||
|
for uid, (comparison, params) in expected_thresholds.items():
|
||||||
|
rule = rules[uid]
|
||||||
|
stages = {stage["refId"]: stage for stage in rule["data"]}
|
||||||
|
|
||||||
|
threshold = stages[rule["condition"]]["model"]
|
||||||
|
assert threshold["type"] == "threshold", uid
|
||||||
|
|
||||||
|
reduce_stage = stages[threshold["expression"]]["model"]
|
||||||
|
assert reduce_stage["type"] == "reduce", uid
|
||||||
|
assert reduce_stage["reducer"] == "last", uid
|
||||||
|
|
||||||
|
query = stages[reduce_stage["expression"]]["model"]
|
||||||
|
assert query["range"] is True, uid
|
||||||
|
|
||||||
|
evaluator = threshold["conditions"][0]["evaluator"]
|
||||||
|
assert evaluator == {"type": comparison, "params": params}, uid
|
||||||
|
|
||||||
|
|
||||||
|
def test_alert_annotations_use_reduced_values():
|
||||||
|
"""Описание алерта выводит число после Reduce, а не диапазонный ряд."""
|
||||||
|
for filename in EXPECTED_THRESHOLDS:
|
||||||
|
for uid, rule in _rules_by_uid(ALERTING_DIR / filename).items():
|
||||||
|
stages = {stage["refId"]: stage for stage in rule["data"]}
|
||||||
|
|
||||||
|
for annotation in rule.get("annotations", {}).values():
|
||||||
|
for ref_id in re.findall(r"\$values\.([A-Z])", annotation):
|
||||||
|
model = stages[ref_id]["model"]
|
||||||
|
assert model.get("type") == "reduce", uid
|
||||||
Reference in new issue
Block a user