Skip to content

Commit

Permalink
Build custom alert message (getredash#3137)
Browse files Browse the repository at this point in the history
* build custom alert message

* fit button color tone

* pass existing test

* fix typos

* follow code style

* add webhook alert description and avoid key error

* refactor: create alert template module

* follow code style

* use es6 class, fix template display

* use alerts.options, use mustache

* fix email description

* alert custom subject

* add alert state to template context, sanitized preview

* remove console.log 🙇

* chatwork custom_subject

* add alert custom message. pagerduty, mattermost, hangoutschat

* Pass custom subject in webhook destination

* Add log message when checking alert.

* Add feature flag for extra alert options.
  • Loading branch information
k-tomoyasu authored and The-Alchemist committed Jul 15, 2019
1 parent 43c98fd commit f12b412
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 13 deletions.
33 changes: 33 additions & 0 deletions client/app/pages/alert/alert.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,39 @@
</div>
</div>
</div>
<div class="form-group" ng-show="$ctrl.selectedQuery && $ctrl.showExtendedOptions">
<label>Custom subject</label>
<input type="string" class="form-control" ng-model="$ctrl.alert.options.subject" ng-disabled="!$ctrl.canEdit">
</div>
<div ng-show="$ctrl.selectedQuery && $ctrl.showExtendedOptions">
<div class="form-group" ng-show="$ctrl.selectedQuery">
<label>Description template</label>
<i class="fa fa-question-circle" uib-tooltip="{{$ctrl.alertTemplate.helpMessage}}"></i>
<div class="row bg-white p-b-5" ng-if="$ctrl.canEdit" resizable r-directions="['bottom']" r-height="300" style="min-height:100px;">
<div ui-ace="$ctrl.alertTemplate.editorOptions" ng-model="$ctrl.alert.options.template"></div>
</div>
</div>
<div class="form-group" ng-if="$ctrl.canEdit">
<button class="btn btn-default" ng-click="$ctrl.preview()">Preview</button>
<label for="show-as-html">Show As HTML</label>
<input type="checkbox" name="show-as-html" ng-model="$ctrl.showAsHTML">
</div>
<div class="panel panel-default" ng-if="$ctrl.alert.preview">
<div class="panel-heading">
<label for="hide-preview">Hide Preview</label>
<input type="checkbox" name="hide-preview" ng-model="$ctrl.hidePreview">
</div>
<div class="panel-body" ng-if="$ctrl.hidePreview == false">
<div ng-if="!$ctrl.showAsHTML">
<div ng-bind-html="$ctrl.alert.preview"></div>
</div>
<div ng-if="$ctrl.showAsHTML">
<div ng-bind-html="$ctrl.alert.previewHTML"></div>
</div>
</div>
<div class="panel-footer"></div>
</div>
</div>

<div class="form-group" ng-if="$ctrl.canEdit">
<button class="btn btn-primary" ng-disabled="!alertForm.$valid" ng-click="$ctrl.saveChanges()">Save</button>
Expand Down
26 changes: 25 additions & 1 deletion client/app/pages/alert/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { template as templateBuilder } from 'lodash';
import notification from '@/services/notification';
import Modal from 'antd/lib/modal';
import template from './alert.html';
import AlertTemplate from '@/services/alert-template';
import { clientConfig } from '@/services/auth';
import navigateTo from '@/services/navigateTo';

function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Events, Alert) {
function AlertCtrl($scope, $routeParams, $location, $sce, $sanitize, currentUser, Query, Events, Alert) {
this.alertId = $routeParams.alertId;
this.hidePreview = false;
this.alertTemplate = new AlertTemplate();
this.showExtendedOptions = clientConfig.extendedAlertOptions;

if (this.alertId === 'new') {
Events.record('view', 'page', 'alerts/new');
Expand Down Expand Up @@ -62,6 +67,9 @@ function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Ev
if (this.alert.rearm === '' || this.alert.rearm === 0) {
this.alert.rearm = null;
}
if (this.alert.template === undefined || this.alert.template === '') {
this.alert.template = null;
}
this.alert.$save(
(alert) => {
notification.success('Saved.');
Expand All @@ -75,6 +83,22 @@ function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Ev
);
};

this.preview = () => {
const notifyError = () => notification.error('Unable to render description. please confirm your template.');
try {
const result = this.alertTemplate.render(this.alert, this.queryResult.query_result.data);
this.alert.preview = $sce.trustAsHtml(result.escaped);
this.alert.previewHTML = $sce.trustAsHtml($sanitize(result.raw));
if (!result.raw) {
notifyError();
}
} catch (e) {
notifyError();
this.alert.preview = e.message;
this.alert.previewHTML = e.message;
}
};

this.delete = () => {
const doDelete = () => {
this.alert.$delete(() => {
Expand Down
41 changes: 41 additions & 0 deletions client/app/services/alert-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// import { $http } from '@/services/ng';
import Mustache from 'mustache';

export default class AlertTemplate {
render(alert, queryResult) {
const view = {
state: alert.state,
rows: queryResult.rows,
cols: queryResult.columns,
};
const result = Mustache.render(alert.options.template, view);
const escaped = result
.replace(/"/g, '&quot;')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n|\r/g, '<br>');

return { escaped, raw: result };
}

constructor() {
this.helpMessage = `using template engine "mustache".
you can build message with latest query result.
variable name "rows" is assigned as result rows. "cols" as result columns, "state" as alert state.`;

this.editorOptions = {
useWrapMode: true,
showPrintMargin: false,
advanced: {
behavioursEnabled: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
autoScrollEditorIntoView: true,
},
onLoad(editor) {
editor.$blockScrolling = Infinity;
},
};
}
}
10 changes: 7 additions & 3 deletions redash/destinations/chatwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,18 @@ def notify(self, alert, query, user, new_state, app, host, options):

alert_url = '{host}/alerts/{alert_id}'.format(host=host, alert_id=alert.id)
query_url = '{host}/queries/{query_id}'.format(host=host, query_id=query.id)

message_template = options.get('message_template', ChatWork.ALERTS_DEFAULT_MESSAGE_TEMPLATE)

message = message_template.replace('\\n', '\n').format(
message = ''
if alert.custom_subject:
message = alert.custom_subject + '\n'
message += message_template.replace('\\n', '\n').format(
alert_name=alert.name, new_state=new_state.upper(),
alert_url=alert_url,
query_url=query_url)

if alert.template:
description = alert.render_template()
message = message + "\n" + description
headers = {'X-ChatWorkToken': options.get('api_token')}
payload = {'body': message}

Expand Down
15 changes: 12 additions & 3 deletions redash/destinations/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,30 @@ def notify(self, alert, query, user, new_state, app, host, options):
logging.warning("No emails given. Skipping send.")

html = """
Check <a href="{host}/alerts/{alert_id}">alert</a> / check <a href="{host}/queries/{query_id}">query</a>.
Check <a href="{host}/alerts/{alert_id}">alert</a> / check <a href="{host}/queries/{query_id}">query</a> </br>.
""".format(host=host, alert_id=alert.id, query_id=query.id)
if alert.template:
description = alert.render_template()
html += "<br>" + description
logging.debug("Notifying: %s", recipients)

try:
alert_name = alert.name.encode('utf-8', 'ignore')
state = new_state.upper()
subject_template = options.get('subject_template', settings.ALERTS_DEFAULT_MAIL_SUBJECT_TEMPLATE)
if alert.custom_subject:
subject = alert.custom_subject
else:
subject_template = options.get('subject_template', settings.ALERTS_DEFAULT_MAIL_SUBJECT_TEMPLATE)
subject = subject_template.format(alert_name=alert_name, state=state)

message = Message(
recipients=recipients,
subject=subject_template.format(alert_name=alert_name, state=state),
subject=subject,
html=html
)
mail.send(message)
except Exception:
logging.exception("Mail send error.")


register(Email)
18 changes: 17 additions & 1 deletion redash/destinations/hangoutschat.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,16 @@ def notify(self, alert, query, user, new_state, app, host, options):
else:
message = "Unable to determine status. Check Query and Alert configuration."

if alert.custom_subject:
title = alert.custom_subject
else:
title = alert.name

data = {
"cards": [
{
"header": {
"title": alert.name
"title": title
},
"sections": [
{
Expand All @@ -65,6 +70,17 @@ def notify(self, alert, query, user, new_state, app, host, options):
]
}

if alert.template:
data["cards"][0]["sections"].append({
"widgets": [
{
"textParagraph": {
"text": alert.render_template()
}
}
]
})

if options.get("icon_url"):
data["cards"][0]["header"]["imageUrl"] = options.get("icon_url")

Expand Down
9 changes: 9 additions & 0 deletions redash/destinations/mattermost.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ def notify(self, alert, query, user, new_state, app, host, options):
else:
text = "####" + alert.name + " went back to normal"

if alert.custom_subject:
text += '\n' + alert.custom_subject
payload = {'text': text}

if alert.template:
payload['attachments'] = [{'fields': [{
"title": "Description",
"value": alert.render_template()
}]}]

if options.get('username'): payload['username'] = options.get('username')
if options.get('icon_url'): payload['icon_url'] = options.get('icon_url')
if options.get('channel'): payload['channel'] = options.get('channel')
Expand Down
7 changes: 6 additions & 1 deletion redash/destinations/pagerduty.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def notify(self, alert, query, user, new_state, app, host, options):

default_desc = self.DESCRIPTION_STR.format(query_id=query.id, query_name=query.name)

if options.get('description'):
if alert.custom_subject:
default_desc = alert.custom_subject
elif options.get('description'):
default_desc = options.get('description')

incident_key = self.KEY_STRING.format(alert_id=alert.id, query_id=query.id)
Expand All @@ -58,6 +60,9 @@ def notify(self, alert, query, user, new_state, app, host, options):
}
}

if alert.template:
data['payload']['custom_details'] = alert.render_template()

if new_state == 'triggered':
data['event_action'] = 'trigger'
elif new_state == "unknown":
Expand Down
11 changes: 10 additions & 1 deletion redash/destinations/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,17 @@ def notify(self, alert, query, user, new_state, app, host, options):
"short": True
}
]
if alert.template:
description = alert.render_template()
fields.append({
"title": "Description",
"value": description
})
if new_state == "triggered":
text = alert.name + " just triggered"
if alert.custom_subject:
text = alert.custom_subject
else:
text = alert.name + " just triggered"
color = "#c0392b"
else:
text = alert.name + " went back to normal"
Expand Down
6 changes: 5 additions & 1 deletion redash/destinations/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ def notify(self, alert, query, user, new_state, app, host, options):
data = {
'event': 'alert_state_change',
'alert': serialize_alert(alert, full=False),
'url_base': host
'url_base': host,
}

data['alert']['description'] = alert.render_template()
data['alert']['title'] = alert.custom_subject

headers = {'Content-Type': 'application/json'}
auth = HTTPBasicAuth(options.get('username'), options.get('password')) if options.get('username') else None
resp = requests.post(options.get('url'), data=json_dumps(data), auth=auth, headers=headers, timeout=5.0)
Expand Down
3 changes: 2 additions & 1 deletion redash/handlers/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_fields)
from redash.permissions import (require_access, require_admin_or_owner,
require_permission, view_only)
from redash.utils import json_dumps


class AlertResource(BaseResource):
Expand Down Expand Up @@ -60,7 +61,7 @@ def post(self):
query_rel=query,
user=self.current_user,
rearm=req.get('rearm'),
options=req['options']
options=req['options'],
)

models.db.session.add(alert)
Expand Down
1 change: 1 addition & 0 deletions redash/handlers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def client_config():
'showPermissionsControl': current_org.get_setting("feature_show_permissions_control"),
'allowCustomJSVisualizations': settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,
'autoPublishNamedQueries': settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,
'extendedAlertOptions': settings.FEATURE_EXTENDED_ALERT_OPTIONS,
'mailSettingsMissing': not settings.email_server_is_configured(),
'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS,
'queryRefreshIntervals': settings.QUERY_REFRESH_INTERVALS,
Expand Down
17 changes: 16 additions & 1 deletion redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from redash.metrics import database # noqa: F401
from redash.query_runner import (get_configuration_schema_for_query_runner_type,
get_query_runner, TYPE_BOOLEAN, TYPE_DATE, TYPE_DATETIME)
from redash.utils import generate_token, json_dumps, json_loads
from redash.utils import generate_token, json_dumps, json_loads, mustache_render
from redash.utils.configuration import ConfigurationContainer
from redash.models.parameterized_query import ParameterizedQuery

Expand Down Expand Up @@ -796,6 +796,21 @@ def evaluate(self):
def subscribers(self):
return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self)

def render_template(self):
if not self.template:
return ''
data = json_loads(self.query_rel.latest_query_data.data)
context = {'rows': data['rows'], 'cols': data['columns'], 'state': self.state}
return mustache_render(self.template, context)

@property
def template(self):
return self.options.get('template', '')

@property
def custom_subject(self):
return self.options.get('subject', '')

@property
def groups(self):
return self.query_rel.groups
Expand Down
1 change: 1 addition & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ def email_server_is_configured():
FEATURE_SHOW_QUERY_RESULTS_COUNT = parse_boolean(os.environ.get("REDASH_FEATURE_SHOW_QUERY_RESULTS_COUNT", "true"))
FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS = parse_boolean(os.environ.get("REDASH_FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS", "false"))
FEATURE_AUTO_PUBLISH_NAMED_QUERIES = parse_boolean(os.environ.get("REDASH_FEATURE_AUTO_PUBLISH_NAMED_QUERIES", "true"))
FEATURE_EXTENDED_ALERT_OPTIONS = parse_boolean(os.environ.get("REDASH_FEATURE_EXTENDED_ALERT_OPTIONS", "false"))

# BigQuery
BIGQUERY_HTTP_TIMEOUT = int(os.environ.get("REDASH_BIGQUERY_HTTP_TIMEOUT", "600"))
Expand Down
1 change: 1 addition & 0 deletions redash/tasks/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def check_alerts_for_query(query_id):
query = models.Query.query.get(query_id)

for alert in query.alerts:
logger.info("Checking alert (%d) of query %d.", alert.id, query_id)
new_state = alert.evaluate()

if should_notify(alert, new_state):
Expand Down

0 comments on commit f12b412

Please sign in to comment.