前言
本文基于 superset 5.0.0 版本进行配置以及二次开发。
github:https://github.com/apache/superset
文档:https://superset.apache.org/docs/
中文文档:https://superset.org.cn/docs/
安装&运行
Docker运行
参考文档:https://superset.org.cn/docs/contributing/development/#docker-compose-recommended
1
2
3
|
# getting docker compose to fire up services, and rebuilding if some docker layers have changed
# using the `--build` suffix may be slower and optional if layers like py dependencies haven't changed
docker compose up --build
|
运行最简单,自动帮你配置各种数据库依赖
本地运行
参考官方文档:https://superset.org.cn/docs/contributing/development/#flask-server
1
2
3
4
5
6
7
8
9
|
# Create a virtual environment and activate it (recommended)
$ python3 -m venv venv # setup a python3 virtualenv
$ source venv/bin/activate
# install pip packages + pre-commit
$ make install
# Install superset pip packages and setup env only
$ make superset
|
前端页面
1
|
cd superset-frontend; npm run dev-server
|
服务端
1
|
venv/bin/gunicorn superset.app:create_app() --bind 0.0.0.0:8088
|
Worker
1
|
venv/bin/celery --app=superset.tasks.celery_app:app worker --pool=prefork --max-tasks-per-child=128 -c 4
|
配置文件
关于环境变量以及配置参考:
.env
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
PYTHONPATH=/Users/marun/code/leetCode-study/apache/superset/docker/pythonpath_dev
SUPERSET_PORT=8088
SMTP_HOST=smtp.exmail.qq.com
SMTP_MAIL_FROM=
SMTP_PASSWORD=
SMTP_PORT=25
SMTP_USER=
SUPERSET_LOG_LEVEL=DEBUG
DB_HOST=
DB_NAME=superset
DB_PASS=''
DB_PORT=3306
DB_USER=
REDIS_CELERY_DB=28
REDIS_DB=27
REDIS_HOST=
REDIS_PASSWORD=
REDIS_PORT=6379
REDIS_PROTO=redis
REDIS_USER=
|
这里定义了配置文件夹PYTHONPATH=/Users/marun/code/leetCode-study/apache/superset/docker/pythonpath_dev
配置文件都放在这里配置文件下,保持跟 docker-compose一致
PyCharm 脚本启动配置:

