Add webhook action

This action can be added to an alert as :
      "webhook" : {
        "url" : "http://localhost:8080/alert/{{alert_name}}",
        "method" : "GET"
      }

Original commit: elastic/x-pack-elasticsearch@34e04229ab
This commit is contained in:
Brian Murphy 2015-01-15 09:31:23 -05:00
parent 30506ef41d
commit f90d600a22
5 changed files with 326 additions and 1 deletions

View File

@ -15,6 +15,7 @@ import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ScriptService;
import java.io.IOException;
import java.util.ArrayList;
@ -25,11 +26,13 @@ public class AlertActionRegistry extends AbstractComponent {
private volatile ImmutableOpenMap<String, AlertActionFactory> actionImplemented;
@Inject
public AlertActionRegistry(Settings settings, Client client, ConfigurationManager configurationManager) {
public AlertActionRegistry(Settings settings, Client client, ConfigurationManager configurationManager,
ScriptService scriptService) {
super(settings);
actionImplemented = ImmutableOpenMap.<String, AlertActionFactory>builder()
.fPut("email", new SmtpAlertActionFactory(configurationManager))
.fPut("index", new IndexAlertActionFactory(client, configurationManager))
.fPut("webhook", new WebhookAlertActionFactory(scriptService))
.build();
}

View File

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.actions;
import org.elasticsearch.common.netty.handler.codec.http.HttpMethod;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
/**
*/
public class WebhookAlertAction implements AlertAction {
private final String url;
private final HttpMethod method;
private final String parameterString;
public WebhookAlertAction(String url, HttpMethod method, String parameterString) {
this.url = url;
this.method = method;
this.parameterString = parameterString;
}
public String getUrl() {
return url;
}
public HttpMethod getMethod() {
return method;
}
public String getParameterString() {
return parameterString;
}
/**
*/
@Override
public String getActionName() {
return "webhook";
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field("url", url);
builder.field("method", method.getName());
builder.field("parameter_string", parameterString);
builder.endObject();
return builder;
}
@Override
public String toString() {
return "WebhookAlertAction{" +
"url='" + url + '\'' +
", method=" + method +
", parameterString='" + parameterString + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebhookAlertAction that = (WebhookAlertAction) o;
if (method != null ? !method.equals(that.method) : that.method != null) return false;
if (parameterString != null ? !parameterString.equals(that.parameterString) : that.parameterString != null)
return false;
if (url != null ? !url.equals(that.url) : that.url != null) return false;
return true;
}
@Override
public int hashCode() {
int result = url != null ? url.hashCode() : 0;
result = 31 * result + (method != null ? method.hashCode() : 0);
result = 31 * result + (parameterString != null ? parameterString.hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.actions;
import org.apache.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.ElasticsearchIllegalStateException;
import org.elasticsearch.alerts.Alert;
import org.elasticsearch.alerts.triggers.TriggerResult;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.netty.handler.codec.http.HttpMethod;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* This action factory will call back a web hook using a templatized url
* If the method is PUT or POST the request,response and alert name will be sent as well
*/
public class WebhookAlertActionFactory implements AlertActionFactory {
private static String ALERT_NAME = "alert_name";
private static String RESPONSE = "response";
private static String REQUEST = "request";
static String DEFAULT_PARAMETER_STRING = "alertname={{alert_name}}&request=%{{request}}&response=%{{response}}";
private final ScriptService scriptService;
private final Logger logger = Logger.getLogger(WebhookAlertActionFactory.class);
public WebhookAlertActionFactory(ScriptService scriptService) {
this.scriptService = scriptService;
}
@Override
public AlertAction createAction(XContentParser parser) throws IOException {
String url = null;
HttpMethod method = HttpMethod.POST;
String parameterTemplate = DEFAULT_PARAMETER_STRING;
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
switch (currentFieldName) {
case "url":
url = parser.text();
break;
case "method":
method = HttpMethod.valueOf(parser.text());
if (method != HttpMethod.POST && method != HttpMethod.GET && method != HttpMethod.PUT) {
throw new ElasticsearchIllegalArgumentException("Unsupported http method ["
+ method.getName() + "]");
}
break;
case "parameter_string":
parameterTemplate = parser.text();
break;
default:
throw new ElasticsearchIllegalArgumentException("Unexpected field [" + currentFieldName + "]");
}
} else {
throw new ElasticsearchIllegalArgumentException("Unexpected token [" + token + "]");
}
}
return new WebhookAlertAction(url, method, parameterTemplate);
}
@Override
public boolean doAction(AlertAction action, Alert alert, TriggerResult result) {
if (!(action instanceof WebhookAlertAction)) {
throw new ElasticsearchIllegalStateException("Bad action [" + action.getClass() + "] passed to WebhookAlertActionFactory expected [" + WebhookAlertAction.class + "]");
}
WebhookAlertAction webhookAlertAction = (WebhookAlertAction)action;
String url = webhookAlertAction.getUrl();
String renderedUrl = renderUrl(url, alert, result, scriptService);
HttpMethod method = webhookAlertAction.getMethod();
try {
URL urlToOpen = new URL(renderedUrl);
HttpURLConnection httpConnection = (HttpURLConnection) urlToOpen.openConnection();
httpConnection.setRequestMethod(method.getName());
httpConnection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.name());
if (method == HttpMethod.POST || method == HttpMethod.PUT) {
httpConnection.setDoOutput(true);
httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset="
+ StandardCharsets.UTF_8.name());
String parameters = encodeParameterString(webhookAlertAction.getParameterString(), alert, result, scriptService);
httpConnection.getOutputStream().write(parameters.getBytes(StandardCharsets.UTF_8.name()));
}
int status = httpConnection.getResponseCode();
if (status >= 400) {
throw new ElasticsearchException("Got status [" + status + "] when connecting to [" + renderedUrl + "]");
} else {
if (status >= 300) {
logger.warn("A 200 range return code was expected, but got [" + status + "]");
}
return true;
}
} catch (IOException ioe) {
throw new ElasticsearchException("Unable to connect to [" + renderedUrl + "]", ioe);
}
}
static String encodeParameterString(String parameterString, Alert alert, TriggerResult result, ScriptService scriptService) throws IOException {
XContentBuilder responseBuilder = XContentFactory.jsonBuilder();
responseBuilder.startObject();
responseBuilder.field("response",result.getTriggerResponse());
responseBuilder.endObject();
String requestJSON = XContentHelper.convertToJson(result.getTriggerRequest().source(), true);
String responseJSON = XContentHelper.convertToJson(responseBuilder.bytes(), true);
Map<String, Object> templateParams = new HashMap<>();
templateParams.put(ALERT_NAME, URLEncoder.encode(alert.getAlertName(), StandardCharsets.UTF_8.name()));
templateParams.put(RESPONSE, URLEncoder.encode(responseJSON, StandardCharsets.UTF_8.name()));
templateParams.put(REQUEST, URLEncoder.encode(requestJSON, StandardCharsets.UTF_8.name()));
ExecutableScript script = scriptService.executable("mustache", parameterString, ScriptService.ScriptType.INLINE, templateParams);
return ((BytesReference) script.run()).toUtf8();
}
public String renderUrl(String url, Alert alert, TriggerResult result, ScriptService scriptService) {
Map<String, Object> templateParams = new HashMap<>();
templateParams.put(ALERT_NAME, alert.getAlertName());
templateParams.put(RESPONSE, result.getActionResponse());
ExecutableScript script = scriptService.executable("mustache", url, ScriptService.ScriptType.INLINE, templateParams);
return ((BytesReference) script.run()).toUtf8();
}
}

View File

@ -8,8 +8,10 @@ package org.elasticsearch.alerts;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.alerts.actions.AlertAction;
import org.elasticsearch.alerts.actions.SmtpAlertAction;
import org.elasticsearch.alerts.actions.WebhookAlertAction;
import org.elasticsearch.alerts.triggers.ScriptedTrigger;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.netty.handler.codec.http.HttpMethod;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -35,6 +37,7 @@ public class AlertSerializationTest extends AbstractAlertingTests {
List<AlertAction> actions = new ArrayList<>();
actions.add(new SmtpAlertAction("message", "foo@bar.com"));
actions.add(new WebhookAlertAction("http://localhost/foobarbaz/{{alert_name}}", HttpMethod.GET, ""));
Alert alert = new Alert("test-serialization",
triggerRequest,
new ScriptedTrigger("return true", ScriptService.ScriptType.INLINE, "groovy"),

View File

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.actions;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.alerts.Alert;
import org.elasticsearch.alerts.AlertAckState;
import org.elasticsearch.alerts.triggers.AlertTrigger;
import org.elasticsearch.alerts.triggers.ScriptedTrigger;
import org.elasticsearch.alerts.triggers.TriggerResult;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.script.ScriptEngineService;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.mustache.MustacheScriptEngineService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import java.util.*;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
/**
*/
public class WebhookTest extends ElasticsearchTestCase {
public void testRequestParameterSerialization() throws Exception {
SearchRequest triggerRequest = (new SearchRequest()).source(searchSource().query(matchAllQuery()));
AlertTrigger trigger = new ScriptedTrigger("return true", ScriptService.ScriptType.INLINE, "groovy");
List<AlertAction> actions = new ArrayList<>();
Alert alert = new Alert("test-email-template",
triggerRequest,
trigger,
actions,
"0/5 * * * * ? *",
new DateTime(),
0,
new TimeValue(0),
AlertAckState.NOT_TRIGGERED);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("hits",0);
Settings settings = ImmutableSettings.settingsBuilder().build();
MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings);
ThreadPool tp;
tp = new ThreadPool(ThreadPool.Names.SAME);
Set<ScriptEngineService> engineServiceSet = new HashSet<>();
engineServiceSet.add(mustacheScriptEngineService);
ScriptService scriptService = new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, tp));
String encodedRequestParameters = WebhookAlertActionFactory.encodeParameterString(WebhookAlertActionFactory.DEFAULT_PARAMETER_STRING, alert,
new TriggerResult(true, triggerRequest, responseMap, trigger), scriptService);
assertEquals("alertname=test-email-template&request=%%7B%22query%22%3A%7B%22match_all%22%3A%7B%7D%7D%7D&response=%%7B%22response%22%3A%7B%22hits%22%3A0%7D%7D", encodedRequestParameters);
tp.shutdownNow();
}
}