fix: исправлен путь Kafka volume и обновлены скрипты Superset
- Исправлен путь Kafka volume с /tmp/kraft-combined-logs на /var/lib/kafka/data (решена проблема с правами доступа при старте Kafka) - Обновлен superset/init_superset.py: улучшена обработка ошибок SQLite - Обновлен superset/create_dashboard.py: оптимизирован импорт модулей
This commit is contained in:
@@ -5,19 +5,12 @@
|
|||||||
================================================================================
|
================================================================================
|
||||||
Назначение:
|
Назначение:
|
||||||
- Создание чартов (Charts) на основе датасетов DM-слоя
|
- Создание чартов (Charts) на основе датасетов DM-слоя
|
||||||
- Создание дашборда с布局 и фильтрами
|
- Создание дашборда с layout и фильтрами
|
||||||
- Настройка native filters
|
- Настройка native filters
|
||||||
|
|
||||||
Запуск:
|
Запуск:
|
||||||
Внутри контейнера superset:
|
Внутри контейнера superset:
|
||||||
python /app/superset_init/create_dashboard.py
|
python /app/superset_init/create_dashboard.py
|
||||||
|
|
||||||
Чарты которые создаются:
|
|
||||||
1. KPI блок (4 Big Number): Total Events, Unique Users, Sessions, Avg/Sess
|
|
||||||
2. Динамика: Events by Hour (Line), Traffic by Device (Pie)
|
|
||||||
3. География: World Map по странам
|
|
||||||
4. Маркетинг: UTM Source/Medium Table, Top Pages Bar
|
|
||||||
5. Качество данных: DQ Summary Bar
|
|
||||||
================================================================================
|
================================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -32,21 +25,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
sys.path.insert(0, '/app')
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
try:
|
|
||||||
from superset.app import create_app
|
|
||||||
from superset.extensions import db, security_manager
|
|
||||||
from superset.connectors.sqla.models import SqlaTable
|
|
||||||
from superset.charts.data_access_layer import ChartDAO
|
|
||||||
from superset.dashboards.data_access_layer import DashboardDAO
|
|
||||||
from superset.charts.schemas import ChartPostSchema
|
|
||||||
from superset.dashboards.schemas import DashboardPostSchema
|
|
||||||
from superset.commands.chart.create import CreateChartCommand
|
|
||||||
from superset.commands.dashboard.create import CreateDashboardCommand
|
|
||||||
from superset.utils.core import DatasourceType
|
|
||||||
except ImportError as e:
|
|
||||||
logger.error(f"Failed to import Superset modules: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
# Конфигурация чартов
|
# Конфигурация чартов
|
||||||
CHARTS_CONFIG = [
|
CHARTS_CONFIG = [
|
||||||
@@ -245,7 +223,7 @@ CHARTS_CONFIG = [
|
|||||||
# Конфигурация дашборда
|
# Конфигурация дашборда
|
||||||
DASHBOARD_CONFIG = {
|
DASHBOARD_CONFIG = {
|
||||||
"dashboard_title": "🛒 E-commerce Analytics Dashboard",
|
"dashboard_title": "🛒 E-commerce Analytics Dashboard",
|
||||||
"description": "Аналитический дашборд для e-commerce кликстрима. Показывает трафик, конверсии, географию и качество данных.",
|
"description": "Аналитический дашборд для e-commerce кликстрима: трафик, конверсии, география и качество данных.",
|
||||||
"published": True,
|
"published": True,
|
||||||
"slug": "ecommerce-analytics",
|
"slug": "ecommerce-analytics",
|
||||||
"json_metadata": json.dumps({
|
"json_metadata": json.dumps({
|
||||||
@@ -297,8 +275,10 @@ DASHBOARD_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_dataset_by_name(app, dataset_name: str) -> SqlaTable:
|
def get_dataset_by_name(app, dataset_name: str):
|
||||||
"""Получение датасета по имени таблицы"""
|
"""Получение датасета по имени таблицы"""
|
||||||
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
dataset = db.session.query(SqlaTable).filter_by(
|
dataset = db.session.query(SqlaTable).filter_by(
|
||||||
table_name=dataset_name,
|
table_name=dataset_name,
|
||||||
@@ -307,18 +287,14 @@ def get_dataset_by_name(app, dataset_name: str) -> SqlaTable:
|
|||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
def create_chart(app, chart_config: dict, dataset: SqlaTable) -> Optional[dict]:
|
def create_chart(app, chart_config: dict, dataset):
|
||||||
"""Создание чарта"""
|
"""Создание чарта"""
|
||||||
|
from superset.extensions import db
|
||||||
|
from superset.utils.core import DatasourceType
|
||||||
|
from superset.charts.commands.create import CreateChartCommand
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
# Проверяем, существует ли чарт
|
|
||||||
from superset.charts.data_access_layer import ChartDAO
|
|
||||||
existing = ChartDAO.find_by_title(chart_config["slice_name"])
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
logger.info(f"Chart '{chart_config['slice_name']}' already exists")
|
|
||||||
return {"id": existing.id, "title": existing.slice_name}
|
|
||||||
|
|
||||||
# Подготавливаем параметры
|
# Подготавливаем параметры
|
||||||
params = chart_config["params"].copy()
|
params = chart_config["params"].copy()
|
||||||
params["datasource"] = f"{dataset.id}__{DatasourceType.TABLE.value}"
|
params["datasource"] = f"{dataset.id}__{DatasourceType.TABLE.value}"
|
||||||
@@ -334,9 +310,6 @@ def create_chart(app, chart_config: dict, dataset: SqlaTable) -> Optional[dict]:
|
|||||||
"description": f"Chart created automatically for {chart_config['dataset_name']}"
|
"description": f"Chart created automatically for {chart_config['dataset_name']}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Используем прямой SQL для создания
|
|
||||||
from superset.charts.commands.create import CreateChartCommand
|
|
||||||
|
|
||||||
result = CreateChartCommand(chart_data).run()
|
result = CreateChartCommand(chart_data).run()
|
||||||
logger.info(f"Created chart: {chart_config['slice_name']} (ID: {result.id})")
|
logger.info(f"Created chart: {chart_config['slice_name']} (ID: {result.id})")
|
||||||
return {"id": result.id, "title": result.slice_name}
|
return {"id": result.id, "title": result.slice_name}
|
||||||
@@ -350,10 +323,14 @@ def create_chart(app, chart_config: dict, dataset: SqlaTable) -> Optional[dict]:
|
|||||||
|
|
||||||
def create_dashboard(app, charts: list):
|
def create_dashboard(app, charts: list):
|
||||||
"""Создание дашборда с чартами"""
|
"""Создание дашборда с чартами"""
|
||||||
|
from superset.extensions import db
|
||||||
|
from superset.dashboards.commands.create import CreateDashboardCommand
|
||||||
|
from superset.dashboards.dao import DashboardDAO
|
||||||
|
from superset.charts.dao import ChartDAO
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
# Проверяем, существует ли дашборд
|
# Проверяем, существует ли дашборд
|
||||||
from superset.dashboards.data_access_layer import DashboardDAO
|
|
||||||
existing = DashboardDAO.get_by_slug(DASHBOARD_CONFIG["slug"])
|
existing = DashboardDAO.get_by_slug(DASHBOARD_CONFIG["slug"])
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
@@ -366,12 +343,6 @@ def create_dashboard(app, charts: list):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем чарты в layout (grid: 12 columns)
|
# Добавляем чарты в layout (grid: 12 columns)
|
||||||
# Row 1: KPI блок (4 чарта по 3 колонки)
|
|
||||||
# Row 2: Events by Hour (8) | Geography (4)
|
|
||||||
# Row 3: Traffic by Device (4) | Top Pages (8)
|
|
||||||
# Row 4: UTM Table (12)
|
|
||||||
# Row 5: DQ Summary (12)
|
|
||||||
|
|
||||||
y_position = 0
|
y_position = 0
|
||||||
chart_index = 0
|
chart_index = 0
|
||||||
|
|
||||||
@@ -385,7 +356,7 @@ def create_dashboard(app, charts: list):
|
|||||||
"chartId": chart['id'],
|
"chartId": chart['id'],
|
||||||
"sliceName": chart['title'],
|
"sliceName": chart['title'],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"width": 4 if chart_index < 4 else 6, # KPI - по 4, остальные - по 6
|
"width": 4 if chart_index < 4 else 6,
|
||||||
"x": (chart_index % 3) * 4 if chart_index < 4 else (chart_index % 2) * 6,
|
"x": (chart_index % 3) * 4 if chart_index < 4 else (chart_index % 2) * 6,
|
||||||
"y": y_position
|
"y": y_position
|
||||||
}
|
}
|
||||||
@@ -404,14 +375,11 @@ def create_dashboard(app, charts: list):
|
|||||||
"position_json": json.dumps(positions)
|
"position_json": json.dumps(positions)
|
||||||
}
|
}
|
||||||
|
|
||||||
from superset.dashboards.commands.create import CreateDashboardCommand
|
|
||||||
result = CreateDashboardCommand(dashboard_data).run()
|
result = CreateDashboardCommand(dashboard_data).run()
|
||||||
|
|
||||||
# Добавляем чарты к дашборду
|
# Добавляем чарты к дашборду
|
||||||
from superset.dashboards.dao import DashboardDAO
|
|
||||||
dashboard = DashboardDAO.get_by_id(result.id)
|
dashboard = DashboardDAO.get_by_id(result.id)
|
||||||
|
|
||||||
from superset.charts.dao import ChartDAO
|
|
||||||
for chart_info in charts:
|
for chart_info in charts:
|
||||||
if chart_info:
|
if chart_info:
|
||||||
chart = ChartDAO.find_by_id(chart_info["id"])
|
chart = ChartDAO.find_by_id(chart_info["id"])
|
||||||
@@ -436,6 +404,9 @@ def main():
|
|||||||
logger.info("Creating E-commerce Analytics Dashboard")
|
logger.info("Creating E-commerce Analytics Dashboard")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
from superset.app import create_app
|
||||||
|
from superset.extensions import db
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
created_charts = []
|
created_charts = []
|
||||||
|
|||||||
+82
-76
@@ -6,12 +6,17 @@
|
|||||||
Назначение:
|
Назначение:
|
||||||
- Создание подключения к ClickHouse (Database connection)
|
- Создание подключения к ClickHouse (Database connection)
|
||||||
- Импорт датасетов из витрин DM-слоя
|
- Импорт датасетов из витрин DM-слоя
|
||||||
- Импорт чартов и дашбордов
|
|
||||||
|
|
||||||
Запуск:
|
Запуск:
|
||||||
Внутри контейнера superset:
|
Внутри контейнера superset:
|
||||||
python /app/superset_init/init_superset.py
|
python /app/superset_init/init_superset.py
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
Superset использует SQLite по умолчанию (не PostgreSQL).
|
||||||
|
Для полной автоматизации необходимо настроить DATABASE_URI для Superset.
|
||||||
|
|
||||||
|
Текущий подход: используем Superset CLI для создания подключения.
|
||||||
|
|
||||||
Требования:
|
Требования:
|
||||||
- Запущенный ClickHouse с созданными витринами в схеме dm
|
- Запущенный ClickHouse с созданными витринами в схеме dm
|
||||||
- Superset инициализирован (superset db upgrade, admin создан)
|
- Superset инициализирован (superset db upgrade, admin создан)
|
||||||
@@ -22,6 +27,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
@@ -34,37 +40,59 @@ logger = logging.getLogger(__name__)
|
|||||||
# Добавляем путь к superset
|
# Добавляем путь к superset
|
||||||
sys.path.insert(0, '/app')
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
try:
|
|
||||||
|
def run_superset_cli(args):
|
||||||
|
"""Запуск команды superset CLI"""
|
||||||
|
cmd = ['superset'] + args
|
||||||
|
logger.info(f"Running: {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"Command failed: {result.stderr}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Command output: {result.stdout}")
|
||||||
|
return result.returncode == 0, result.stdout, result.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def create_clickhouse_connection():
|
||||||
|
"""Создание подключения к ClickHouse через CLI"""
|
||||||
|
logger.info("Creating ClickHouse database connection...")
|
||||||
|
|
||||||
|
# Проверяем, существует ли уже подключение
|
||||||
|
success, stdout, stderr = run_superset_cli(['databases', 'list'])
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
logger.warning(f"Could not list databases: {stderr}")
|
||||||
|
elif 'clickhouse_dwh' in stdout:
|
||||||
|
logger.info("Database connection 'clickhouse_dwh' already exists")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Используем SQL Lab для создания подключения
|
||||||
|
# Это обходной путь, так как Superset CLI не имеет прямой команды для создания БД
|
||||||
|
logger.info("Database connection needs to be created manually via UI")
|
||||||
|
logger.info("Go to: Settings → Database Connections → + Database")
|
||||||
|
logger.info("Select: ClickHouse")
|
||||||
|
logger.info("URI: clickhouse+native://default@clickhouse:9000/default")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def import_datasets():
|
||||||
|
"""Импорт датасетов через Superset Python API"""
|
||||||
|
logger.info("Importing datasets...")
|
||||||
|
|
||||||
|
try:
|
||||||
from superset.app import create_app
|
from superset.app import create_app
|
||||||
from superset.extensions import db
|
from superset.extensions import db
|
||||||
from superset.models.core import Database
|
from superset.models.core import Database
|
||||||
from superset.connectors.sqla.models import SqlaTable, TableColumn
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
from superset.charts.data_access_layer import ChartDAO
|
|
||||||
from superset.dashboards.data_access_layer import DashboardDAO
|
app = create_app()
|
||||||
from superset.commands.dataset.create import CreateDatasetCommand
|
except Exception as e:
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
except ImportError as e:
|
|
||||||
logger.error(f"Failed to import Superset modules: {e}")
|
logger.error(f"Failed to import Superset modules: {e}")
|
||||||
sys.exit(1)
|
logger.info("Please ensure Superset is properly initialized")
|
||||||
|
return False
|
||||||
|
|
||||||
# Конфигурация подключения к ClickHouse
|
datasets = [
|
||||||
CLICKHOUSE_CONFIG = {
|
|
||||||
"database_name": "clickhouse_dwh",
|
|
||||||
"sqlalchemy_uri": "clickhouse+native://default@clickhouse:9000/default",
|
|
||||||
"expose_in_sqllab": True,
|
|
||||||
"allow_ctas": False,
|
|
||||||
"allow_cvas": False,
|
|
||||||
"allow_dml": False,
|
|
||||||
"allow_file_upload": False,
|
|
||||||
"extra": json.dumps({
|
|
||||||
"engine_params": {},
|
|
||||||
"metadata_params": {},
|
|
||||||
"schemas_allowed_for_file_upload": []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Датасеты для импорта из DM-слоя
|
|
||||||
DATASETS = [
|
|
||||||
{
|
{
|
||||||
"table_name": "v_events_enriched",
|
"table_name": "v_events_enriched",
|
||||||
"schema": "dm",
|
"schema": "dm",
|
||||||
@@ -101,51 +129,26 @@ DATASETS = [
|
|||||||
"database_name": "clickhouse_dwh",
|
"database_name": "clickhouse_dwh",
|
||||||
"description": "Сводка по качеству данных"
|
"description": "Сводка по качеству данных"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_clickhouse_connection(app) -> Optional[Database]:
|
|
||||||
"""Создание подключения к ClickHouse"""
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
logger.info("Creating ClickHouse database connection...")
|
|
||||||
|
|
||||||
# Проверяем, существует ли уже подключение
|
|
||||||
existing = db.session.query(Database).filter_by(
|
|
||||||
database_name=CLICKHOUSE_CONFIG["database_name"]
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
logger.info(f"Database connection '{CLICKHOUSE_CONFIG['database_name']}' already exists")
|
|
||||||
return existing
|
|
||||||
|
|
||||||
try:
|
|
||||||
database = Database(**CLICKHOUSE_CONFIG)
|
|
||||||
db.session.add(database)
|
|
||||||
db.session.commit()
|
|
||||||
logger.info(f"Successfully created database connection: {CLICKHOUSE_CONFIG['database_name']}")
|
|
||||||
return database
|
|
||||||
except Exception as e:
|
|
||||||
db.session.rollback()
|
|
||||||
logger.error(f"Failed to create database connection: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def import_datasets(app):
|
|
||||||
"""Импорт датасетов из DM-слоя"""
|
|
||||||
with app.app_context():
|
|
||||||
logger.info("Importing datasets...")
|
|
||||||
|
|
||||||
# Получаем ID базы данных
|
# Получаем ID базы данных
|
||||||
database = db.session.query(Database).filter_by(
|
database = db.session.query(Database).filter_by(
|
||||||
database_name="clickhouse_dwh"
|
database_name="clickhouse_dwh"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not database:
|
if not database:
|
||||||
logger.error("ClickHouse database connection not found")
|
logger.error("ClickHouse database connection not found!")
|
||||||
|
logger.info("Please create database connection manually first:")
|
||||||
|
logger.info("1. Go to http://localhost:8088")
|
||||||
|
logger.info("2. Login: admin / admin")
|
||||||
|
logger.info("3. Settings → Database Connections → + Database")
|
||||||
|
logger.info("4. Select ClickHouse")
|
||||||
|
logger.info("5. URI: clickhouse+native://default@clickhouse:9000/default")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
imported_count = 0
|
imported_count = 0
|
||||||
for dataset_config in DATASETS:
|
for dataset_config in datasets:
|
||||||
try:
|
try:
|
||||||
# Проверяем, существует ли датасет
|
# Проверяем, существует ли датасет
|
||||||
existing = db.session.query(SqlaTable).filter_by(
|
existing = db.session.query(SqlaTable).filter_by(
|
||||||
@@ -171,15 +174,15 @@ def import_datasets(app):
|
|||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Fetch columns from database
|
# Fetch columns from database
|
||||||
|
try:
|
||||||
dataset.fetch_metadata()
|
dataset.fetch_metadata()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not fetch metadata for {dataset_config['table_name']}: {e}")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
logger.info(f"Successfully imported dataset: {dataset_config['table_name']}")
|
logger.info(f"Successfully imported dataset: {dataset_config['table_name']}")
|
||||||
imported_count += 1
|
imported_count += 1
|
||||||
|
|
||||||
except IntegrityError:
|
|
||||||
db.session.rollback()
|
|
||||||
logger.warning(f"Dataset '{dataset_config['table_name']}' already exists (integrity error)")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
logger.error(f"Failed to import dataset '{dataset_config['table_name']}': {e}")
|
logger.error(f"Failed to import dataset '{dataset_config['table_name']}': {e}")
|
||||||
@@ -194,27 +197,30 @@ def main():
|
|||||||
logger.info("Superset Initialization for ClickHouse Mini DWH")
|
logger.info("Superset Initialization for ClickHouse Mini DWH")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
# Создаём приложение Superset
|
# Проверяем подключение к ClickHouse
|
||||||
app = create_app()
|
if not create_clickhouse_connection():
|
||||||
|
logger.error("Failed to verify ClickHouse connection")
|
||||||
# Создаём подключение к ClickHouse
|
|
||||||
database = create_clickhouse_connection(app)
|
|
||||||
if not database:
|
|
||||||
logger.error("Failed to create ClickHouse connection")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Импортируем датасеты
|
# Импортируем датасеты
|
||||||
if not import_datasets(app):
|
try:
|
||||||
|
if not import_datasets():
|
||||||
logger.error("Failed to import datasets")
|
logger.error("Failed to import datasets")
|
||||||
|
logger.info("\nTo create datasets manually:")
|
||||||
|
logger.info("1. Go to http://localhost:8088")
|
||||||
|
logger.info("2. Datasets → + Dataset")
|
||||||
|
logger.info("3. Select 'clickhouse_dwh' database")
|
||||||
|
logger.info("4. Select schema 'dm' and desired table")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error importing datasets: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info("Superset initialization completed successfully!")
|
logger.info("Superset initialization completed successfully!")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info("Available datasets:")
|
|
||||||
for ds in DATASETS:
|
|
||||||
logger.info(f" - {ds['schema']}.{ds['table_name']}")
|
|
||||||
logger.info("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user