helm k8s 运行
安装
1
|
cd helm/supersethelm install -n company superset ./ --value values.yaml
|
更新
1
|
cd helm/supersethelm upgrade superset ./ -n company --values values.yaml
|
配置项
注意以下配置无特殊说明的话,均在superset_config.py
中
缓存配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
# Overrides
# cache
FILTER_STATE_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_filter_cache:',
'CACHE_REDIS_URL': f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_DB')}"
}
EXPLORE_FORM_DATA_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_explore_cache:',
'CACHE_REDIS_URL': f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_DB')}"
}
GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_async_queries_cache:',
'CACHE_REDIS_HOST' : env('REDIS_HOST'),
'CACHE_REDIS_DB' : env('REDIS_DB'),
'CACHE_REDIS_PASSWORD' : env('REDIS_PASSWORD')
}
CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_cache:',
'CACHE_REDIS_URL': f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_DB')}"
}
DATA_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 86400,
'CACHE_KEY_PREFIX': 'superset_data_cache:',
'CACHE_REDIS_URL': f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_DB')}"
}
THUMBNAIL_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 604800,
'CACHE_KEY_PREFIX': 'thumbnail_:',
'CACHE_REDIS_URL': f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_DB')}"
}
from flask_caching.backends.rediscache import RedisCache
RESULTS_BACKEND = RedisCache(
host=env('REDIS_HOST'),
password=env('REDIS_PASSWORD'),
port=env('REDIS_PORT'),
key_prefix='superset_results:',
db=env('REDIS_DB')
)
|
增加一些缓存配置
Celery Worker 的一些配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
# celery_conf
from celery.schedules import crontab
class CeleryConfig:
broker_url = f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_CELERY_DB')}"
imports = (
"superset.sql_lab",
"superset.tasks.cache",
"superset.tasks.scheduler",
)
result_backend = f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/{env('REDIS_CELERY_DB')}"
task_annotations = {
"sql_lab.get_sql_results": {
"rate_limit": "100/s",
},
}
beat_schedule = {
"reports.scheduler": {
"task": "reports.scheduler",
"schedule": crontab(minute="*", hour="*"),
},
"reports.prune_log": {
"task": "reports.prune_log",
'schedule': crontab(minute=0, hour=0),
},
'cache-warmup-hourly': {
"task": "cache-warmup",
"schedule": crontab(minute="*/30", hour="*"),
"kwargs": {
"strategy_name": "top_n_dashboards",
"top_n": 10,
"since": "7 days ago",
},
}
}
CELERY_CONFIG = CeleryConfig
SCREENSHOT_LOAD_WAIT=60
SCREENSHOT_LOCATE_WAIT = 60
# 替换为自己的密钥
GLOBAL_ASYNC_QUERIES_JWT_SECRET = ""
|
配置 Worker 缓存 以及一些调度规则之类的。
SMTP邮箱配置
1
2
3
4
5
6
7
|
SMTP_HOST = os.getenv("SMTP_HOST","localhost")
SMTP_STARTTLS = False
SMTP_SSL = False
SMTP_USER = os.getenv("SMTP_USER","superset")
SMTP_PORT = os.getenv("SMTP_PORT",25)
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD","superset")
SMTP_MAIL_FROM = os.getenv("SMTP_MAIL_FROM","superset@superset.com")
|
主要是为了告警跟报告发送的。
告警&报告发送
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# reports
EMAIL_PAGE_RENDER_WAIT = 60
WEBDRIVER_BASEURL = "https://superset.cn"
WEBDRIVER_BASEURL_USER_FRIENDLY = "https://superset.cn"
WEBDRIVER_TYPE = "chrome"
WEBDRIVER_OPTION_ARGS = [
"--force-device-scale-factor=2.0",
"--high-dpi-support=2.0",
"--headless",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-extensions",
]
|
记得要在FEATURE_FLAGS
中 增加 "ALERT_REPORTS": True,
开启告警和报告
上面的配置是使用 chrome 来进行截图,我在本地运行使用的时候会去掉"--headless",
看一下chrome 截图会卡在哪一步。
嵌入式SDK设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
# embedding
FEATURE_FLAGS = {
"EMBEDDED_SUPERSET": True,
"EMBEDDED_SUPERSET_API": True,
"EMBEDDABLE_DASHBOARDS": True,
"GLOBAL_ASYNC_QUERIES": True,
"ALERT_REPORTS": True,
"ENABLE_TEMPLATE_PROCESSING" : True,
}
# 设置 guest token 密钥,生产环境请务必更换为强密码
GUEST_TOKEN_JWT_SECRET = ""
GUEST_TOKEN_JWT_AUDIENCE="superset"
TALISMAN_ENABLED = True
TALISMAN_CONFIG = {
"content_security_policy": {
# 其他 CSP 配置...
"frame-ancestors": ["http://localhost:5173"],
},
"force_https" : False
}
ENABLE_CORS = True
WTF_CSRF_ENABLED = True
PUBLIC_ROLE_LIKE = "Gamma"
WTF_CSRF_EXEMPT_LIST = [
"superset.views.core.log",
"superset.views.core.explore_json",
"superset.charts.data.api.data",
"superset.dashboards.api.cache_dashboard_screenshot",
"superset.security.api.guest_token",
"superset.charts.api.warm_up_cache",
"superset.datasets.api.warm_up_cache",
]
CORS_OPTIONS = {
"supports_credentials": True,
"origins": [
"http://localhost:5173"
],
"methods": ["GET", "POST", "OPTIONS", "PUT", "DELETE"],
"allow_headers": "*",
"expose_headers": "*",
"resources": "*"
}
|
这里是一些关于报表嵌入的配置,后文会单独说明
oauth 认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
# enable_oauth
# This will make sure the redirect_uri is properly computed, even with SSL offloading
ENABLE_PROXY_FIX = True
from flask_appbuilder.security.manager import AUTH_OAUTH
from custom_sso_security_manager import CustomSsoSecurityManager
AUTH_TYPE = AUTH_OAUTH
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
OAUTH_PROVIDERS = [
{
"name": "dex",
"icon": "fa-address-card",
"token_key": "access_token",
"remote_app": {
"client_id": "superset-login",
"client_secret": "",
"api_base_url": "https://dex-auth.cn",
"authorize_url": "https://dex-auth.cn/auth?connector_id=fsoa",
"access_token_url": "https://dex-auth.cn/token",
"userinfo_endpoint": "https://dex-auth.cn/userinfo",
'jwks_uri':'https://dex-auth.cn/keys',
"client_kwargs": {"scope": "openid email profile"}
},
}
]
# Map Authlib roles to superset roles
AUTH_ROLE_ADMIN = 'Admin'
AUTH_ROLE_PUBLIC = 'Public'
# Will allow user self registration, allowing to create Flask users from Authorized User
AUTH_USER_REGISTRATION = True
# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = "Gamma"
|
注意这里,我使用的是 dex 认证,需要多个额外的配置文件。
与配置文件同目录custom_sso_security_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import logging
from superset.security import SupersetSecurityManager
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
userinfo = {}
# logging.debug("Oauth2 provider: {0}.".format(provider))
if provider == 'dex':
# As example, this line request a GET to base_url + '/' + userDetails with Bearer Authentication,
# and expects that authorization server checks the token, and response with user details
me = self.appbuilder.sm.oauth_remotes[provider].get('userinfo').json()
# logging.debug("user_data me: {0}".format(me))
# logging.debug("user_data me[name]: {0}".format(me['name']))
# logging.debug("user_data me[groups]: {0}".format(me['groups']))
userinfo = {
'name' : me['name'],
'email' : me['email'],
'id' : me['name'],
'username' : me['name'],
'first_name': me['name'],
'groups': []
}
logging.debug("user_info: {0}".format(userinfo))
return userinfo
else:
return userinfo
|
将 dex 返回的用户信息转换为superset 可以正常读取的。
参考:https://gist.github.com/nelaaro/a2ef6f2a268d5a8a7caf6676e2ef2bb5
如果你不是使用的 dex, 或者使用其他的 oauth 服务,需要自己考虑是否需要这个额外配置。
多语言
1
2
3
4
5
|
# language
LANGUAGES = {
"zh": {"flag": "cn", "name": "Chinese"},
}
BABEL_DEFAULT_LOCALE = "zh"
|
我这里配置的默认中文,这里配置之后后面部署的时候 也要编译多语言文件,后文会介绍
其他配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# my_override
SQLALCHEMY_DATABASE_URI = f"mysql://{env('DB_USER')}:{env('DB_PASS')}@{env('DB_HOST')}:{env('DB_PORT')}/{env('DB_NAME')}"
RATELIMIT_STORAGE_URI=f"{env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/28"
LOG_LEVEL = "DEBUG"
SUPERSET_LOG_LEVEL = "DEBUG"
# secret
# Generate your own secret key for encryption. Use `openssl rand -base64 42` to generate a good key
SECRET_KEY = ''
# Lark
LARK_APP_ID=''
LARK_APP_SECRET=''
LARK_LOG_LEVEL='debug'
|
这里我将默认的postgresql
改为mysql
然后这里的SECRET_KEY
记得自己生成一个新的。
Lark 是我加了飞书消息发送的,一些配置,后面会提到。
k8s 配置
关于在 k8s 中运行,上面这些配置基本都是在configOverrides
下

