基于superset+Doris创建的报表,报表体验丝滑,界面看上去舒服,Superset默认只支持Slack和EMail推送报表和报警,不能像ELK那样支持Webhook。通过下面几行简单修改即可实现Webhook功能,将数据推送到指定的Webhook上,利用自己擅长的编程语言(如PHP、Java、Python、Go等)玩出属于你的火花,可以发送到钉钉、企业微信、公众号、短信等,畅想无限可能。
这个是本人的案例,每月初将报表推送到钉钉群里。
我用的docker容器构建的Superset,版本4.0.2,以下基于Docker容器标准来操作的,别的架构方式大同小异,在superset-worker容器中完成如下操作。
新增文件/app/superset/reports/notifications/webhook.py
# file:/app/superset/reports/notifications/webhook.py
import json
import logging
import textwrap
import requests
import base64
from dataclasses import dataclass
from email.utils import make_msgid, parseaddr
from typing import Any, Optional
from flask_babel import gettext as __
from superset import app
from superset.exceptions import SupersetErrorsException
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
logger = logging.getLogger(__name__)
@dataclass
class WebhookContent:
body: str
header_data: Optional[HeaderDataType] = None
data: Optional[dict[str, Any]] = None
images: Optional[dict[str, bytes]] = None
class WebhookNotification(BaseNotification): # pylint: disable=too-few-public-methods
"""
Calls webhook for a report recipient
"""
type = ReportRecipientType.WEBHOOK
@staticmethod
def _error_template(text: str) -> str:
return __(
"""
Error: %(text)s
""",
text=text,
)
def _get_content(self) -> WebhookContent:
if self._content.text:
return WebhookContent(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 = [
base64.b64encode(screenshot).decode('ascii')
for screenshot in self._content.screenshots
]
if self._content.csv:
csv_data = {__("%(name)s.csv", name=self._content.name): self._content.csv}
return WebhookContent(
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()
content = self._get_content()
to = self._get_to()
headers = {
}
payload = {
'subject': subject,
'content': {
#'header_data': content.header_data,
'body': content.body,
'data': content.data,
'images': content.images,
}
}
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
- 修改/app/superset/views/base.py:
if conf.get("SLACK_API_TOKEN"):
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
ReportRecipientType.SLACK,
ReportRecipientType.WEBHOOK, # 增加此行
]
else:
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
ReportRecipientType.WEBHOOK, # 增加此行
]
- 修改/app/superset/reports/models.py:
class ReportRecipientType(StrEnum):
EMAIL = "Email"
SLACK = "Slack"
WEBHOOK = "Webhook" #增加此行
- 修改/app/superset/reports/notifications/__init__.py
from superset.reports.models import ReportRecipients
from superset.reports.notifications.base import BaseNotification, NotificationContent
from superset.reports.notifications.email import EmailNotification
from superset.reports.notifications.slack import SlackNotification
from superset.reports.notifications.webhook import WebhookNotification #增加此行
- 完成以上操作后重启服务,此时可以看到通知方式中有了Webhook选项,在接收者中填写API地址(我的这个地址是指向PHP写的接口,最终推送到钉钉群里):
APi将会收到如下json数据:
payload = {
'subject': subject, //字符串
'content': {
'body': content.body, //字符串
'data': content.data, //字符串
'images': content.images, //数组 [图片base64字符串]
}
}
最后附上我的PHP的Webhook逻辑和docker-compose代码,仅供参考,这部分根据自己情况了~!
php代码
class Superset extends Base
{
public function report(){
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if(empty($data)){
exit('data empty');
}
$markdown = "### ".$data['subject'];
foreach($data['content']['images'] as $k=>$v){
$url = $this->upload($v); //将base64图片上传到cdn上,并获取url
$markdown .= "\r\n";
$markdown .= "";
}
$url = 'https://oapi.dingtalk.com/robot/send?access_token=xxx';
$payload = [
'msgtype' => 'markdown',
'markdown' => [
'title' => '数据报告',
'text' => $markdown,
]
];
$resp = Services::guzzle()->post($url, ['json' => $payload]);
Services::logging()->write_log('superset', 'Call DINGDING robot api, response code:'.$resp->getStatusCode().',payload:'.var_export($payload, true).',content:'.$resp->getBody()->getContents());
}
private function upload($blob){
....
}
}
docker-compose.yml
x-superset-volumes:
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- superset_home:/app/superset_home
x-common-build: &common-build apache/superset:4.0.2
services:
superset:
env_file:
- path: docker/.env # default
required: true
image: *common-build
container_name: superset_app
command: ["/app/docker/docker-bootstrap.sh", "app-gunicorn"]
user: "root"
restart: unless-stopped
networks:
- superset
ports:
- 8081:8088
volumes: *superset-volumes
superset-init:
container_name: superset_init
image: *common-build
command: ["/app/docker/docker-init.sh"]
env_file:
- path: docker/.env # default
required: true
user: "root"
volumes: *superset-volumes
networks:
- superset
healthcheck:
disable: true
superset-worker:
image: *common-build
container_name: superset_worker
command: ["/app/docker/docker-bootstrap.sh", "worker"]
env_file:
- path: docker/.env # default
required: true
restart: unless-stopped
user: "root"
volumes: *superset-volumes
networks:
- superset
healthcheck:
test:
[
"CMD-SHELL",
"celery -A superset.tasks.celery_app:app inspect ping -d celery@$$HOSTNAME",
]
superset-worker-beat:
image: *common-build
container_name: superset_worker_beat
command: ["/app/docker/docker-bootstrap.sh", "beat"]
env_file:
- path: docker/.env # default
required: true
restart: unless-stopped
user: "root"
volumes: *superset-volumes
networks:
- superset
healthcheck:
disable: true
volumes:
superset_home:
external: false
networks:
superset:
ipam:
config:
- subnet: 172.19.1.0/24