fix(superset): align bootstrap with clickhousedb uri

- Why:
  - Superset bootstrap used outdated ClickHouse URI format and did not fail fast on init errors.
  - docs and exported dashboard metadata diverged from runtime connection settings.
- What:
  - build ClickHouse URI from env vars and use clickhousedb:// in init script.
  - refresh dataset metadata on existing datasets and surface import errors.
  - run create_dashboard during superset-init startup and align docs/exported URI references.
  - ignore node_modules in git.
- Check:
  - python3 -m py_compile superset/init_superset.py
  - manual dashboard smoke check in UI (charts render)
This commit is contained in:
2026-02-10 23:51:38 +03:00
parent 6533f8b32c
commit 22f08382e5
7 changed files with 71 additions and 18 deletions
+1
View File
@@ -171,3 +171,4 @@ $RECYCLE.BIN/
*.msm *.msm
*.msp *.msp
*.lnk *.lnk
node_modules
+1 -1
View File
@@ -249,7 +249,7 @@ curl http://localhost:8088/health # должно вернуть 200
``` ```
Что создаётся автоматически: Что создаётся автоматически:
- **Подключение к ClickHouse**: `clickhouse_dwh` (URI: `clickhouse+connect://default@clickhouse:8123/default`) - **Подключение к ClickHouse**: `clickhouse_dwh` (URI: `clickhousedb://default:123456@clickhouse:8123/default`)
- **Датасеты** (6 шт.): `v_events_enriched`, `v_daily_traffic`, `v_utm_effectiveness`, `v_top_pages_daily`, `v_session_overview`, `dq_summary` - **Датасеты** (6 шт.): `v_events_enriched`, `v_daily_traffic`, `v_utm_effectiveness`, `v_top_pages_daily`, `v_session_overview`, `dq_summary`
- **Чарты** (10 шт.): KPI метрики, графики трафика, география, UTM-эффективность, качество данных - **Чарты** (10 шт.): KPI метрики, графики трафика, география, UTM-эффективность, качество данных
- **Дашборд**: "🛒 E-commerce Analytics Dashboard" - **Дашборд**: "🛒 E-commerce Analytics Dashboard"
+3 -1
View File
@@ -273,7 +273,9 @@ services:
echo 'Initializing roles...' && echo 'Initializing roles...' &&
superset init && superset init &&
echo 'Creating ClickHouse connection and datasets...' && echo 'Creating ClickHouse connection and datasets...' &&
python /app/superset_init/init_superset.py || true && python /app/superset_init/init_superset.py &&
echo 'Creating dashboard and charts...' &&
python /app/superset_init/create_dashboard.py &&
echo 'Superset initialized successfully' echo 'Superset initialized successfully'
" "
depends_on: depends_on:
+1 -1
View File
@@ -602,7 +602,7 @@ INSERT INTO dm.daily_traffic SELECT * FROM dm.v_daily_traffic;
- URL: `clickhouse://default:123456@clickhouse:9000/default` (native TCP для Airflow plugin) - URL: `clickhouse://default:123456@clickhouse:9000/default` (native TCP для Airflow plugin)
- Provider/интеграция: `airflow-clickhouse-plugin``airflow/requirements.txt`), задачи выполняются через `ClickHouseOperator`. - Provider/интеграция: `airflow-clickhouse-plugin``airflow/requirements.txt`), задачи выполняются через `ClickHouseOperator`.
- Дополнительно: `kafka-python==2.0.6` для работы с Kafka из DAG. - Дополнительно: `kafka-python==2.0.6` для работы с Kafka из DAG.
- Примечание: Superset подключается к ClickHouse по HTTP (обычно `clickhouse+connect://...:8123/...`). - Примечание: Superset подключается к ClickHouse по HTTP (обычно `clickhousedb://...:8123/...`).
--- ---
+2 -2
View File
@@ -93,7 +93,7 @@ make superset-dashboard
# Основные # Основные
make up # Запуск всех сервисов make up # Запуск всех сервисов
make down # Остановка сервисов make down # Остановка сервисов
make down-v # Остановка с удалением volumes make clean # Остановка с удалением volumes
make logs service=superset # Логи сервиса make logs service=superset # Логи сервиса
# ETL # ETL
@@ -120,7 +120,7 @@ make superset-restart # Перезапуск сервиса
3. Выберите **ClickHouse** 3. Выберите **ClickHouse**
4. Введите SQLAlchemy URI: 4. Введите SQLAlchemy URI:
``` ```
clickhouse+native://default@clickhouse:9000/default clickhousedb://default:123456@clickhouse:8123/default
``` ```
5. Установите: 5. Установите:
- **Expose in SQL Lab:** - **Expose in SQL Lab:**
@@ -188,7 +188,7 @@
{ {
"__Database__": { "__Database__": {
"database_name": "clickhouse_dwh", "database_name": "clickhouse_dwh",
"sqlalchemy_uri": "clickhouse+native://default@clickhouse:9000/default", "sqlalchemy_uri": "clickhousedb://default:123456@clickhouse:8123/default",
"expose_in_sqllab": true, "expose_in_sqllab": true,
"allow_ctas": false, "allow_ctas": false,
"allow_cvas": false, "allow_cvas": false,
+62 -12
View File
@@ -24,12 +24,11 @@
================================================================================ ================================================================================
""" """
import os
import sys import sys
import json import os
import logging import logging
import subprocess import subprocess
from typing import Optional from urllib.parse import quote_plus
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
@@ -41,6 +40,22 @@ logger = logging.getLogger(__name__)
# Добавляем путь к superset # Добавляем путь к superset
sys.path.insert(0, '/app') sys.path.insert(0, '/app')
CLICKHOUSE_USER = os.getenv('CLICKHOUSE_USER', 'default')
CLICKHOUSE_PASSWORD = os.getenv('CLICKHOUSE_PASSWORD', '123456')
CLICKHOUSE_HOST = os.getenv('CLICKHOUSE_HOST', 'clickhouse')
CLICKHOUSE_PORT = os.getenv('CLICKHOUSE_HTTP_PORT', '8123')
CLICKHOUSE_DATABASE = os.getenv('CLICKHOUSE_DATABASE', 'default')
def build_clickhouse_uri() -> str:
"""Собирает URI подключения к ClickHouse для Superset."""
user = quote_plus(CLICKHOUSE_USER)
password = quote_plus(CLICKHOUSE_PASSWORD)
host = CLICKHOUSE_HOST
port = CLICKHOUSE_PORT
database = quote_plus(CLICKHOUSE_DATABASE)
return f"clickhousedb://{user}:{password}@{host}:{port}/{database}"
def run_superset_cli(args): def run_superset_cli(args):
"""Запуск команды superset CLI""" """Запуск команды superset CLI"""
@@ -57,13 +72,14 @@ def run_superset_cli(args):
def create_clickhouse_connection(): def create_clickhouse_connection():
"""Создание подключения к ClickHouse через CLI""" """Создание подключения к ClickHouse через CLI"""
logger.info("Creating ClickHouse database connection...") logger.info("Creating ClickHouse database connection...")
database_uri = build_clickhouse_uri()
# Создаем подключение через set-database-uri # Создаем подключение через set-database-uri
# clickhouse-connect использует HTTP порт 8123 внутри Docker сети # clickhouse-connect использует HTTP порт 8123 внутри Docker сети
success, stdout, stderr = run_superset_cli([ success, stdout, stderr = run_superset_cli([
'set-database-uri', 'set-database-uri',
'-d', 'clickhouse_dwh', '-d', 'clickhouse_dwh',
'-u', 'clickhouse+connect://default@clickhouse:8123/default' '-u', database_uri
]) ])
if success: if success:
@@ -74,7 +90,7 @@ def create_clickhouse_connection():
logger.info("Please create manually via UI:") logger.info("Please create manually via UI:")
logger.info("1. Go to: Settings → Database Connections → + Database") logger.info("1. Go to: Settings → Database Connections → + Database")
logger.info("2. Select: ClickHouse") logger.info("2. Select: ClickHouse")
logger.info("3. URI: clickhouse+connect://default@clickhouse:8123/default") logger.info(f"3. URI: {database_uri}")
return False return False
@@ -136,31 +152,65 @@ def import_datasets():
"print(f'Found database: {database.database_name} (id={database.id})')", "print(f'Found database: {database.database_name} (id={database.id})')",
"", "",
"imported = 0", "imported = 0",
"refreshed = 0",
"errors = 0",
] ]
for ds in datasets: for ds in datasets:
table_name = ds["table_name"]
schema_name = ds["schema"]
description = ds["description"]
script_lines.extend([ script_lines.extend([
"", "",
f"# Dataset: {ds['table_name']}", f"# Dataset: {table_name}",
f"existing = db.session.query(SqlaTable).filter_by(table_name='{ds['table_name']}', schema='{ds['schema']}').first()", (
"existing = db.session.query(SqlaTable).filter_by("
f"table_name={table_name!r}, schema={schema_name!r}"
").first()"
),
"if existing:", "if existing:",
f" print(f'Dataset {ds['table_name']} already exists')", " try:",
" if not existing.columns:",
" existing.fetch_metadata()",
" db.session.commit()",
f" print('Refreshed dataset metadata: {table_name}')",
" refreshed += 1",
" else:",
f" print('Dataset {table_name} already exists')",
" except Exception as e:",
f" print(f'ERROR: failed to refresh {table_name}: {{e}}')",
" db.session.rollback()",
" errors += 1",
"else:", "else:",
" try:", " try:",
f" dataset = SqlaTable(table_name='{ds['table_name']}', schema='{ds['schema']}', database_id=database.id, database=database, description='{ds['description']}')", (
" dataset = SqlaTable("
f"table_name={table_name!r}, "
f"schema={schema_name!r}, "
"database_id=database.id, "
"database=database, "
f"description={description!r}"
")"
),
" db.session.add(dataset)", " db.session.add(dataset)",
" db.session.flush()", " db.session.flush()",
f" print(f'Created dataset: {ds['table_name']}')", " dataset.fetch_metadata()",
" db.session.commit()",
f" print('Created dataset: {table_name}')",
" imported += 1", " imported += 1",
" except Exception as e:", " except Exception as e:",
f" print(f'Error creating {ds['table_name']}: {{e}}')", f" print(f'ERROR: failed to create {table_name}: {{e}}')",
" db.session.rollback()", " db.session.rollback()",
" errors += 1",
]) ])
script_lines.extend([ script_lines.extend([
"", "",
"db.session.commit()",
"print(f'Successfully imported {imported} datasets')", "print(f'Successfully imported {imported} datasets')",
"print(f'Refreshed metadata for {refreshed} datasets')",
"print(f'Errors: {errors}')",
"if errors > 0:",
" raise SystemExit(1)",
]) ])
script_content = '\n'.join(script_lines) script_content = '\n'.join(script_lines)