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:
parent
30506ef41d
commit
f90d600a22
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue