diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html
index 1091e993ba..cf8c86f474 100644
--- a/client/app/pages/alert/alert.html
+++ b/client/app/pages/alert/alert.html
@@ -47,6 +47,39 @@
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js
index 486629563c..1296468222 100644
--- a/client/app/pages/alert/index.js
+++ b/client/app/pages/alert/index.js
@@ -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');
@@ -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.');
@@ -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(() => {
diff --git a/client/app/services/alert-template.js b/client/app/services/alert-template.js
new file mode 100644
index 0000000000..74da808326
--- /dev/null
+++ b/client/app/services/alert-template.js
@@ -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, '"')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/\n|\r/g, '
');
+
+ 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;
+ },
+ };
+ }
+}
diff --git a/redash/destinations/chatwork.py b/redash/destinations/chatwork.py
index 6d794b13ed..c4751f25d8 100644
--- a/redash/destinations/chatwork.py
+++ b/redash/destinations/chatwork.py
@@ -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}
diff --git a/redash/destinations/email.py b/redash/destinations/email.py
index b66c9e9484..537ec6bc2b 100644
--- a/redash/destinations/email.py
+++ b/redash/destinations/email.py
@@ -35,21 +35,30 @@ def notify(self, alert, query, user, new_state, app, host, options):
logging.warning("No emails given. Skipping send.")
html = """
- Check
alert / check
query.
+ Check
alert / check
query .
""".format(host=host, alert_id=alert.id, query_id=query.id)
+ if alert.template:
+ description = alert.render_template()
+ html += "
" + 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)
diff --git a/redash/destinations/hangoutschat.py b/redash/destinations/hangoutschat.py
index 28951b6193..5db48e9cf7 100644
--- a/redash/destinations/hangoutschat.py
+++ b/redash/destinations/hangoutschat.py
@@ -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": [
{
@@ -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")
diff --git a/redash/destinations/mattermost.py b/redash/destinations/mattermost.py
index ea0280954d..ca9e4ea161 100644
--- a/redash/destinations/mattermost.py
+++ b/redash/destinations/mattermost.py
@@ -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')
diff --git a/redash/destinations/pagerduty.py b/redash/destinations/pagerduty.py
index c0411a04ff..737d801e57 100644
--- a/redash/destinations/pagerduty.py
+++ b/redash/destinations/pagerduty.py
@@ -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)
@@ -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":
diff --git a/redash/destinations/slack.py b/redash/destinations/slack.py
index e9e90ebf9c..9cbcfda2c5 100644
--- a/redash/destinations/slack.py
+++ b/redash/destinations/slack.py
@@ -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"
diff --git a/redash/destinations/webhook.py b/redash/destinations/webhook.py
index 5b572db080..eb0cd06e0b 100644
--- a/redash/destinations/webhook.py
+++ b/redash/destinations/webhook.py
@@ -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)
diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py
index 8dd3ec1c8e..74c47c0d48 100644
--- a/redash/handlers/alerts.py
+++ b/redash/handlers/alerts.py
@@ -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):
@@ -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)
diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py
index 2ace2d3e26..461e033e5a 100644
--- a/redash/handlers/authentication.py
+++ b/redash/handlers/authentication.py
@@ -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,
diff --git a/redash/models/__init__.py b/redash/models/__init__.py
index 25f24180d9..5cecdbc530 100644
--- a/redash/models/__init__.py
+++ b/redash/models/__init__.py
@@ -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
@@ -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
diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py
index c7b57ff85c..3955a26d01 100644
--- a/redash/settings/__init__.py
+++ b/redash/settings/__init__.py
@@ -333,6 +333,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"))
diff --git a/redash/tasks/alerts.py b/redash/tasks/alerts.py
index 7a239cf6e4..2346b9dee1 100644
--- a/redash/tasks/alerts.py
+++ b/redash/tasks/alerts.py
@@ -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):