From c5dd9c2d342c0c7a5166aa503b73a5996ee75a50 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 21 Aug 2026 09:56:56 -0400 Subject: [PATCH] =?UTF-8?q?feat(monitoring):=20=D0=BF=D0=BE=D0=B4=D0=B3?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=D0=B0=20Kafka=20=D0=B8=20ClickHouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - подготовлен пол мониторинга для SQL-панелей и метрик отставания Kafka. - Что: - добавлены отдельная роль ClickHouse, источник Grafana и Kafka exporter. - образ Grafana собирается с закреплённым плагином вне тома данных. - Prometheus, смоук и документация обновлены под четыре цели. - Проверка: - make config-test; make lint. - make smoke; make check-clickhouse; make check-services. - make -C generator lint typecheck test. --- .env.example | 1 + README.md | 46 +++++----- compose.yaml | 21 ++++- docs/adr/0007-clickhouse-access.md | 40 ++++++++- docs/architecture/monitoring.md | 83 +++++++++++++++++++ docs/architecture/testing.md | 20 +++-- infra/clickhouse/users.d/access.xml | 19 +++++ infra/grafana/Dockerfile | 11 +++ .../provisioning/dashboards/provider.yml | 11 +++ .../provisioning/datasources/clickhouse.yml | 17 ++++ infra/prometheus/prometheus.yml | 6 ++ scripts/config-test.sh | 20 +++++ scripts/stand-common.sh | 2 +- scripts/stand-smoke.sh | 76 +++++++++++++---- 14 files changed, 326 insertions(+), 47 deletions(-) create mode 100644 docs/architecture/monitoring.md create mode 100644 infra/grafana/Dockerfile create mode 100644 infra/grafana/provisioning/dashboards/provider.yml create mode 100644 infra/grafana/provisioning/datasources/clickhouse.yml diff --git a/.env.example b/.env.example index bd4dd69..1a35c39 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ CLICKHOUSE_DEFAULT_PASSWORD=default-26-local CLICKHOUSE_ETL_PASSWORD=etl-26-writer CLICKHOUSE_BI_PASSWORD=bi-26-reader CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader +CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader KAFKA_EXTERNAL_PORT=29092 PROMETHEUS_PORT=29090 GRAFANA_PORT=23000 diff --git a/README.md b/README.md index 5df513a..26f4395 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ [«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md). Сейчас работают кластер ClickHouse из двух шардов, отдельный clickhouse-keeper, односерверная Kafka в режиме KRaft, Airflow 3.3, Superset, -Prometheus, Grafana и общая база Postgres для метаданных. Генератор -кликстрима в [`generator/`](generator/) собран целиком со стороны клиента: у -него есть контракт схемы события, из которого собрано -[описание выгрузки](docs/formats/clickstream-event.md), модельный мир и +Prometheus, Grafana, экспортёр метрик Kafka и общая база Postgres для +метаданных. Генератор кликстрима в [`generator/`](generator/) собран +целиком со стороны клиента: у него есть контракт схемы события, из которого +собрано [описание выгрузки](docs/formats/clickstream-event.md), модельный мир и проигрыватель, отправляющий дни в Kafka или в файл. События доезжают до типизированного `ods.event`, а `make up` наполняет стенд стартовым миром — первой неделей модельного времени. @@ -63,9 +63,9 @@ make check-services все локальные данные стенда, а следующий `make up` создаст их с новыми значениями. -`make up` собирает локальные образы Airflow и Superset, поднимает весь стенд и -ждёт здорового состояния долгоживущих контейнеров. В образ Airflow добавлены -закреплённые клиенты ClickHouse и Kafka. Одноразовые `airflow-init` и +`make up` собирает локальные образы Airflow, Superset и Grafana, поднимает весь +стенд и ждёт здорового состояния долгоживущих контейнеров. В образ Airflow +добавлены закреплённые клиенты ClickHouse и Kafka. Одноразовые `airflow-init` и `superset-init` завершаются с кодом 0. Первый обновляет схему Airflow, подготавливает администратора и подключение к `clickhouse-01`. Второй обновляет Superset, создаёт администратора и импортирует подключение к `clickhouse-02`. @@ -73,13 +73,13 @@ Superset, создаёт администратора и импортирует `make smoke` за секунды спрашивает, собран ли стенд: зависимости машины, здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый -порт, три цели Prometheus, источник Grafana, компоненты Airflow и подготовленное -подключение к `clickhouse-01`. В конце проверка спрашивает у Docker, не убивало -ли ядро что-нибудь в долгоживущих контейнерах за нехватку памяти и не включалась -ли политика перезапуска: убитый контейнер Docker поднимает сам, и проверка -состояния об этом промолчит. +порт, четыре цели Prometheus, два источника Grafana, компоненты Airflow и +подготовленное подключение к `clickhouse-01`. В конце проверка спрашивает у +Docker, не убивало ли ядро что-нибудь в долгоживущих контейнерах за нехватку +памяти и не включалась ли политика перезапуска: убитый контейнер Docker +поднимает сам, и проверка состояния об этом промолчит. -Одиннадцать проверок здоровья сразу после `make up --wait` повторяют то, чего +Двенадцать проверок здоровья сразу после `make up --wait` повторяют то, чего Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`. Оставлены они потому, что первый вопрос к стенду всё равно «всё ли живо», а ответ на него стоит меньше секунды. Устройство keeper — другое дело: он должен @@ -268,12 +268,13 @@ uv run --project generator python -m clickstream_generator batch \ - `clickhouse-02` — точка подключения Superset; - `clickhouse-keeper` — координатор кластера; - `kafka` — один брокер KRaft; +- `kafka-exporter` — экспортёр метрик Kafka для Prometheus; - `postgres-metadata` — один Postgres с отдельными базами и пользователями `airflow` и `superset`; - `airflow-apiserver`, `airflow-scheduler` и `airflow-dag-processor` — Airflow 3.3 с LocalExecutor, без triggerer; - `superset` — интерфейс и подготовленное подключение ClickHouse; -- `prometheus` и `grafana` — сбор и просмотр встроенных метрик ClickHouse. +- `prometheus` и `grafana` — сбор и просмотр метрик ClickHouse и Kafka. С настройками из `.env.example` порты доступны только с локальной машины: @@ -285,7 +286,7 @@ uv run --project generator python -m clickstream_generator batch \ - Prometheus — `http://127.0.0.1:29090`; - Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`. -В ClickHouse пять пользователей: +В ClickHouse шесть пользователей и пять ролей: - `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет слои хранилища; @@ -293,6 +294,8 @@ uv run --project generator python -m clickstream_generator batch \ слои DDS и DM; - `analyst` предназначен для подключения человека и читает все слои и справочники; +- `grafana` с ролью `monitoring_reader` подключает Grafana и читает все слои, + системные таблицы и данные со всех реплик кластера; - `dict` читает только базу `dic` и пароля не имеет: им словарь товаров ходит за своей подложкой; - `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри @@ -302,7 +305,7 @@ uv run --project generator python -m clickstream_generator batch \ [ADR 0007](docs/adr/0007-clickhouse-access.md). Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли и общий секрет нод живут в `.env` и передаются в конфигурацию через окружение; -Superset получает пароль `bi` тем же путём через штатную функцию настройки. +Superset получает пароль `bi`, а Grafana — пароль `grafana` тем же путём. Значения для локального стенда есть в `.env.example`. Изменения значений ClickHouse в `.env` и файлов настройки серверов в @@ -344,10 +347,13 @@ docker compose up --force-recreate --wait clickhouse-01 clickhouse-02 серверы и keeper используют один образ. Настройки Kafka 4.3.1 сверены с [примером односерверного KRaft](https://github.com/apache/kafka/blob/4.3.1/docker/examples/docker-compose-files/single-node/plaintext/docker-compose.yml). Секция метрик взята из конфигурации закреплённого образа ClickHouse и проверена -на серверах и keeper. Подготовка источника Grafana сверена с -[официальным описанием автоматической настройки](https://grafana.com/docs/grafana/latest/administration/provisioning/). -Prometheus собирает только встроенные метрики двух серверов и keeper; внешних -сборщиков, панелей и правил оповещения пока нет. +на серверах и keeper. Подготовка источников Grafana сверена с +[официальным описанием автоматической настройки](https://grafana.com/docs/grafana/latest/administration/provisioning/) +и [документацией плагина ClickHouse](https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/configure/). +Prometheus собирает встроенные метрики двух серверов и keeper, а через +`kafka-exporter` — состояние Kafka. Устройство зоны описано в +[справочнике мониторинга](docs/architecture/monitoring.md); панелей и правил +оповещения пока нет. Airflow закреплён на 3.3.0. Состав обязательных процессов, LocalExecutor, публичный `airflow.sdk`, API здоровья и SimpleAuthManager сверены с diff --git a/compose.yaml b/compose.yaml index d8a208f..3abf49d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -19,6 +19,7 @@ x-clickhouse-common: &clickhouse-common CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env} depends_on: clickhouse-keeper: condition: service_healthy @@ -213,6 +214,20 @@ services: retries: 20 start_period: 30s + kafka-exporter: + image: danielqsj/kafka-exporter:v1.9.0 + restart: unless-stopped + depends_on: + kafka: + condition: service_healthy + command: ["--kafka.server=kafka:9092"] + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9308/metrics >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 10s + kafka-init: image: apache/kafka:4.3.1 restart: "no" @@ -504,7 +519,9 @@ services: start_period: 10s grafana: - image: grafana/grafana:13.1.1 + image: clickstream-grafana:local + build: + context: ./infra/grafana restart: unless-stopped depends_on: prometheus: @@ -516,8 +533,10 @@ services: GF_NEWS_NEWS_FEED_ENABLED: "false" GF_ANALYTICS_REPORTING_ENABLED: "false" GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env} volumes: - ./infra/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro + - ./infra/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - grafana_data:/var/lib/grafana ports: - "127.0.0.1:${GRAFANA_PORT:?Скопируйте .env.example в .env}:3000" diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md index 337ee60..afb09c5 100644 --- a/docs/adr/0007-clickhouse-access.md +++ b/docs/adr/0007-clickhouse-access.md @@ -1,10 +1,11 @@ # ADR 0007. Доступ к ClickHouse: пользователи файлом, доверие нод секретом -Дата: 9 августа 2026 года. Статус: принято. +Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader` +и пользователь `grafana` добавлены 21 августа 2026 года (#114). ## Решение -У кластера пять пользователей и четыре роли. +У кластера шесть пользователей и пять ролей. - `default` — с паролем, только служебный: проверки здоровья нод и работа изнутри контейнеров. Приложения им не ходят. @@ -14,6 +15,8 @@ Им ходит Superset. - `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им человек подключается снаружи, из своего клиента. +- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и + системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто. - `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им словарь читает свою подложку, и больше он никем не используется ([ADR 0012](0012-dictionary-home.md)). @@ -58,6 +61,20 @@ DDS под ними, потому что так чаще всего и быва есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно — `SELECT` на `dic`, и хост источника локальный. +**Своя роль дашбордам.** Панели мониторинга живут не на витринах, а на +`system.query_log`, частях MergeTree и очереди распределённых DDL — и читают +их через `clusterAllReplicas`, потому что системные таблицы у каждой ноды свои +([ADR 0002](0002-monitoring-scope.md)). Для этого нужны `SELECT` на `system.*` +и `REMOTE`; контракт роли также включает `CLUSTER` для запросов `ON CLUSTER`. +Такого набора не было ни у одного читателя: у обеих читающих ролей из +системных таблиц только `system.settings`. Догрузить +недостающее в `analyst_reader` отвергнуто: аналитик получил бы системные +таблицы и `REMOTE` заодно, а в `system.query_log` запросы Grafana стали бы +неотличимы от человеческих — ровно там, где дашборд «Запросы» и должен их +различать. Пускать Grafana под `etl` отвергнуто сразу: читатель не получает +права писать. Осталась своя роль по здешней общей схеме — у каждого +потребителя она своя. + **Секрет, а не учётные данные по репликам.** У каждой реплики в описании кластера своя учётка — вписанная явно или подразумеваемая, и тогда это беспарольный `default`. Подключение к соседней ноде идёт от неё, а не от @@ -141,6 +158,25 @@ DDS под ними, потому что так чаще всего и быва настройки ClickHouse. - Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`). +Права роли `monitoring_reader` сверены 21 августа 2026 года по документации +ClickHouse через MCP Context7 (#114). + +- Системные таблицы выдаются обычным `SELECT`: в документации это + `GRANT SELECT ON system.query_log`, значит `SELECT ON system.*` и есть «все + системные таблицы». Отдельного права на них нет. +- `GRANT CLUSTER ON *.*` — документированная форма права на запросы + `ON CLUSTER`; текущий запрос `clusterAllReplicas` его не использует. +- `clusterAllReplicas` документация держит в одном ряду с `remote`, + `remoteSecure` и `cluster`: это табличная функция, которая берёт имя + кластера из `remote_servers` и идёт на каждую его реплику. Отсюда `REMOTE` — + без права на удалённое обращение запрос дальше своей ноды не пойдёт, а + панели ADR 0002 читают `system.*` именно так. + +Набор проверен на живом стенде 21 августа 2026 года (#114): пользователь +`grafana` из Grafana выполнил +`SELECT count() AS replicas FROM clusterAllReplicas('clickstream_cluster', system.one)` +и получил 2 — прав хватает, чтобы прочесть системную таблицу с обеих нод. + Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56. - Секрет кластера принимается внутри `remote_servers` в виде diff --git a/docs/architecture/monitoring.md b/docs/architecture/monitoring.md new file mode 100644 index 0000000..a523ba4 --- /dev/null +++ b/docs/architecture/monitoring.md @@ -0,0 +1,83 @@ +# Мониторинг: из чего собран и что скребёт + +Документ описывает зону мониторинга: какие сервисы в неё входят, что собирает +Prometheus, какие источники данных подготовлены в Grafana и каким +пользователем она ходит в ClickHouse. Зачем зоне именно такой состав и какие +дашборды на нём задуманы — [ADR 0002](../adr/0002-monitoring-scope.md); +устройство доступа — [ADR 0007](../adr/0007-clickhouse-access.md). + +**Что здесь есть и чего ещё нет.** Собран пол: свой образ Grafana с плагином +ClickHouse, два подготовленных источника данных, экспортёр метрик Kafka и цель +для него у Prometheus. Дашбордов пока ни одного — каталог под них заведён +пустым, панели приносят отдельные тикеты. Правил оповещения и серверного +журнала `system.text_log` тоже пока нет. + +## Состав + +| Сервис | Образ | Что делает | +|---|---|---| +| `prometheus` | `prom/prometheus:v3.13.2` | опрашивает цели раз в 15 секунд, порт стенда `29090` | +| `grafana` | `clickstream-grafana:local` | интерфейс мониторинга; собирается из `infra/grafana/`, порт стенда `23000` | +| `kafka-exporter` | `danielqsj/kafka-exporter:v1.9.0` | переводит состояние брокера в метрики Prometheus, порт `9308` | + +Плагин ClickHouse 4.20.0 входит в образ Grafana. Фоновая установка плагинов +при запуске отключена. + +## Что скребёт Prometheus + +| Цель | Задание | Метка `role` | Откуда метрики | +|---|---|---|---| +| `clickhouse-01:9363` | `clickhouse` | `server` | встроенный экспортёр ноды | +| `clickhouse-02:9363` | `clickhouse` | `server` | встроенный экспортёр ноды | +| `clickhouse-keeper:9363` | `clickhouse-keeper` | `keeper` | встроенный экспортёр keeper | +| `kafka-exporter:9308` | `kafka` | `exporter` | внешний экспортёр | + +Ровно эти четыре цели пересчитывает `check_prometheus_targets` в +`scripts/stand-smoke.sh` — и по значению `up`, и по списку адресов. Пятая цель +красит смоук; это и есть способ заметить, что состав пола поменяли молча. + +## Источники данных Grafana + +Оба источника подготовлены файлами в `infra/grafana/provisioning/datasources/` +и объявлены `editable: false`: править их через интерфейс нельзя, источник +истины — репозиторий. + +- `prometheus` — основной по умолчанию, ходит на `http://prometheus:9090`. +- `clickhouse` — плагин `grafana-clickhouse-datasource`, нативный протокол, + `clickhouse-01:9000`, пользователь `grafana`, пароль из `.env`. + +Источник ClickHouse смотрит на `clickhouse-01`. Системные таблицы обеих нод +панели читают через `clusterAllReplicas`, как принято в ADR 0002. + +## Доступ к ClickHouse + +Grafana ходит пользователем `grafana` с ролью `monitoring_reader`: читает слои, +таблицы базы `dic` и системные таблицы. `REMOTE` разрешает +`clusterAllReplicas`, `CLUSTER` — запросы `ON CLUSTER`. Решение о своей роли +записано в [ADR 0007](../adr/0007-clickhouse-access.md). Пароль задаётся +переменной `CLICKHOUSE_GRAFANA_PASSWORD`. + +## Куда кладутся дашборды + +Каталог `infra/grafana/provisioning/dashboards/` смонтирован в контейнер и +объявлен провайдером в `provider.yml`: Grafana подхватывает оттуда файлы +дашбордов, удалять и править их через интерфейс запрещено. Сейчас в каталоге +только сам файл провайдера. Тикет с панелями кладёт туда JSON и в +`compose.yaml` уже не заходит — ради этого каталог и заведён заранее. + +## Что проверено + +Права роли `monitoring_reader` сверены 21 августа 2026 года по документации +ClickHouse через MCP Context7; разбор — в +[ADR 0007](../adr/0007-clickhouse-access.md), раздел «Что проверено». + +Сам состав проверен на живом стенде 21 августа 2026 года, собранном заново +после `make clean`. + +- Grafana видит плагин `grafana-clickhouse-datasource` 4.20.0 + (`/api/plugins?type=datasource`), оба подготовленных источника здоровы. +- Запрос источника ClickHouse к `system.one` через `clusterAllReplicas` вернул + 2 — с одной ноды видны обе. +- Prometheus видит ровно четыре цели, все со значением `up = 1`; запрос + `kafka_consumergroup_lag` вернул ряды по группам `clickstream_*`. +- `make smoke` 21/21, `make check-clickhouse` 10/10, `make check-services` 7/7. diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index 1145354..d970744 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -33,18 +33,18 @@ генератора: он отдельный пакет со своим `pyproject.toml`, локом и образом, и спрашивают его отдельно. -Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено»: -`make test`, `make smoke`, `make check-clickhouse` и `make check-services` -перемерены 7 августа 2026 года на приёмке этапа 2, корневой `make lint` — -9 августа при его появлении; остальные стоят с замера 6 августа. +Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено». +`make smoke` и `make check-services` перемерены 21 августа 2026 года, +`make check-clickhouse` — 19 августа, корневой `make lint` — 9 августа, +`make test` — 7 августа; остальные стоят с замера 6 августа. | Цель | Откуда | Что утверждает | Стенд | Цена | |---|---|---|---|---| | `make config-test` | корень | Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с | | `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с | -| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 9 с | +| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 17 с | | `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против описи | нужен | 24 с | -| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 59 с | +| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 93 с | | `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с | | `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с | | `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с | @@ -209,7 +209,7 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`. контейнер Docker поднимает сам, и проверка здоровья об этом промолчит (ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и тому, кто гонял один смоук. Это единственная проверка, которая считается - дважды: 20 у смоука плюс 7 у `check-services` — это 26 разных проверок. + дважды: 21 у смоука плюс 7 у `check-services` — это 27 разных проверок. - **Зависимости машины считает только смоук.** «На машине есть Docker, curl и jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд собран». Для `check-services` это условие запуска: без них он не начнёт @@ -221,6 +221,12 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`. ## Что проверено +**Перезамер 21 августа 2026 года при исполнении #114.** После добавления +`kafka-exporter` и источника ClickHouse смоук прошёл 21/21 за 16–17 секунд, +`check-services` — 7/7 за 93 секунды. При остановленном экспортёре смоук дал +19/2: назвал нездоровый сервис и отсутствие одной из четырёх целей. После +возврата сервиса прогон снова дал 21/21. + **Перезамер 20 августа 2026 года при исполнении #96.** `make up` после `make clean` прошёл за 8 м 18 с, повтор на живом стенде — за 3 м 35 с. Служба семи слепков заняла около 28 секунд между концом `world-init` и своим концом. diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index 785b76e..88cf8b8 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -44,6 +44,18 @@ GRANT SELECT ON system.settings + + + GRANT SELECT ON stg.* + GRANT SELECT ON ods.* + GRANT SELECT ON dds.* + GRANT SELECT ON dm.* + GRANT SELECT ON dic.* + GRANT SELECT ON system.* + GRANT CLUSTER ON *.* + GRANT REMOTE ON *.* + + @@ -77,6 +89,13 @@ GRANT analyst_reader + + + default + + GRANT monitoring_reader + + default diff --git a/infra/grafana/Dockerfile b/infra/grafana/Dockerfile new file mode 100644 index 0000000..d7600f1 --- /dev/null +++ b/infra/grafana/Dockerfile @@ -0,0 +1,11 @@ +FROM grafana/grafana:13.1.1 + +# Именованный том закрывает /var/lib/grafana, поэтому плагин остаётся в образе. +# Фоновая установка выключена: нужная версия уже закреплена, а каталог образа +# доступен процессу Grafana только для чтения. +USER root +ENV GF_PATHS_PLUGINS=/usr/share/grafana/plugins \ + GF_PLUGINS_PREINSTALL_DISABLED=true +RUN grafana cli --pluginsDir "$GF_PATHS_PLUGINS" \ + plugins install grafana-clickhouse-datasource 4.20.0 +USER grafana diff --git a/infra/grafana/provisioning/dashboards/provider.yml b/infra/grafana/provisioning/dashboards/provider.yml new file mode 100644 index 0000000..8a0ad25 --- /dev/null +++ b/infra/grafana/provisioning/dashboards/provider.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: clickstream + orgId: 1 + folder: "" + type: file + disableDeletion: true + editable: false + options: + path: /etc/grafana/provisioning/dashboards diff --git a/infra/grafana/provisioning/datasources/clickhouse.yml b/infra/grafana/provisioning/datasources/clickhouse.yml new file mode 100644 index 0000000..652d757 --- /dev/null +++ b/infra/grafana/provisioning/datasources/clickhouse.yml @@ -0,0 +1,17 @@ +apiVersion: 1 + +datasources: + - name: ClickHouse + uid: clickhouse + type: grafana-clickhouse-datasource + access: proxy + isDefault: false + editable: false + jsonData: + host: clickhouse-01 + port: 9000 + protocol: native + username: grafana + defaultDatabase: system + secureJsonData: + password: ${CLICKHOUSE_GRAFANA_PASSWORD} diff --git a/infra/prometheus/prometheus.yml b/infra/prometheus/prometheus.yml index 53abd14..27d26c6 100644 --- a/infra/prometheus/prometheus.yml +++ b/infra/prometheus/prometheus.yml @@ -16,3 +16,9 @@ scrape_configs: - clickhouse-keeper:9363 labels: role: keeper + - job_name: kafka + static_configs: + - targets: + - kafka-exporter:9308 + labels: + role: exporter diff --git a/scripts/config-test.sh b/scripts/config-test.sh index 55e376d..a4abb2f 100755 --- a/scripts/config-test.sh +++ b/scripts/config-test.sh @@ -30,6 +30,26 @@ done jq -e ' .services.superset.image == "clickstream-superset:local" ' >/dev/null <<<"$config_json" +jq -e ' + .services.grafana.image == "clickstream-grafana:local" and + any( + .services.grafana.volumes[]; + (.source | endswith("/infra/grafana/provisioning/dashboards")) and + .target == "/etc/grafana/provisioning/dashboards" and + .read_only == true + ) +' >/dev/null <<<"$config_json" +jq -e ' + .services.grafana.environment.CLICKHOUSE_GRAFANA_PASSWORD as $password | + ($password | length > 0) and + .services["clickhouse-01"].environment.CLICKHOUSE_GRAFANA_PASSWORD == $password and + .services["clickhouse-02"].environment.CLICKHOUSE_GRAFANA_PASSWORD == $password +' >/dev/null <<<"$config_json" +jq -e ' + .services["kafka-exporter"].image == "danielqsj/kafka-exporter:v1.9.0" and + .services["kafka-exporter"].command == ["--kafka.server=kafka:9092"] and + .services["kafka-exporter"].depends_on.kafka.condition == "service_healthy" +' >/dev/null <<<"$config_json" jq -e ' .services["airflow-init"].image == "clickstream-airflow:local" and all( diff --git a/scripts/stand-common.sh b/scripts/stand-common.sh index 0069a4e..0932d9d 100644 --- a/scripts/stand-common.sh +++ b/scripts/stand-common.sh @@ -7,7 +7,7 @@ readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" read -r -a COMPOSE_CMD <<<"${COMPOSE_BIN:-docker compose}" readonly -a LONG_LIVED_SERVICES=( - clickhouse-keeper clickhouse-01 clickhouse-02 kafka postgres-metadata + clickhouse-keeper clickhouse-01 clickhouse-02 kafka kafka-exporter postgres-metadata airflow-apiserver airflow-scheduler airflow-dag-processor superset prometheus grafana ) diff --git a/scripts/stand-smoke.sh b/scripts/stand-smoke.sh index a29239b..96e24e1 100755 --- a/scripts/stand-smoke.sh +++ b/scripts/stand-smoke.sh @@ -87,33 +87,37 @@ check_prometheus_targets() { targets_response="$(curl -sf "http://127.0.0.1:${port}/api/v1/targets?state=active" 2>/dev/null || true)" if jq -e ' .status == "success" and - (.data.result | length == 3) and + (.data.result | length == 4) and ([.data.result[].metric.instance] | sort == - ["clickhouse-01:9363", "clickhouse-02:9363", "clickhouse-keeper:9363"]) and + ["clickhouse-01:9363", "clickhouse-02:9363", "clickhouse-keeper:9363", "kafka-exporter:9308"]) and all(.data.result[]; .value[1] == "1") ' >/dev/null 2>&1 <<<"$response" && jq -e ' .status == "success" and - (.data.activeTargets | length == 3) and + (.data.activeTargets | length == 4) and ([.data.activeTargets[].scrapeUrl] | sort == [ "http://clickhouse-01:9363/metrics", "http://clickhouse-02:9363/metrics", - "http://clickhouse-keeper:9363/metrics" + "http://clickhouse-keeper:9363/metrics", + "http://kafka-exporter:9308/metrics" ]) and all(.data.activeTargets[]; .health == "up" and .lastError == "") ' >/dev/null 2>&1 <<<"$targets_response"; then - pass 'Prometheus видит ровно три цели ClickHouse, все со значением up=1' + pass 'Prometheus видит ровно четыре здоровые цели ClickHouse и Kafka' else - fail 'Prometheus не видит ровно три здоровые цели: clickhouse-01, clickhouse-02 и clickhouse-keeper' + fail 'Prometheus не видит ровно четыре здоровые цели: clickhouse-01, clickhouse-02, clickhouse-keeper и kafka-exporter' fi } check_grafana_datasource() { + local clickhouse_datasource + local clickhouse_response local config + local datasources local password local port - local datasource local provisioning_file_ok=0 - local response + local prometheus_datasource + local prometheus_response local user config="$(compose config --format json 2>/dev/null || true)" @@ -129,24 +133,64 @@ check_grafana_datasource() { grep -Eq "^[[:space:]]+url: http://prometheus:9090$" "$file" grep -Eq "^[[:space:]]+isDefault: true$" "$file" grep -Eq "^[[:space:]]+editable: false$" "$file" + file=/etc/grafana/provisioning/datasources/clickhouse.yml + test -r "$file" + grep -Eq "^[[:space:]]+uid: clickhouse$" "$file" + grep -Eq "^[[:space:]]+type: grafana-clickhouse-datasource$" "$file" + grep -Eq "^[[:space:]]+host: clickhouse-01$" "$file" + grep -Eq "^[[:space:]]+port: 9000$" "$file" + grep -Eq "^[[:space:]]+username: grafana$" "$file" + grep -Eq "^[[:space:]]+editable: false$" "$file" ' >/dev/null 2>&1 && provisioning_file_ok=1 - datasource="$(curl -sf -u "${user}:${password}" \ + datasources="$(curl -sf -u "${user}:${password}" \ + "http://127.0.0.1:${port}/api/plugins?type=datasource" 2>/dev/null || true)" + prometheus_datasource="$(curl -sf -u "${user}:${password}" \ "http://127.0.0.1:${port}/api/datasources/uid/prometheus" 2>/dev/null || true)" - response="$(curl -sf -u "${user}:${password}" \ + prometheus_response="$(curl -sf -u "${user}:${password}" \ "http://127.0.0.1:${port}/api/datasources/uid/prometheus/health" 2>/dev/null || true)" - if [[ "$provisioning_file_ok" -eq 1 ]] && jq -e ' + clickhouse_datasource="$(curl -sf -u "${user}:${password}" \ + "http://127.0.0.1:${port}/api/datasources/uid/clickhouse" 2>/dev/null || true)" + clickhouse_response="$(curl -sf -u "${user}:${password}" \ + "http://127.0.0.1:${port}/api/datasources/uid/clickhouse/health" 2>/dev/null || true)" + if [[ "$provisioning_file_ok" -ne 1 ]]; then + fail 'Grafana не получила файлы настройки источников' + return + fi + if ! jq -e ' + any(.[]; .id == "grafana-clickhouse-datasource") + ' >/dev/null 2>&1 <<<"$datasources"; then + fail 'Grafana не видит плагин ClickHouse' + return + fi + if ! jq -e ' .uid == "prometheus" and .type == "prometheus" and .url == "http://prometheus:9090" and .access == "proxy" and .isDefault == true and .readOnly == true - ' >/dev/null 2>&1 <<<"$datasource" && \ - jq -e '.status == "OK"' >/dev/null 2>&1 <<<"$response"; then - pass 'Grafana проверила файл настройки и подготовленный источник Prometheus с uid=prometheus' - else - fail 'Grafana не смогла проверить файл настройки и подготовленный источник Prometheus с uid=prometheus' + ' >/dev/null 2>&1 <<<"$prometheus_datasource" || \ + ! jq -e '.status == "OK"' >/dev/null 2>&1 <<<"$prometheus_response"; then + fail 'Grafana не видит здоровый подготовленный источник Prometheus' + return fi + if ! jq -e ' + .uid == "clickhouse" and + .type == "grafana-clickhouse-datasource" and + .access == "proxy" and + .isDefault == false and + .readOnly == true and + .jsonData.host == "clickhouse-01" and + .jsonData.port == 9000 and + .jsonData.protocol == "native" and + .jsonData.username == "grafana" and + .jsonData.defaultDatabase == "system" + ' >/dev/null 2>&1 <<<"$clickhouse_datasource" || \ + ! jq -e '.status == "OK"' >/dev/null 2>&1 <<<"$clickhouse_response"; then + fail 'Grafana не видит здоровый подготовленный источник ClickHouse' + return + fi + pass 'Grafana видит плагин и два здоровых подготовленных источника' } # Три вопроса о связности, и все три Airflow отвечает сразу. Запуск пробников —