[template] formalized the template definition

- Introduced `Template` & `Template.Parser` interfaces
- There main template implementation is the `ScriptTemplate` and its parser is bound to `Template.Parser`
- There are also xContent templates - YAML & JSON that just render the model as xContent. (used as a fallback in webhook action)
- updated all actions to use the new template infrastructure

Also

- introduced mockito for unit testing
- removed `WebhookTest` as it was effectively testing the template functionality... we'll add a proper test for teh webhook action in a later commit

Original commit: elastic/x-pack-elasticsearch@34a90e8c2f
This commit is contained in:
uboness 2015-02-21 01:49:41 +01:00
parent 48bf4e8a8b
commit 2136210711
23 changed files with 797 additions and 445 deletions

14
pom.xml
View File

@ -86,6 +86,20 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<dependency> <!--required for mockito-->
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>
<!-- Remove this when LocalDiscovery gets fixed in core -->
<dependency>
<groupId>com.google.guava</groupId>

View File

@ -13,6 +13,7 @@ import org.elasticsearch.alerts.rest.AlertsRestModule;
import org.elasticsearch.alerts.scheduler.SchedulerModule;
import org.elasticsearch.alerts.support.TemplateUtils;
import org.elasticsearch.alerts.support.init.InitializingModule;
import org.elasticsearch.alerts.support.template.TemplateModule;
import org.elasticsearch.alerts.transform.TransformModule;
import org.elasticsearch.alerts.transport.AlertsTransportModule;
import org.elasticsearch.alerts.condition.ConditionModule;
@ -28,6 +29,7 @@ public class AlertsModule extends AbstractModule implements SpawnModules {
public Iterable<? extends Module> spawnModules() {
return ImmutableList.of(
new InitializingModule(),
new TemplateModule(),
new AlertsClientModule(),
new TransformModule(),
new AlertsRestModule(),

View File

@ -8,6 +8,7 @@ package org.elasticsearch.alerts.actions;
import org.elasticsearch.alerts.ExecutionContext;
import org.elasticsearch.alerts.Payload;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -19,8 +20,8 @@ import java.io.IOException;
*/
public abstract class Action<R extends Action.Result> implements ToXContent {
public static final String ALERT_NAME_VARIABLE_NAME = "alert_name";
public static final String RESPONSE_VARIABLE_NAME = "response";
public static final String ALERT_NAME_VARIABLE = "alert_name";
public static final String PAYLOAD_VARIABLE = "payload";
protected final ESLogger logger;
@ -38,7 +39,12 @@ public abstract class Action<R extends Action.Result> implements ToXContent {
*/
public abstract R execute(ExecutionContext context, Payload payload) throws IOException;
protected static ImmutableMap<String, Object> templateModel(ExecutionContext ctx, Payload payload) {
return ImmutableMap.<String, Object>builder()
.put(ALERT_NAME_VARIABLE, ctx.alert().name())
.put(PAYLOAD_VARIABLE, payload.data())
.build();
}
/**
* Parses xcontent to a concrete action of the same type.
*/
@ -57,8 +63,6 @@ public abstract class Action<R extends Action.Result> implements ToXContent {
R parseResult(XContentParser parser) throws IOException;
}
public static abstract class Result implements ToXContent {
public static final ParseField SUCCESS_FIELD = new ParseField("success");

View File

@ -0,0 +1,20 @@
/*
* 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;
/**
*
*/
public class ActionSettingsException extends ActionException {
public ActionSettingsException(String msg) {
super(msg);
}
public ActionSettingsException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@ -8,9 +8,11 @@ package org.elasticsearch.alerts.actions.email;
import org.elasticsearch.alerts.ExecutionContext;
import org.elasticsearch.alerts.Payload;
import org.elasticsearch.alerts.actions.Action;
import org.elasticsearch.alerts.actions.ActionSettingsException;
import org.elasticsearch.alerts.actions.email.service.*;
import org.elasticsearch.alerts.support.StringTemplateUtils;
import org.elasticsearch.alerts.support.template.Template;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.ESLogger;
@ -20,8 +22,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*/
@ -33,21 +33,18 @@ public class EmailAction extends Action<EmailAction.Result> {
private final Authentication auth;
private final Profile profile;
private final String account;
private final StringTemplateUtils.Template subject;
private final StringTemplateUtils.Template textBody;
private final StringTemplateUtils.Template htmlBody;
private final Template subject;
private final Template textBody;
private final Template htmlBody;
private final boolean attachPayload;
private final EmailService emailService;
private final StringTemplateUtils templateUtils;
public EmailAction(ESLogger logger, EmailService emailService, StringTemplateUtils templateUtils,
Email.Builder email, Authentication auth, Profile profile, String account,
StringTemplateUtils.Template subject, StringTemplateUtils.Template textBody,
StringTemplateUtils.Template htmlBody, boolean attachPayload) {
public EmailAction(ESLogger logger, EmailService emailService, Email.Builder email, Authentication auth, Profile profile,
String account, Template subject, Template textBody, Template htmlBody, boolean attachPayload) {
super(logger);
this.emailService = emailService;
this.templateUtils = templateUtils;
this.email = email;
this.auth = auth;
this.profile = profile;
@ -65,21 +62,14 @@ public class EmailAction extends Action<EmailAction.Result> {
@Override
public Result execute(ExecutionContext ctx, Payload payload) throws IOException {
ImmutableMap<String, Object> model = templateModel(ctx, payload);
email.id(ctx.id());
Map<String, Object> alertParams = new HashMap<>();
alertParams.put(Action.ALERT_NAME_VARIABLE_NAME, ctx.alert().name());
alertParams.put(RESPONSE_VARIABLE_NAME, payload.data());
String text = templateUtils.executeTemplate(subject, alertParams);
email.subject(text);
text = templateUtils.executeTemplate(textBody, alertParams);
email.textBody(text);
email.subject(subject.render(model));
email.textBody(textBody.render(model));
if (htmlBody != null) {
text = templateUtils.executeTemplate(htmlBody, alertParams);
email.htmlBody(text);
email.htmlBody(htmlBody.render(model));
}
if (attachPayload) {
@ -100,17 +90,17 @@ public class EmailAction extends Action<EmailAction.Result> {
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (account != null) {
builder.field(Parser.ACCOUNT_FIELD.getPreferredName(), account);
builder.field(EmailAction.Parser.ACCOUNT_FIELD.getPreferredName(), account);
}
if (profile != null) {
builder.field(Parser.PROFILE_FIELD.getPreferredName(), profile);
builder.field(EmailAction.Parser.PROFILE_FIELD.getPreferredName(), profile);
}
builder.field(Email.TO_FIELD.getPreferredName(), (ToXContent) email.to());
if (subject != null) {
StringTemplateUtils.writeTemplate(Email.SUBJECT_FIELD.getPreferredName(), subject, builder, params);
builder.field(Email.SUBJECT_FIELD.getPreferredName(), subject);
}
if (textBody != null) {
StringTemplateUtils.writeTemplate(Email.TEXT_BODY_FIELD.getPreferredName(), textBody, builder, params);
builder.field(Email.TEXT_BODY_FIELD.getPreferredName(), textBody);
}
return builder.endObject();
}
@ -125,14 +115,14 @@ public class EmailAction extends Action<EmailAction.Result> {
public static final ParseField EMAIL_FIELD = new ParseField("email");
public static final ParseField REASON_FIELD = new ParseField("reason");
private final StringTemplateUtils templateUtils;
private final Template.Parser templateParser;
private final EmailService emailService;
@Inject
public Parser(Settings settings, EmailService emailService, StringTemplateUtils templateUtils) {
public Parser(Settings settings, EmailService emailService, Template.Parser templateParser) {
super(settings);
this.emailService = emailService;
this.templateUtils = templateUtils;
this.templateParser = templateParser;
}
@Override
@ -147,9 +137,9 @@ public class EmailAction extends Action<EmailAction.Result> {
String account = null;
Profile profile = null;
Email.Builder email = Email.builder();
StringTemplateUtils.Template subject = null;
StringTemplateUtils.Template textBody = null;
StringTemplateUtils.Template htmlBody = null;
Template subject = null;
Template textBody = null;
Template htmlBody = null;
boolean attachPayload = false;
String currentFieldName = null;
@ -169,15 +159,27 @@ public class EmailAction extends Action<EmailAction.Result> {
} else if (Email.BCC_FIELD.match(currentFieldName)) {
email.bcc(Email.AddressList.parse(currentFieldName, token, parser));
} else if (Email.SUBJECT_FIELD.match(currentFieldName)) {
subject = StringTemplateUtils.readTemplate(parser);
try {
subject = templateParser.parse(parser);
} catch (Template.Parser.ParseException pe) {
throw new ActionSettingsException("could not parse email [subject] template", pe);
}
} else if (Email.TEXT_BODY_FIELD.match(currentFieldName)) {
textBody = StringTemplateUtils.readTemplate(parser);
try {
textBody = templateParser.parse(parser);
} catch (Template.Parser.ParseException pe) {
throw new ActionSettingsException("could not parse email [text_body] template", pe);
}
} else if (Email.HTML_BODY_FIELD.match(currentFieldName)) {
try {
htmlBody = templateParser.parse(parser);
} catch (Template.Parser.ParseException pe) {
throw new ActionSettingsException("could not parse email [html_body] template", pe);
}
} else if (token == XContentParser.Token.VALUE_STRING) {
if (Email.PRIORITY_FIELD.match(currentFieldName)) {
email.priority(Email.Priority.resolve(parser.text()));
} else if (Email.HTML_BODY_FIELD.match(currentFieldName)) {
htmlBody = StringTemplateUtils.readTemplate(parser);
} else if (ACCOUNT_FIELD.match(currentFieldName)) {
} else if (ACCOUNT_FIELD.match(currentFieldName)) {
account = parser.text();
} else if (USER_FIELD.match(currentFieldName)) {
user = parser.text();
@ -186,26 +188,26 @@ public class EmailAction extends Action<EmailAction.Result> {
} else if (PROFILE_FIELD.match(currentFieldName)) {
profile = Profile.resolve(parser.text());
} else {
throw new EmailException("could not parse email action. unrecognized string field [" + currentFieldName + "]");
throw new ActionSettingsException("could not parse email action. unrecognized string field [" + currentFieldName + "]");
}
} else if (token == XContentParser.Token.VALUE_BOOLEAN) {
if (ATTACH_PAYLOAD_FIELD.match(currentFieldName)) {
attachPayload = parser.booleanValue();
} else {
throw new EmailException("could not parse email action. unrecognized boolean field [" + currentFieldName + "]");
throw new ActionSettingsException("could not parse email action. unrecognized boolean field [" + currentFieldName + "]");
}
} else {
throw new EmailException("could not parse email action. unexpected token [" + token + "]");
throw new ActionSettingsException("could not parse email action. unexpected token [" + token + "]");
}
}
}
if (email.to() == null || email.to().isEmpty()) {
throw new EmailException("could not parse email action. [to] was not found or was empty");
throw new ActionSettingsException("could not parse email action. [to] was not found or was empty");
}
Authentication auth = new Authentication(user, password);
return new EmailAction(logger, emailService, templateUtils, email, auth, profile, account, subject, textBody, htmlBody, attachPayload);
return new EmailAction(logger, emailService, email, auth, profile, account, subject, textBody, htmlBody, attachPayload);
}
@Override

View File

@ -12,6 +12,7 @@ import org.elasticsearch.alerts.ExecutionContext;
import org.elasticsearch.alerts.Payload;
import org.elasticsearch.alerts.actions.Action;
import org.elasticsearch.alerts.actions.ActionException;
import org.elasticsearch.alerts.actions.ActionSettingsException;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.component.AbstractComponent;
@ -90,6 +91,26 @@ public class IndexAction extends Action<IndexAction.Result> {
return builder;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IndexAction that = (IndexAction) o;
if (index != null ? !index.equals(that.index) : that.index != null) return false;
if (type != null ? !type.equals(that.type) : that.type != null) return false;
return true;
}
@Override
public int hashCode() {
int result = index != null ? index.hashCode() : 0;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
public static class Parser extends AbstractComponent implements Action.Parser<Result, IndexAction> {
public static final ParseField INDEX_FIELD = new ParseField("index");
@ -127,19 +148,19 @@ public class IndexAction extends Action<IndexAction.Result> {
} else if (TYPE_FIELD.match(currentFieldName)) {
type = parser.text();
} else {
throw new ActionException("could not parse index action. unexpected field [" + currentFieldName + "]");
throw new ActionSettingsException("could not parse index action. unexpected field [" + currentFieldName + "]");
}
} else {
throw new ActionException("could not parse index action. unexpected token [" + token + "]");
throw new ActionSettingsException("could not parse index action. unexpected token [" + token + "]");
}
}
if (index == null) {
throw new ActionException("could not parse index action [index] is required");
throw new ActionSettingsException("could not parse index action [index] is required");
}
if (type == null) {
throw new ActionException("could not parse index action [type] is required");
throw new ActionSettingsException("could not parse index action [type] is required");
}
return new IndexAction(logger, client, index, type);
@ -215,24 +236,4 @@ public class IndexAction extends Action<IndexAction.Result> {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IndexAction that = (IndexAction) o;
if (index != null ? !index.equals(that.index) : that.index != null) return false;
if (type != null ? !type.equals(that.type) : that.type != null) return false;
return true;
}
@Override
public int hashCode() {
int result = index != null ? index.hashCode() : 0;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
}

View File

@ -5,13 +5,17 @@
*/
package org.elasticsearch.alerts.actions.webhook;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.ExecutionContext;
import org.elasticsearch.alerts.Payload;
import org.elasticsearch.alerts.actions.Action;
import org.elasticsearch.alerts.actions.ActionException;
import org.elasticsearch.alerts.support.StringTemplateUtils;
import org.elasticsearch.alerts.actions.ActionSettingsException;
import org.elasticsearch.alerts.support.template.Template;
import org.elasticsearch.alerts.support.template.XContentTemplate;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.ESLogger;
@ -21,8 +25,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*/
@ -30,27 +32,18 @@ public class WebhookAction extends Action<WebhookAction.Result> {
public static final String TYPE = "webhook";
static final StringTemplateUtils.Template DEFAULT_BODY_TEMPLATE = new StringTemplateUtils.Template(
"{ 'alertname' : '{{alert_name}}', 'response' : {{response}} }");
private final StringTemplateUtils templateUtils;
private final HttpClient httpClient;
private final StringTemplateUtils.Template urlTemplate;
private final HttpMethod method;
private final Template url;
private final @Nullable Template body;
//Optional, default will be used if not provided
private final StringTemplateUtils.Template bodyTemplate;
public WebhookAction(ESLogger logger, StringTemplateUtils templateUtils, HttpClient httpClient,
@Nullable StringTemplateUtils.Template bodyTemplate,
StringTemplateUtils.Template urlTemplate, HttpMethod method) {
public WebhookAction(ESLogger logger, HttpClient httpClient, HttpMethod method, Template url, Template body) {
super(logger);
this.templateUtils = templateUtils;
this.httpClient = httpClient;
this.bodyTemplate = bodyTemplate;
this.urlTemplate = urlTemplate;
this.method = method;
this.url = url;
this.body = body;
}
@Override
@ -60,44 +53,62 @@ public class WebhookAction extends Action<WebhookAction.Result> {
@Override
public Result execute(ExecutionContext ctx, Payload payload) throws IOException {
Map<String, Object> data = payload.data();
String renderedUrl = applyTemplate(templateUtils, urlTemplate, ctx.alert().name(), data);
String body = applyTemplate(templateUtils, bodyTemplate != null ? bodyTemplate : DEFAULT_BODY_TEMPLATE, ctx.alert().name(), data);
ImmutableMap<String, Object> model = ImmutableMap.<String, Object>builder()
.put(ALERT_NAME_VARIABLE, ctx.alert().name())
.put(PAYLOAD_VARIABLE, payload.data())
.build();
String urlText = url.render(model);
String bodyText = body != null ? body.render(model) : XContentTemplate.YAML.render(model);
try {
int status = httpClient.execute(method, renderedUrl, body);
int status = httpClient.execute(method, urlText, bodyText);
if (status >= 400) {
logger.warn("got status [" + status + "] when connecting to [" + renderedUrl + "]");
logger.warn("got status [" + status + "] when connecting to [" + urlText + "]");
} else {
if (status >= 300) {
logger.warn("a 200 range return code was expected, but got [" + status + "]");
}
}
return new Result.Executed(status, renderedUrl, body);
return new Result.Executed(status, urlText, bodyText);
} catch (IOException ioe) {
logger.error("failed to connect to [{}] for alert [{}]", ioe, renderedUrl, ctx.alert().name());
logger.error("failed to connect to [{}] for alert [{}]", ioe, urlText, ctx.alert().name());
return new Result.Failure("failed to send http request. " + ioe.getMessage());
}
}
static String applyTemplate(StringTemplateUtils templateUtils, StringTemplateUtils.Template template, String alertName, Map<String, Object> data) {
Map<String, Object> webHookParams = new HashMap<>();
webHookParams.put(ALERT_NAME_VARIABLE_NAME, alertName);
webHookParams.put(RESPONSE_VARIABLE_NAME, data);
return templateUtils.executeTemplate(template, webHookParams);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(Parser.METHOD_FIELD.getPreferredName(), method.getName());
if (bodyTemplate != null) {
StringTemplateUtils.writeTemplate(Parser.BODY_TEMPLATE_FIELD.getPreferredName(), bodyTemplate, builder, params);
builder.field(Parser.URL_FIELD.getPreferredName(), url);
if (body != null) {
builder.field(Parser.BODY_FIELD.getPreferredName(), body);
}
StringTemplateUtils.writeTemplate(Parser.URL_TEMPLATE_FIELD.getPreferredName(), urlTemplate, builder, params);
builder.endObject();
return builder;
return builder.endObject();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebhookAction that = (WebhookAction) o;
if (!body.equals(that.body)) return false;
if (!method.equals(that.method)) return false;
if (!url.equals(that.url)) return false;
return true;
}
@Override
public int hashCode() {
int result = method.hashCode();
result = 31 * result + url.hashCode();
result = 31 * result + body.hashCode();
return result;
}
public abstract static class Result extends Action.Result {
@ -108,19 +119,19 @@ public class WebhookAction extends Action<WebhookAction.Result> {
public static class Executed extends Result {
private final int httpStatusCode;
private final int httpStatus;
private final String url;
private final String body;
public Executed(int httpStatusCode, String url, String body) {
super(TYPE, httpStatusCode < 400);
this.httpStatusCode = httpStatusCode;
public Executed(int httpStatus, String url, String body) {
super(TYPE, httpStatus < 400);
this.httpStatus = httpStatus;
this.url = url;
this.body = body;
}
public int httpStatusCode() {
return httpStatusCode;
public int httpStatus() {
return httpStatus;
}
public String url() {
@ -134,9 +145,9 @@ public class WebhookAction extends Action<WebhookAction.Result> {
@Override
protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException {
return builder.field("success", success())
.field(Parser.HTTP_STATUS_FIELD.getPreferredName(), httpStatusCode)
.field(Parser.URL_FIELD.getPreferredName(), url)
.field(Parser.BODY_FIELD.getPreferredName(), body);
.field(WebhookAction.Parser.HTTP_STATUS_FIELD.getPreferredName(), httpStatus)
.field(WebhookAction.Parser.URL_FIELD.getPreferredName(), url)
.field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body);
}
}
@ -151,7 +162,7 @@ public class WebhookAction extends Action<WebhookAction.Result> {
@Override
protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException {
return builder.field(Parser.REASON_FIELD.getPreferredName(), reason);
return builder.field(WebhookAction.Parser.REASON_FIELD.getPreferredName(), reason);
}
}
}
@ -160,21 +171,18 @@ public class WebhookAction extends Action<WebhookAction.Result> {
public static class Parser extends AbstractComponent implements Action.Parser<Result, WebhookAction> {
public static final ParseField METHOD_FIELD = new ParseField("method");
public static final ParseField URL_TEMPLATE_FIELD = new ParseField("url_template");
public static final ParseField BODY_TEMPLATE_FIELD = new ParseField("body_template");
public static final ParseField BODY_FIELD = new ParseField("body");
public static final ParseField URL_FIELD = new ParseField("url");
public static final ParseField BODY_FIELD = new ParseField("body");
public static final ParseField HTTP_STATUS_FIELD = new ParseField("http_status");
public static final ParseField REASON_FIELD = new ParseField("reason");
private final StringTemplateUtils templateUtils;
private final Template.Parser templateParser;
private final HttpClient httpClient;
@Inject
public Parser(Settings settings, StringTemplateUtils templateUtils, HttpClient httpClient) {
public Parser(Settings settings, Template.Parser templateParser, HttpClient httpClient) {
super(settings);
this.templateUtils = templateUtils;
this.templateParser = templateParser;
this.httpClient = httpClient;
}
@ -186,8 +194,8 @@ public class WebhookAction extends Action<WebhookAction.Result> {
@Override
public WebhookAction parse(XContentParser parser) throws IOException {
HttpMethod method = HttpMethod.POST;
StringTemplateUtils.Template urlTemplate = null;
StringTemplateUtils.Template bodyTemplate = null;
Template urlTemplate = null;
Template bodyTemplate = null;
String currentFieldName = null;
XContentParser.Token token;
@ -199,26 +207,33 @@ public class WebhookAction extends Action<WebhookAction.Result> {
if (METHOD_FIELD.match(currentFieldName)) {
method = HttpMethod.valueOf(parser.text());
if (method != HttpMethod.POST && method != HttpMethod.GET && method != HttpMethod.PUT) {
throw new ActionException("could not parse webhook action. unsupported http method ["
+ method.getName() + "]");
throw new ActionSettingsException("could not parse webhook action. unsupported http method [" + method.getName() + "]");
}
} else if (URL_FIELD.match(currentFieldName)) {
try {
urlTemplate = templateParser.parse(parser);
} catch (Template.Parser.ParseException pe) {
throw new AlertsSettingsException("could not parse webhook action [url] template", pe);
}
} else if (BODY_FIELD.match(currentFieldName)) {
try {
bodyTemplate = templateParser.parse(parser);
} catch (Template.Parser.ParseException pe) {
throw new ActionSettingsException("could not parse webhook action [body] template", pe);
}
} else if (URL_TEMPLATE_FIELD.match(currentFieldName)) {
urlTemplate = StringTemplateUtils.readTemplate(parser);
} else if (BODY_TEMPLATE_FIELD.match(currentFieldName)) {
bodyTemplate = StringTemplateUtils.readTemplate(parser);
} else {
throw new ActionException("could not parse webhook action. unexpected field [" + currentFieldName + "]");
throw new ActionSettingsException("could not parse webhook action. unexpected field [" + currentFieldName + "]");
}
} else {
throw new ActionException("could not parse webhook action. unexpected token [" + token + "]");
throw new ActionSettingsException("could not parse webhook action. unexpected token [" + token + "]");
}
}
if (urlTemplate == null) {
throw new ActionException("could not parse webhook action. [url_template] is required");
throw new ActionSettingsException("could not parse webhook action. [url_template] is required");
}
return new WebhookAction(logger, templateUtils, httpClient, bodyTemplate, urlTemplate, method);
return new WebhookAction(logger, httpClient, method, urlTemplate, bodyTemplate);
}
@Override
@ -264,25 +279,4 @@ public class WebhookAction extends Action<WebhookAction.Result> {
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebhookAction that = (WebhookAction) o;
if (bodyTemplate != null ? !bodyTemplate.equals(that.bodyTemplate) : that.bodyTemplate != null) return false;
if (method != null ? !method.equals(that.method) : that.method != null) return false;
if (urlTemplate != null ? !urlTemplate.equals(that.urlTemplate) : that.urlTemplate != null) return false;
return true;
}
@Override
public int hashCode() {
int result = urlTemplate != null ? urlTemplate.hashCode() : 0;
result = 31 * result + (method != null ? method.hashCode() : 0);
result = 31 * result + (bodyTemplate != null ? bodyTemplate.hashCode() : 0);
return result;
}
}

View File

@ -134,7 +134,7 @@ public final class AlertUtils {
searchRequest.templateName(parser.textOrNull());
break;
case "template_type":
searchRequest.templateType(readScriptType(parser.textOrNull()));
searchRequest.templateType(ScriptService.ScriptType.valueOf(parser.text().toUpperCase(Locale.ROOT)));
break;
case "search_type":
searchType = SearchType.fromString(parser.text());
@ -169,7 +169,7 @@ public final class AlertUtils {
builder.field("template_name", searchRequest.templateName());
}
if (searchRequest.templateType() != null) {
builder.field("template_type", writeScriptType(searchRequest.templateType()));
builder.field("template_type", searchRequest.templateType().name().toLowerCase(Locale.ROOT));
}
builder.startArray("indices");
for (String index : searchRequest.indices()) {
@ -200,30 +200,4 @@ public final class AlertUtils {
builder.endObject();
}
static ScriptService.ScriptType readScriptType(String value) {
switch (value) {
case "indexed":
return ScriptService.ScriptType.INDEXED;
case "inline":
return ScriptService.ScriptType.INLINE;
case "file":
return ScriptService.ScriptType.FILE;
default:
throw new ElasticsearchIllegalArgumentException("Unknown script_type value [" + value + "]");
}
}
static String writeScriptType(ScriptService.ScriptType value) {
switch (value) {
case INDEXED:
return "indexed";
case INLINE:
return "inline";
case FILE:
return "file";
default:
throw new ElasticsearchIllegalArgumentException("Illegal script_type value [" + value + "]");
}
}
}

View File

@ -1,166 +0,0 @@
/*
* 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.support;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
*/
public class StringTemplateUtils extends AbstractComponent {
private final ScriptServiceProxy scriptService;
@Inject
public StringTemplateUtils(Settings settings, ScriptServiceProxy scriptService) {
super(settings);
this.scriptService = scriptService;
}
public String executeTemplate(Template template) {
return executeTemplate(template, Collections.<String, Object>emptyMap());
}
public String executeTemplate(Template template, Map<String, Object> additionalParams) {
Map<String, Object> params = new HashMap<>();
params.putAll(template.getParams());
params.putAll(additionalParams);
ExecutableScript script = scriptService.executable(template.getLanguage(), template.getTemplate(), template.getScriptType(), params);
Object result = script.run();
if (result instanceof String) {
return (String) result;
} else if (result instanceof BytesReference) {
return ((BytesReference) script.run()).toUtf8();
} else {
return result.toString();
}
}
public static Template readTemplate(XContentParser parser) throws IOException {
assert parser.currentToken() == XContentParser.Token.START_OBJECT : "Expected START_OBJECT, but was " + parser.currentToken();
Map<String, String> params = null;
String script = null;
ScriptService.ScriptType type = ScriptService.ScriptType.INLINE;
String language = "mustache";
String fieldName = parser.currentName();
for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) {
switch (token) {
case FIELD_NAME:
fieldName = parser.currentName();
break;
case START_OBJECT:
switch (fieldName) {
case "params":
params = (Map) parser.map();
break;
default:
throw new ElasticsearchIllegalArgumentException("Unexpected field [" + fieldName + "]");
}
break;
case VALUE_STRING:
switch (fieldName) {
case "script":
script = parser.text();
break;
case "language":
language = parser.text();
break;
case "type":
type = AlertUtils.readScriptType(parser.text());
break;
default:
throw new ElasticsearchIllegalArgumentException("Unexpected field [" + fieldName + "]");
}
break;
default:
throw new ElasticsearchIllegalArgumentException("Unexpected json token [" + token + "]");
}
}
return new Template(script, params, language, type);
}
public static void writeTemplate(String objectName, Template template, XContentBuilder builder, ToXContent.Params params) throws IOException {
builder.startObject(objectName);
builder.field("script", template.getTemplate());
builder.field("type", AlertUtils.writeScriptType(template.getScriptType()));
builder.field("language", template.getLanguage());
if (template.getParams() != null && !template.getParams().isEmpty()) {
builder.field("params", template.getParams());
}
builder.endObject();
}
public static class Template {
private final String template;
private final Map<String, String> params;
private final String language;
private final ScriptService.ScriptType scriptType;
public Template(String template) {
this.template = template;
this.params = Collections.emptyMap();
this.language = "mustache";
this.scriptType = ScriptService.ScriptType.INLINE;
}
public Template(String template, Map<String, String> params, String language, ScriptService.ScriptType scriptType) {
this.template = template;
this.params = params;
this.language = language;
this.scriptType = scriptType;
}
public ScriptService.ScriptType getScriptType() {
return scriptType;
}
public String getTemplate() {
return template;
}
public String getLanguage() {
return language;
}
public Map<String, String> getParams() {
return params;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Template template1 = (Template) o;
if (language != null ? !language.equals(template1.language) : template1.language != null) return false;
if (params != null ? !params.equals(template1.params) : template1.params != null) return false;
if (scriptType != template1.scriptType) return false;
if (template != null ? !template.equals(template1.template) : template1.template != null) return false;
return true;
}
@Override
public int hashCode() {
int result = template != null ? template.hashCode() : 0;
result = 31 * result + (params != null ? params.hashCode() : 0);
result = 31 * result + (language != null ? language.hashCode() : 0);
result = 31 * result + (scriptType != null ? scriptType.hashCode() : 0);
return result;
}
}
}

View File

@ -0,0 +1,193 @@
/*
* 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.support.template;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.base.Function;
import org.elasticsearch.common.base.Joiner;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Collections2;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
*
*/
public class ScriptTemplate implements ToXContent, Template {
static final ParseField TEXT_FIELD = new ParseField("script", "text");
static final ParseField LANG_FIELD = new ParseField("lang", "language", "script_lang");
static final ParseField TYPE_FIELD = new ParseField("type", "script_type");
static final ParseField PARAMS_FIELD = new ParseField("model", "params");
public static final String DEFAULT_LANG = "mustache";
private final String text;
private final String lang;
private final ScriptService.ScriptType type;
private final Map<String, Object> params;
private final ScriptServiceProxy service;
public ScriptTemplate(ScriptServiceProxy service, String text) {
this(service, text, DEFAULT_LANG, ScriptService.ScriptType.INLINE, Collections.<String, Object>emptyMap());
}
public ScriptTemplate(ScriptServiceProxy service, String text, String lang, ScriptService.ScriptType type, Map<String, Object> params) {
this.service = service;
this.text = text;
this.lang = lang;
this.type = type;
this.params = params;
}
public String text() {
return text;
}
public ScriptService.ScriptType type() {
return type;
}
public String lang() {
return lang;
}
public Map<String, Object> params() {
return params;
}
@Override
public String render(Map<String, Object> model) {
Map<String, Object> mergedModel = new HashMap<>();
mergedModel.putAll(params);
mergedModel.putAll(model);
ExecutableScript script = service.executable(lang, text, type, mergedModel);
Object result = script.run();
if (result instanceof BytesReference) {
return ((BytesReference) script.run()).toUtf8();
}
return result.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScriptTemplate template = (ScriptTemplate) o;
if (!lang.equals(template.lang)) return false;
if (!params.equals(template.params)) return false;
if (!text.equals(template.text)) return false;
if (type != template.type) return false;
return true;
}
@Override
public int hashCode() {
int result = text.hashCode();
result = 31 * result + lang.hashCode();
result = 31 * result + type.hashCode();
result = 31 * result + params.hashCode();
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject()
.field(TEXT_FIELD.getPreferredName(), text)
.field(TYPE_FIELD.getPreferredName(), type.name().toLowerCase(Locale.ROOT))
.field(LANG_FIELD.getPreferredName(), lang)
.field(PARAMS_FIELD.getPreferredName(), this.params)
.endObject();
}
/**
*/
public static class Parser extends AbstractComponent implements Template.Parser<ScriptTemplate> {
private final ScriptServiceProxy scriptService;
@Inject
public Parser(Settings settings, ScriptServiceProxy scriptService) {
super(settings);
this.scriptService = scriptService;
}
@Override
public ScriptTemplate parse(XContentParser parser) throws IOException {
assert parser.currentToken() == XContentParser.Token.START_OBJECT : "Expected START_OBJECT, but was " + parser.currentToken();
String text = null;
ScriptService.ScriptType type = ScriptService.ScriptType.INLINE;
String lang = DEFAULT_LANG;
ImmutableMap.Builder<String, Object> params = ImmutableMap.builder();
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 (TEXT_FIELD.match(currentFieldName)) {
if (token.isValue()) {
text = parser.text();
} else {
throw new ParseException("expected a string value for [" + currentFieldName + "], but found [" + token + "] instead");
}
} else if (LANG_FIELD.match(currentFieldName)) {
if (token == XContentParser.Token.VALUE_STRING) {
lang = parser.text();
} else {
throw new ParseException("expected a string value for [" + currentFieldName + "], but found [" + token + "] instead");
}
} else if (TYPE_FIELD.match(currentFieldName)) {
if (token == XContentParser.Token.VALUE_STRING) {
String value = parser.text();
try {
type = ScriptService.ScriptType.valueOf(value.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException iae) {
String typeOptions = Joiner.on(",").join(Collections2.transform(ImmutableList.copyOf(ScriptService.ScriptType.values()), new Function<ScriptService.ScriptType, String>() {
@Override
public String apply(ScriptService.ScriptType scriptType) {
return scriptType.name().toLowerCase(Locale.ROOT);
}
}));
throw new ParseException("unknown template script type [" + currentFieldName + "]. type can only be on of: [" + typeOptions + "]");
}
}
} else if (PARAMS_FIELD.match(currentFieldName)) {
if (token != XContentParser.Token.START_OBJECT) {
throw new ParseException("expected an object for [" + currentFieldName + "], but found [" + token + "]");
}
params.putAll(parser.map());
} else {
throw new ParseException("unexpected field [" + currentFieldName + "]");
}
}
if (text == null) {
throw new ParseException("missing required field [" + TEXT_FIELD.getPreferredName() + "]");
}
return new ScriptTemplate(scriptService, text, lang, type, params.build());
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.support.template;
import org.elasticsearch.alerts.AlertsException;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Map;
/**
*
*/
public interface Template extends ToXContent {
String render(Map<String, Object> model);
interface Parser<T extends Template> {
T parse(XContentParser parser) throws IOException, ParseException;
public static class ParseException extends AlertsException {
public ParseException(String msg) {
super(msg);
}
}
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.support.template;
import org.elasticsearch.alerts.AlertsException;
/**
*
*/
public class TemplateException extends AlertsException {
public TemplateException(String msg) {
super(msg);
}
public TemplateException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@ -0,0 +1,19 @@
/*
* 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.support.template;
import org.elasticsearch.common.inject.AbstractModule;
/**
*
*/
public class TemplateModule extends AbstractModule {
@Override
protected void configure() {
bind(Template.Parser.class).to(ScriptTemplate.Parser.class).asEagerSingleton();
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.support.template;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.yaml.YamlXContent;
import java.io.IOException;
import java.util.Map;
/**
*
*/
public class XContentTemplate implements Template {
public static XContentTemplate YAML = new XContentTemplate(YamlXContent.yamlXContent);
private final XContent xContent;
private XContentTemplate(XContent xContent) {
this.xContent = xContent;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject().endObject();
}
@Override
public String render(Map<String, Object> model) {
try {
return XContentBuilder.builder(xContent).map(model).bytes().toUtf8();
} catch (IOException ioe) {
throw new TemplateException("could not render [" + xContent.type().name() + "] xcontent template", ioe);
}
}
}

View File

@ -24,9 +24,10 @@ import org.elasticsearch.alerts.history.FiredAlert;
import org.elasticsearch.alerts.history.HistoryStore;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.AlertUtils;
import org.elasticsearch.alerts.support.StringTemplateUtils;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.support.template.ScriptTemplate;
import org.elasticsearch.alerts.support.template.Template;
import org.elasticsearch.alerts.transform.SearchTransform;
import org.elasticsearch.alerts.transport.actions.stats.AlertsStatsResponse;
import org.elasticsearch.client.Client;
@ -162,10 +163,10 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
List<Action> actions = new ArrayList<>();
StringTemplateUtils.Template template =
new StringTemplateUtils.Template("{{alert_name}} executed with {{response.hits.total}} hits");
Template url = new ScriptTemplate(scriptService(), "http://localhost/foobarbaz/{{alert_name}}");
Template body = new ScriptTemplate(scriptService(), "{{alert_name}} executed with {{response.hits.total}} hits");
actions.add(new WebhookAction(logger, stringTemplateUtils(), httpClient(), template, new StringTemplateUtils.Template("http://localhost/foobarbaz/{{alert_name}}"), HttpMethod.GET));
actions.add(new WebhookAction(logger, httpClient(), HttpMethod.GET, url, body));
Email.Address from = new Email.Address("from@test.com");
List<Email.Address> emailAddressList = new ArrayList<>();
@ -178,8 +179,8 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
emailBuilder.to(to);
EmailAction emailAction = new EmailAction(logger, noopEmailService(), stringTemplateUtils(), emailBuilder,
new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", template, template, null, true);
EmailAction emailAction = new EmailAction(logger, noopEmailService(), emailBuilder,
new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", body, body, null, true);
actions.add(emailAction);
@ -189,9 +190,9 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
return new Alert(
alertName,
new CronSchedule("0/5 * * * * ? *"),
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
conditionRequest,"return true", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), transformRequest),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), transformRequest),
new TimeValue(0),
new Actions(actions),
metadata,
@ -204,8 +205,12 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
return internalTestCluster().getInstance(AlertsClient.class);
}
protected ScriptService scriptService() {
return internalTestCluster().getInstance(ScriptService.class);
protected ScriptServiceProxy scriptService() {
return internalTestCluster().getInstance(ScriptServiceProxy.class);
}
protected Template.Parser templateParser() {
return internalTestCluster().getInstance(Template.Parser.class);
}
protected HttpClient httpClient() {
@ -216,10 +221,6 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
return new NoopEmailService();
}
protected StringTemplateUtils stringTemplateUtils() {
return internalTestCluster().getInstance(StringTemplateUtils.class);
}
protected FiredAlert.Parser firedAlertParser() {
return internalTestCluster().getInstance(FiredAlert.Parser.class);
}

View File

@ -15,16 +15,15 @@ import org.elasticsearch.alerts.actions.Action;
import org.elasticsearch.alerts.actions.Actions;
import org.elasticsearch.alerts.actions.index.IndexAction;
import org.elasticsearch.alerts.client.AlertsClient;
import org.elasticsearch.alerts.condition.search.ScriptSearchCondition;
import org.elasticsearch.alerts.history.FiredAlert;
import org.elasticsearch.alerts.history.HistoryStore;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.transform.SearchTransform;
import org.elasticsearch.alerts.transport.actions.ack.AckAlertResponse;
import org.elasticsearch.alerts.transport.actions.get.GetAlertResponse;
import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse;
import org.elasticsearch.alerts.condition.search.ScriptSearchCondition;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -40,9 +39,7 @@ import java.util.concurrent.TimeUnit;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsEqual.equalTo;
/**
@ -69,9 +66,9 @@ public class AlertThrottleTests extends AbstractAlertingTests {
Alert alert = new Alert(
"test-serialization",
new CronSchedule("0/5 * * * * ? *"),
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
request, "hits.total > 0", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), request),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), request),
new TimeValue(0),
new Actions(actions),
null,
@ -152,9 +149,9 @@ public class AlertThrottleTests extends AbstractAlertingTests {
Alert alert = new Alert(
"test-time-throttle",
new CronSchedule("0/5 * * * * ? *"),
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
request, "hits.total > 0", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), request),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), request),
new TimeValue(10, TimeUnit.SECONDS),
new Actions(actions),
null,

View File

@ -16,7 +16,6 @@ import org.elasticsearch.alerts.history.FiredAlert;
import org.elasticsearch.alerts.history.HistoryStore;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.transform.SearchTransform;
import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse;
import org.elasticsearch.alerts.transport.actions.stats.AlertsStatsResponse;
@ -80,9 +79,9 @@ public class BootStrapTest extends AbstractAlertingTests {
Alert alert = new Alert(
"test-serialization",
new CronSchedule("0/5 * * * * ? 2035"),
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
searchRequest, "return true", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), searchRequest),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), searchRequest),
new TimeValue(0),
new Actions(new ArrayList<Action>()),
null,
@ -140,9 +139,9 @@ public class BootStrapTest extends AbstractAlertingTests {
Alert alert = new Alert(
"action-test-"+ i + " " + j,
new CronSchedule("0/5 * * * * ? 2035"), //Set a cron schedule far into the future so this alert is never scheduled
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
searchRequest, "return true", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), searchRequest),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), searchRequest),
new TimeValue(0),
new Actions(new ArrayList<Action>()),
null,

View File

@ -13,7 +13,6 @@ import org.elasticsearch.alerts.actions.index.IndexAction;
import org.elasticsearch.alerts.condition.search.ScriptSearchCondition;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.transform.SearchTransform;
import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse;
import org.elasticsearch.common.unit.TimeValue;
@ -59,9 +58,9 @@ public class TransformSearchTest extends AbstractAlertingTests {
Alert alert = new Alert(
"test-serialization",
new CronSchedule("0/5 * * * * ? *"),
new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()),
new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()),
conditionRequest,"return true", ScriptService.ScriptType.INLINE, "groovy"),
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), transformRequest),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), transformRequest),
new TimeValue(0),
new Actions(actions),
metadata,

View File

@ -9,9 +9,10 @@ package org.elasticsearch.alerts.actions;
import org.elasticsearch.alerts.AbstractAlertingTests;
import org.elasticsearch.alerts.Alert;
import org.elasticsearch.alerts.actions.index.IndexAction;
import org.elasticsearch.alerts.condition.Condition;
import org.elasticsearch.alerts.condition.search.ScriptSearchCondition;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.init.proxy.ClientProxy;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.transform.SearchTransform;
import org.elasticsearch.alerts.transport.actions.delete.DeleteAlertRequest;
import org.elasticsearch.alerts.transport.actions.delete.DeleteAlertResponse;
@ -19,8 +20,6 @@ import org.elasticsearch.alerts.transport.actions.get.GetAlertRequest;
import org.elasticsearch.alerts.transport.actions.get.GetAlertResponse;
import org.elasticsearch.alerts.transport.actions.put.PutAlertRequest;
import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse;
import org.elasticsearch.alerts.condition.Condition;
import org.elasticsearch.alerts.condition.search.ScriptSearchCondition;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -57,7 +56,7 @@ public class ActionsTest extends AbstractAlertingTests {
final List<Action> actionList = new ArrayList<>();
actionList.add(alertAction);
Condition alertCondition = new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()),
Condition alertCondition = new ScriptSearchCondition(logger, scriptService(),
ClientProxy.of(client()), createConditionSearchRequest(), "return true", ScriptService.ScriptType.INLINE, "groovy");
@ -65,7 +64,7 @@ public class ActionsTest extends AbstractAlertingTests {
"my-first-alert",
new CronSchedule("0/5 * * * * ? *"),
alertCondition,
new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), createConditionSearchRequest()),
new SearchTransform(logger, scriptService(), ClientProxy.of(client()), createConditionSearchRequest()),
new TimeValue(0),
new Actions(actionList),
null,

View File

@ -13,8 +13,8 @@ import org.elasticsearch.alerts.actions.email.service.Email;
import org.elasticsearch.alerts.actions.email.service.EmailService;
import org.elasticsearch.alerts.actions.email.service.Profile;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.support.StringTemplateUtils;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.alerts.support.template.ScriptTemplate;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.settings.ImmutableSettings;
@ -40,8 +40,6 @@ import java.util.Set;
public class EmailActionTest extends ElasticsearchTestCase {
public void testEmailTemplateRender() throws IOException, MessagingException {
StringTemplateUtils.Template template =
new StringTemplateUtils.Template("{{alert_name}} executed with {{response.hits.total}} hits");
Settings settings = ImmutableSettings.settingsBuilder().build();
MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings);
@ -49,8 +47,9 @@ public class EmailActionTest extends ElasticsearchTestCase {
Set<ScriptEngineService> engineServiceSet = new HashSet<>();
engineServiceSet.add(mustacheScriptEngineService);
ScriptService scriptService = new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, threadPool));
StringTemplateUtils stringTemplateUtils = new StringTemplateUtils(settings, ScriptServiceProxy.of(scriptService));
ScriptServiceProxy scriptService = ScriptServiceProxy.of(new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, threadPool)));
ScriptTemplate template = new ScriptTemplate(scriptService, "{{alert_name}} executed with {{response.hits.total}} hits");
EmailService emailService = new EmailServiceMock();
@ -64,7 +63,7 @@ public class EmailActionTest extends ElasticsearchTestCase {
emailBuilder.from(from);
emailBuilder.to(to);
EmailAction emailAction = new EmailAction(logger, emailService, stringTemplateUtils, emailBuilder,
EmailAction emailAction = new EmailAction(logger, emailService, emailBuilder,
new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", template, template, null, true);
//This is ok since the execution of the action only relies on the alert name

View File

@ -1,50 +0,0 @@
/*
* 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.webhook;
import org.elasticsearch.alerts.support.StringTemplateUtils;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
*/
public class WebhookTest extends ElasticsearchTestCase {
public void testRequestParameterSerialization() throws Exception {
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));
StringTemplateUtils.Template testTemplate = new StringTemplateUtils.Template("{ 'alertname' : '{{alert_name}}', 'response' : { 'hits' : {{response.hits}} } }");
String testBody = WebhookAction.applyTemplate(new StringTemplateUtils(settings, ScriptServiceProxy.of(scriptService)), testTemplate, "foobar", responseMap);
tp.shutdownNow();
assertEquals("{ 'alertname' : 'foobar', 'response' : { 'hits' : 0 } }", testBody);
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.support.template;
import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
*
*/
public class ScriptTemplateTests extends ElasticsearchTestCase {
private ScriptServiceProxy proxy;
private ExecutableScript script;
@Before
public void init() throws Exception {
proxy = mock(ScriptServiceProxy.class);
script = mock(ExecutableScript.class);
}
@Test
public void testRender() throws Exception {
String lang = "_lang";
String templateText = "_template";
Map<String, Object> params = ImmutableMap.<String, Object>of("param_key", "param_val");
Map<String, Object> model = ImmutableMap.<String, Object>of("model_key", "model_val");
Map<String, Object> merged = ImmutableMap.<String, Object>builder().putAll(params).putAll(model).build();
ScriptService.ScriptType scriptType = ScriptService.ScriptType.values()[randomIntBetween(0, ScriptService.ScriptType.values().length - 1)];
when(script.run()).thenReturn("rendered_text");
when(proxy.executable(lang, templateText, scriptType, merged)).thenReturn(script);
ScriptTemplate template = new ScriptTemplate(proxy, templateText, lang, scriptType, params);
assertThat(template.render(model), is("rendered_text"));
}
@Test
public void testRender_OverridingModel() throws Exception {
String lang = "_lang";
String templateText = "_template";
Map<String, Object> params = ImmutableMap.<String, Object>of("key", "param_val");
Map<String, Object> model = ImmutableMap.<String, Object>of("key", "model_val");
ScriptService.ScriptType scriptType = randomScriptType();
when(script.run()).thenReturn("rendered_text");
when(proxy.executable(lang, templateText, scriptType, model)).thenReturn(script);
ScriptTemplate template = new ScriptTemplate(proxy, templateText, lang, scriptType, params);
assertThat(template.render(model), is("rendered_text"));
}
@Test
public void testRender_Defaults() throws Exception {
String templateText = "_template";
Map<String, Object> model = ImmutableMap.<String, Object>of("key", "model_val");
when(script.run()).thenReturn("rendered_text");
when(proxy.executable(ScriptTemplate.DEFAULT_LANG, templateText, ScriptService.ScriptType.INLINE, model)).thenReturn(script);
ScriptTemplate template = new ScriptTemplate(proxy, templateText);
assertThat(template.render(model), is("rendered_text"));
}
@Test
public void testParser() throws Exception {
ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy);
ScriptTemplate template = new ScriptTemplate(proxy, "_template", "_lang", randomScriptType(), ImmutableMap.<String, Object>of("param_key", "param_val"));
XContentBuilder builder = jsonBuilder().startObject()
.field(randomFrom("lang", "script_lang"), template.lang())
.field(randomFrom("script", "text"), template.text())
.field(randomFrom("type", "script_type"), template.type().name())
.field(randomFrom("params", "model"), template.params())
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
ScriptTemplate parsed = templateParser.parse(parser);
assertThat(parsed, notNullValue());
assertThat(parsed, equalTo(template));
}
@Test
public void testParser_ParserSelfGenerated() throws Exception {
ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy);
ScriptTemplate template = new ScriptTemplate(proxy, "_template", "_lang", randomScriptType(), ImmutableMap.<String, Object>of("param_key", "param_val"));
XContentBuilder builder = jsonBuilder().value(template);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
ScriptTemplate parsed = templateParser.parse(parser);
assertThat(parsed, notNullValue());
assertThat(parsed, equalTo(template));
}
@Test(expected = Template.Parser.ParseException.class)
public void testParser_Invalid_UnexpectedField() throws Exception {
ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy);
XContentBuilder builder = jsonBuilder().startObject()
.field("unknown_field", "value")
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
templateParser.parse(parser);
fail("expected parse exception when encountering an unknown field");
}
@Test(expected = Template.Parser.ParseException.class)
public void testParser_Invalid_UnknownScriptType() throws Exception {
ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy);
XContentBuilder builder = jsonBuilder().startObject()
.field("lang", ScriptTemplate.DEFAULT_LANG)
.field("script", "_template")
.field("type", "unknown_type")
.startObject("params").endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
templateParser.parse(parser);
fail("expected parse exception when script type is unknown");
}
@Test(expected = Template.Parser.ParseException.class)
public void testParser_Invalid_MissingScript() throws Exception {
ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy);
XContentBuilder builder = jsonBuilder().startObject()
.field("lang", ScriptTemplate.DEFAULT_LANG)
.field("type", ScriptService.ScriptType.INDEXED)
.startObject("params").endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
templateParser.parse(parser);
fail("expected parse exception when template text is missing");
}
private static ScriptService.ScriptType randomScriptType() {
return randomFrom(ScriptService.ScriptType.values());
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.support.template;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.yaml.YamlXContent;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
/**
*
*/
public class XContentTemplateTests extends ElasticsearchTestCase {
@Test
public void testYaml() throws Exception {
Map<String, Object> model = new HashMap<>();
Map<String, Object> a = new HashMap<>();
a.put("aa", "aa");
a.put("ab", "ab");
model.put("a", a);
Map<String, Object> b = new HashMap<>();
b.put("ba", 21);
model.put("b", b);
model.put("c", "c");
String text = XContentTemplate.YAML.render(model);
// now lets read it as xcontent and make sure it has all the pieces
XContentParser parser = YamlXContent.yamlXContent.createParser(new BytesArray(text));
XContentParser.Token token = parser.nextToken();
assertThat(token, is(XContentParser.Token.START_OBJECT));
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if ("a".equals(currentFieldName)) {
assertThat(token, is(XContentParser.Token.START_OBJECT));
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else {
assertThat(token, is(XContentParser.Token.VALUE_STRING));
if ("aa".equals(currentFieldName)) {
assertThat(parser.text(), equalTo("aa"));
} else if ("ab".equals(currentFieldName)) {
assertThat(parser.text(), equalTo("ab"));
} else {
fail("unexpected field name [" + currentFieldName + "]");
}
}
}
} else if ("b".equals(currentFieldName)) {
assertThat(token, is(XContentParser.Token.START_OBJECT));
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else {
assertThat(token, is(XContentParser.Token.VALUE_NUMBER));
assertThat(currentFieldName, equalTo("ba"));
assertThat(parser.intValue(), is(21));
}
}
} else if ("c".equals(currentFieldName)) {
assertThat(token, is(XContentParser.Token.VALUE_STRING));
assertThat(parser.text(), equalTo("c"));
} else {
fail("unexpected field [" + currentFieldName + "]");
}
}
}
}