额外的依赖
如果开启了一些其他的功能,就需要在 python 中安装额外的依赖,例如 clickhouse,mysql,oatuh 等等。
k8s配置
我们如果是在 k8s 中运行,可以放在 values.yaml
文件的bootstrapScript
字段中。
例如:
1
2
3
4
5
6
7
|
bootstrapScript: |
#!/bin/bash
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ && \
pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn && \
uv pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mysqlclient Authlib \
"clickhouse-connect>=0.6.8" && \
if [ ! -f ~/bootstrap ]; then echo "Running Superset with uid {{ .Values.runAsUser }}" > ~/bootstrap; fi
|
但是这样每次容器重新都要重新安装一下,比较麻烦,建议自行编译 docker 镜像,在镜像中安装好。
dockerfile文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# ========== 生产镜像 ==========
FROM apache/superset:5.0.0
USER root
RUN sed -i 's|http://deb.debian.org|https://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y default-libmysqlclient-dev build-essential pkg-config wget zip libaio1
# 安装依赖(Superset 4.1 及以后推荐用 uv,老版本用 pip)
RUN . /app/.venv/bin/activate && \
uv pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mysqlclient \
"clickhouse-connect>=0.6.8" \
psycopg2-binary \
pymssql \
Authlib \
openpyxl \
Pillow \
flask-cors \
gevent \
psycopg2 \
redis \
lark_oapi
|
类似上面这样,在基础镜像中安装自己需要的依赖。
chrome安装
如果你需要告警 报告发送,那么你需要安装一个浏览器依赖进行截图,我这里选择的是 chrome。
1
2
3
4
5
6
7
8
9
10
|
RUN wget -O google-chrome-stable_current_amd64.deb -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
apt-get install -y --no-install-recommends ./google-chrome-stable_current_amd64.deb && \
rm -f google-chrome-stable_current_amd64.deb
# 这里的版本号构建的时候记得换成最新的,跟上面的 google-chrome 保持一致
RUN wget -q https://storage.googleapis.com/chrome-for-testing-public/138.0.7204.100/linux64/chromedriver-linux64.zip && \
unzip -j chromedriver-linux64.zip -d /usr/bin && \
chmod 755 /usr/bin/chromedriver && \
rm -f chromedriver-linux64.zip
|
多语言构建
在上面的配置项中,我们已经配置了对语言,并选择了默认中文, 但默认的 docker 镜像是没有构建多语言的,需要我们自行构建。
其中前端多语言构建:
1
|
cd superset-frontend && npm run build-translation
|
python 多语言构建:
1
|
pybabel compile -d ./superset/translations
|
运行完之后,你会在superset/translations/zh/LC_MESSAGES
下看到构建好的多语言文件。
完整dockerfile
为了更方便使用,我们将上面的额外依赖安装,chrome安装,以及多语言构建都合并到一起。
Dockfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
# Dockerfile
# ========== 前端构建多语言 ==========
FROM node:20-bookworm-slim AS frontend-build
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
WORKDIR /app
# 复制前端代码和依赖文件
COPY superset-frontend ./superset-frontend
COPY superset/translations ./superset/translations
# 安装翻译工具
RUN npm install -g po2json && npm install -g prettier
# 编译前端(含多语言)
RUN cd superset-frontend && npm run build-translation
# ========== 生产镜像 ==========
FROM apache/superset:5.0.0
USER root
RUN sed -i 's|http://deb.debian.org|https://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y default-libmysqlclient-dev build-essential pkg-config wget zip libaio1
RUN wget -O google-chrome-stable_current_amd64.deb -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
apt-get install -y --no-install-recommends ./google-chrome-stable_current_amd64.deb && \
rm -f google-chrome-stable_current_amd64.deb
RUN wget -q https://storage.googleapis.com/chrome-for-testing-public/138.0.7204.100/linux64/chromedriver-linux64.zip && \
unzip -j chromedriver-linux64.zip -d /usr/bin && \
chmod 755 /usr/bin/chromedriver && \
rm -f chromedriver-linux64.zip
# 安装依赖(Superset 4.1 及以后推荐用 uv,老版本用 pip)
RUN . /app/.venv/bin/activate && \
uv pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mysqlclient \
"clickhouse-connect>=0.6.8" \
psycopg2-binary \
pymssql \
Authlib \
openpyxl \
Pillow \
flask-cors \
gevent \
psycopg2 \
redis \
lark_oapi
# 复制后端翻译文件
COPY superset/translations ./superset/translations
COPY --from=frontend-build /app/superset/translations /app/superset/translations
# 编译 mo 文件
RUN pybabel compile -d ./superset/translations | true;
# 拷贝前端静态资源和翻译文件
USER superset
CMD ["/app/docker/entrypoints/run-server.sh"]
|
增加告警报告发送渠道
由于 superset 默认只有邮件 以及 Slack 通知,不太符合国内的使用习惯。 需要增加其他的发送渠道,我这里用的飞书就选择了飞书机器人通知。
上传文件接口
https://open.feishu.cn/document/server-docs/im-v1/image/create
注意,飞书的图片发送必须要先上传,才可以发送图片,不支持图片的 base64 发送。
上传图片又需要自建应用,拿到 appid secret, 所以需要先创建一个应用。
安装飞书依赖
https://open.feishu.cn/document/server-side-sdk/python--sdk/preparations-before-development
1
|
pip install lark-oapi -U
|
服务端代码
新增文件:superset/reports/notifications/lark.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
import json
import logging
from io import BytesIO
import requests
from dataclasses import dataclass
from flask import current_app
from flask_babel import gettext as __
from superset import app
from superset.reports.models import ReportRecipientType
from superset.reports.notifications.base import BaseNotification
from superset.reports.notifications.exceptions import NotificationError
from superset.utils.core import HeaderDataType, send_email_smtp
from superset.utils.decorators import statsd_gauge
import lark_oapi as lark
from lark_oapi.api.im.v1 import *
logger = logging.getLogger(__name__)
@dataclass
class LarkContent:
body: str
header_data: Optional[HeaderDataType] = None
data: Optional[dict[str, Any]] = None
images: Optional[dict[str, bytes]] = None
def str_to_lark_log_level(level_str: str) -> Optional[lark.LogLevel]:
level_map = {
"DEBUG": lark.LogLevel.DEBUG,
"INFO": lark.LogLevel.INFO,
"WARNING": lark.LogLevel.WARNING,
"ERROR": lark.LogLevel.ERROR,
"CRITICAL": lark.LogLevel.CRITICAL,
}
return level_map.get(level_str.upper(), lark.LogLevel.INFO)
def get_client() -> lark.Client:
return lark.Client.builder() \
.app_id(current_app.config["LARK_APP_ID"]) \
.app_secret(current_app.config["LARK_APP_SECRET"]) \
.log_level(str_to_lark_log_level(current_app.config["LARK_LOG_LEVEL"])) \
.build()
def upload_image(img: bytes) -> str:
client = get_client()
# 构造请求对象
request: CreateImageRequest = CreateImageRequest.builder() \
.request_body(CreateImageRequestBody.builder()
.image_type("message") \
.image(BytesIO(img))
.build()) \
.build()
try:
response: CreateImageResponse = client.im.v1.image.create(request)
except requests.exceptions.HTTPError as ex:
raise NotificationError(f"HTTP error occurred: {ex}") from ex
except Exception as ex:
raise NotificationError(f"An error occurred: {ex}") from ex
# error
if not response.success():
raise NotificationError(
f"Lark upload image error : code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
return response.data.image_key
class LarkNotification(BaseNotification): # pylint: disable=too-few-public-methods
"""
Calls webhook for a report recipient """
type = ReportRecipientType.Lark
@staticmethod
def _error_template(text: str) -> str:
return __(
"""
Error: %(text)s """,
text=text,
)
def _get_content(self) -> LarkContent:
if self._content.text:
return LarkContent(body=self._error_template(self._content.text))
# Get the domain from the 'From' address ..
# and make a message id without the < > in the end csv_data = None
images = {}
if self._content.screenshots:
images = [
upload_image(screenshot)
for screenshot in self._content.screenshots
]
if self._content.csv:
csv_data = {__("%(name)s.csv", name=self._content.name): self._content.csv}
return LarkContent(
images=images,
data=csv_data,
body=None,
header_data=self._content.header_data,
)
def _get_subject(self) -> str:
return __(
"%(prefix)s %(title)s",
prefix=app.config["EMAIL_REPORTS_SUBJECT_PREFIX"],
title=self._content.name,
)
def _get_to(self) -> str:
return json.loads(self._recipient.recipient_config_json)["target"]
@statsd_gauge("reports.webhook.send")
def send(self) -> None:
subject = self._get_subject()
to = self._get_to()
content = self._get_content()
markdownContent = ""
for _, img in enumerate(content.images):
markdownContent += f"\n"
headers = {
'Content-type': 'application/json'
}
payload = {
"msg_type": "interactive",
"card": {
"schema": "2.0",
"config": {
"enable_forward": True,
"update_multi": True
},
"body": {
"direction": "vertical",
"elements": [
{
"tag": "markdown",
"content": markdownContent
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": subject
},
"subtitle": {
"tag": "plain_text",
"content": "点击标题跳转到报表地址"
},
},
"card_link": {
"url": self._content.url
}
}
}
try:
response = requests.post(to, headers=headers, data=json.dumps(payload),
timeout=30)
response.raise_for_status()
logger.info(
"Report sent to webhook, notification content is %s, url:%s, status scode:%d",
content.header_data, to, response.status_code)
except requests.exceptions.HTTPError as ex:
raise NotificationError(f"HTTP error occurred: {ex}") from ex
except Exception as ex:
raise NotificationError(f"An error occurred: {ex}") from ex
|
这里 payload
可以根据需要,自行调整。
LARK_APP_ID
和 LARK_APP_SECRET
这两个配置放在配置文件中即可,填自己的应用 id 和 secret。
superset/reports/notifications/__init__.py
添加新行
1
|
from superset.reports.notifications.lark import LarkNotification
|
superset/reports/models.py
添加新枚举类型
1
2
3
4
5
|
class ReportRecipientType(StrEnum):
EMAIL = "Email"
SLACK = "Slack"
SLACKV2 = "SlackV2"
+ Lark = "Lark"
|
superset/views/base.py
添加
1
2
3
4
5
6
7
8
9
10
11
12
|
if conf.get("SLACK_API_TOKEN"):
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
ReportRecipientType.SLACK,
ReportRecipientType.SLACKV2,
+ ReportRecipientType.Lark,
]
else:
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
+ ReportRecipientType.Lark,
]
|
前端代码
superset-frontend/src/features/alerts/types.ts
添加
1
2
3
4
5
6
7
|
export enum NotificationMethodOption {
Email = 'Email',
Slack = 'Slack',
SlackV2 = 'SlackV2',
+ Lark = 'Lark',
}
|
superset-frontend/src/features/alerts/AlertReportModal.tsx
添加
1
2
3
4
|
const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = [
NotificationMethodOption.Email,
+ NotificationMethodOption.Lark,
];
|
superset-frontend/src/features/alerts/components/NotificationMethod.tsx
添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
const methodOptions = useMemo(
() =>
(options || [])
.filter(
method =>
(isFeatureEnabled(FeatureFlag.AlertReportSlackV2) &&
!useSlackV1 &&
method === NotificationMethodOption.SlackV2) ||
((!isFeatureEnabled(FeatureFlag.AlertReportSlackV2) ||
useSlackV1) &&
method === NotificationMethodOption.Slack) ||
+ method === NotificationMethodOption.Email ||
+ method === NotificationMethodOption.Lark,
)
.map(method => ({
label:
method === NotificationMethodOption.SlackV2
? NotificationMethodOption.Slack
: method,
value: method,
})),
[options, useSlackV1],
);
...
{[
NotificationMethodOption.Email,
NotificationMethodOption.Slack,
+ NotificationMethodOption.Lark,
].includes(method) ? (
<>
<div className="input-container">
<Input.TextArea name="To"
data-test="recipients"
value={recipientValue}
onChange={onRecipientsChange}
/>
</div> <div className="input-container">
<div className="helper">
{t('Recipients are separated by "," or ";"')}
</div>
</div> </>)
|
修改之后 重新运行 superset ,worker,以及前端项目。
可以看到通知方式中多了一个 Lark

收到通知大概是这样

如果不是使用的飞书,也可以在这个基础上稍微改改就能用。
比如直接做一个通用的 webhook, 配置推送地址 以及推送模板, 模板使用 Jin2 ,就可以实现很多推送了。
对于飞书这种需要额外上传图片的就要做单独适配了。