diff --git a/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java b/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java index 22fd0069a4a..8d16c2a7b7f 100644 --- a/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java +++ b/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java @@ -7,8 +7,10 @@ package org.elasticsearch.watcher.actions; import org.elasticsearch.watcher.actions.email.EmailAction; import org.elasticsearch.watcher.actions.index.IndexAction; +import org.elasticsearch.watcher.actions.logging.LoggingAction; import org.elasticsearch.watcher.actions.webhook.WebhookAction; import org.elasticsearch.watcher.support.http.TemplatedHttpRequest; +import org.elasticsearch.watcher.support.template.Template; /** * @@ -30,4 +32,12 @@ public final class ActionBuilders { return new WebhookAction.SourceBuilder(id, httpRequest); } + public static LoggingAction.SourceBuilder loggingAction(String id, String text) { + return new LoggingAction.SourceBuilder(id).text(text); + } + + public static LoggingAction.SourceBuilder loggingAction(String id, Template.SourceBuilder text) { + return new LoggingAction.SourceBuilder(id).text(text); + } + } diff --git a/src/main/java/org/elasticsearch/watcher/actions/ActionModule.java b/src/main/java/org/elasticsearch/watcher/actions/ActionModule.java index 2ccd7b37da7..0a60d4d3945 100644 --- a/src/main/java/org/elasticsearch/watcher/actions/ActionModule.java +++ b/src/main/java/org/elasticsearch/watcher/actions/ActionModule.java @@ -11,6 +11,7 @@ import org.elasticsearch.watcher.actions.email.EmailAction; import org.elasticsearch.watcher.actions.email.service.EmailService; import org.elasticsearch.watcher.actions.email.service.InternalEmailService; import org.elasticsearch.watcher.actions.index.IndexAction; +import org.elasticsearch.watcher.actions.logging.LoggingAction; import org.elasticsearch.watcher.actions.webhook.WebhookAction; import java.util.HashMap; @@ -39,6 +40,9 @@ public class ActionModule extends AbstractModule { bind(IndexAction.Parser.class).asEagerSingleton(); parsersBinder.addBinding(IndexAction.TYPE).to(IndexAction.Parser.class); + bind(LoggingAction.Parser.class).asEagerSingleton(); + parsersBinder.addBinding(LoggingAction.TYPE).to(LoggingAction.Parser.class); + for (Map.Entry> entry : parsers.entrySet()) { bind(entry.getValue()).asEagerSingleton(); parsersBinder.addBinding(entry.getKey()).to(entry.getValue()); diff --git a/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingAction.java b/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingAction.java new file mode 100644 index 00000000000..32909fb9aca --- /dev/null +++ b/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingAction.java @@ -0,0 +1,335 @@ +/* + * 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.watcher.actions.logging; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.watcher.actions.Action; +import org.elasticsearch.watcher.actions.ActionException; +import org.elasticsearch.watcher.actions.ActionSettingsException; +import org.elasticsearch.watcher.support.Variables; +import org.elasticsearch.watcher.support.template.ScriptTemplate; +import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.watch.Payload; +import org.elasticsearch.watcher.watch.WatchExecutionContext; + +import java.io.IOException; +import java.util.Locale; + +/** + * + */ +public class LoggingAction extends Action { + + public static final String TYPE = "logging"; + + private final String category; + private final LoggingLevel level; + private final Template template; + + private final ESLogger actionLogger; + + public LoggingAction(ESLogger logger, ESLogger actionLogger, @Nullable String category, LoggingLevel level, Template template) { + super(logger); + this.category = category; + this.level = level; + this.template = template; + this.actionLogger = actionLogger; + } + + @Override + public String type() { + return TYPE; + } + + String category() { + return category; + } + + LoggingLevel level() { + return level; + } + + Template template() { + return template; + } + + ESLogger logger() { + return actionLogger; + } + + @Override + protected LoggingAction.Result execute(String actionId, WatchExecutionContext ctx, Payload payload) throws IOException { + try { + String text = template.render(Variables.createCtxModel(ctx, payload)); + level.log(actionLogger, text); + return new Result.Success(text); + } catch (Exception e) { + return new Result.Failure("failed to execute log action: " + e.getMessage()); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (category != null) { + builder.field(Parser.CATEGORY_FIELD.getPreferredName(), category); + } + builder.field(Parser.LEVEL_FIELD.getPreferredName(), level); + builder.field(Parser.TEXT_FIELD.getPreferredName(), template); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LoggingAction action = (LoggingAction) o; + + if (category != null ? !category.equals(action.category) : action.category != null) return false; + if (level != action.level) return false; + return template.equals(action.template); + } + + @Override + public int hashCode() { + int result = category != null ? category.hashCode() : 0; + result = 31 * result + level.hashCode(); + result = 31 * result + template.hashCode(); + return result; + } + + public static abstract class Result extends Action.Result { + + protected Result(boolean success) { + super(TYPE, success); + } + + public static class Success extends Result { + + private final String loggedText; + + public Success(String loggedText) { + super(true); + this.loggedText = loggedText; + } + + public String loggedText() { + return loggedText; + } + + @Override + protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { + return builder.field(Parser.LOGGED_TEXT_FIELD.getPreferredName(), loggedText); + } + } + + public static class Failure extends Result { + + private final String reason; + + public Failure(String reason) { + super(false); + this.reason = reason; + } + + public String reason() { + return reason; + } + + @Override + protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { + return builder.field(Parser.REASON_FIELD.getPreferredName(), reason); + } + } + } + + public static class Parser implements Action.Parser { + + static final ParseField CATEGORY_FIELD = new ParseField("category"); + static final ParseField LEVEL_FIELD = new ParseField("level"); + static final ParseField TEXT_FIELD = new ParseField("text"); + static final ParseField LOGGED_TEXT_FIELD = new ParseField("logged_text"); + static final ParseField REASON_FIELD = new ParseField("reason"); + + private final Settings settings; + private final Template.Parser templateParser; + private final ESLogger logger; + + @Inject + public Parser(Settings settings, Template.Parser templateParser) { + this.settings = settings; + this.logger = Loggers.getLogger(LoggingAction.class, settings); + this.templateParser = templateParser; + } + + @Override + public String type() { + return TYPE; + } + + @Override + public LoggingAction parse(XContentParser parser) throws IOException { + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + + String category = null; + LoggingLevel level = LoggingLevel.INFO; + Template text = null; + + 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 == XContentParser.Token.VALUE_STRING) { + if (CATEGORY_FIELD.match(currentFieldName)) { + category = parser.text(); + } else if (LEVEL_FIELD.match(currentFieldName)) { + try { + level = LoggingLevel.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException iae) { + throw new ActionSettingsException("failed to parse logging action. unknown logging level [" + parser.text() + "]"); + } + } else if (TEXT_FIELD.match(currentFieldName)) { + try { + text = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("failed to parse logging action. failed to parse text template", pe); + } + } else { + throw new ActionSettingsException("failed to parse logging action. unexpected string field [" + currentFieldName + "]"); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (TEXT_FIELD.match(currentFieldName)) { + try { + text = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("failed to parse logging action. failed to parse text template", pe); + } + } else { + throw new ActionSettingsException("failed to parse logging action. unexpected object field [" + currentFieldName + "]"); + } + } else { + throw new ActionSettingsException("failed to parse logging action. unexpected token [" + token + "]"); + } + } + + if (text == null) { + throw new ActionSettingsException("failed to parse logging action. missing [text] field"); + } + + ESLogger actionLogger = category != null ? Loggers.getLogger(category, settings) : logger; + + return new LoggingAction(logger, actionLogger, category, level, text); + } + + @Override + public LoggingAction.Result parseResult(XContentParser parser) throws IOException { + Boolean success = null; + String loggedText = null; + String reason = null; + + XContentParser.Token token; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if (LOGGED_TEXT_FIELD.match(currentFieldName)) { + loggedText = parser.text(); + } else if (REASON_FIELD.match(currentFieldName)) { + reason = parser.text(); + } else { + throw new ActionException("could not parse index result. unexpected string field [" + currentFieldName + "]"); + } + } else if (token == XContentParser.Token.VALUE_BOOLEAN) { + if (Action.Result.SUCCESS_FIELD.match(currentFieldName)) { + success = parser.booleanValue(); + } else { + throw new ActionException("could not parse index result. unexpected boolean field [" + currentFieldName + "]"); + } + } else { + throw new ActionException("could not parse index result. unexpected token [" + token + "]"); + } + } + + if (success == null) { + throw new ActionException("could not parse index result. expected boolean field [success]"); + } + + if (success) { + if (loggedText == null) { + throw new ActionException("could not parse successful index result. expected string field [logged_text]"); + } + return new Result.Success(loggedText); + } + + if (reason == null) { + throw new ActionException("could not parse failed index result. expected string field [reason]"); + } + + return new Result.Failure(reason); + } + } + + public static class SourceBuilder extends Action.SourceBuilder { + + private Template.SourceBuilder text; + private String category; + private LoggingLevel level; + + public SourceBuilder(String id) { + super(id); + } + + @Override + public String type() { + return TYPE; + } + + public SourceBuilder text(String text) { + return text(new ScriptTemplate.SourceBuilder(text)); + } + + public SourceBuilder text(Template.SourceBuilder text) { + this.text = text; + return this; + } + + public SourceBuilder category(String category) { + this.category = category; + return this; + } + + public SourceBuilder level(LoggingLevel level) { + this.level = level; + return this; + } + + @Override + protected XContentBuilder actionXContent(XContentBuilder builder, Params params) throws IOException { + if (text == null) { + throw new ActionException("could not build logging action source. [text] must be defined"); + } + builder.startObject(); + builder.field(Parser.TEXT_FIELD.getPreferredName(), text); + if (category != null) { + builder.field(Parser.CATEGORY_FIELD.getPreferredName(), category); + } + if (level != null) { + builder.field(Parser.LEVEL_FIELD.getPreferredName(), level); + } + return builder.endObject(); + } + } +} diff --git a/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingLevel.java b/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingLevel.java new file mode 100644 index 00000000000..aeee44db369 --- /dev/null +++ b/src/main/java/org/elasticsearch/watcher/actions/logging/LoggingLevel.java @@ -0,0 +1,58 @@ +/* + * 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.watcher.actions.logging; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Locale; + +/** + * + */ +public enum LoggingLevel implements ToXContent { + + ERROR() { + @Override + void log(ESLogger logger, String text) { + logger.error(text); + } + }, + WARN() { + @Override + void log(ESLogger logger, String text) { + logger.warn(text); + } + }, + INFO() { + @Override + void log(ESLogger logger, String text) { + logger.info(text); + } + }, + DEBUG() { + @Override + void log(ESLogger logger, String text) { + logger.debug(text); + } + }, + TRACE() { + @Override + void log(ESLogger logger, String text) { + logger.trace(text); + } + }; + + abstract void log(ESLogger logger, String text); + + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(name().toLowerCase(Locale.ROOT)); + } +} diff --git a/src/main/java/org/elasticsearch/watcher/support/template/Template.java b/src/main/java/org/elasticsearch/watcher/support/template/Template.java index 312533bde68..901f099ffcf 100644 --- a/src/main/java/org/elasticsearch/watcher/support/template/Template.java +++ b/src/main/java/org/elasticsearch/watcher/support/template/Template.java @@ -24,7 +24,7 @@ public interface Template extends ToXContent { T parse(XContentParser parser) throws IOException, ParseException; - public static class ParseException extends WatcherException { + class ParseException extends WatcherException { public ParseException(String msg) { super(msg); diff --git a/src/test/java/org/elasticsearch/watcher/actions/logging/LoggingActionTests.java b/src/test/java/org/elasticsearch/watcher/actions/logging/LoggingActionTests.java new file mode 100644 index 00000000000..e1deb49ef7b --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/actions/logging/LoggingActionTests.java @@ -0,0 +1,335 @@ +/* + * 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.watcher.actions.logging; + +import com.carrotsearch.randomizedtesting.annotations.Repeat; +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.joda.time.DateTime; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.watcher.actions.ActionException; +import org.elasticsearch.watcher.actions.email.service.Attachment; +import org.elasticsearch.watcher.support.template.ValueTemplate; +import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent; +import org.elasticsearch.watcher.watch.Payload; +import org.elasticsearch.watcher.watch.Watch; +import org.elasticsearch.watcher.watch.WatchExecutionContext; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +import static org.elasticsearch.common.joda.time.DateTimeZone.UTC; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.watcher.actions.ActionBuilders.loggingAction; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.*; + +/** + */ +public class LoggingActionTests extends ElasticsearchTestCase { + + private ESLogger actionLogger; + private LoggingLevel level; + + @Before + public void init() throws IOException { + actionLogger = mock(ESLogger.class); + level = randomFrom(LoggingLevel.values()); + } + + @Test @Repeat(iterations = 30) + public void testExecute() throws Exception { + final DateTime now = DateTime.now(UTC); + + final Map expectedModel = ImmutableMap.builder() + .put("ctx", ImmutableMap.builder() + .put("execution_time", now) + .put("watch_name", "_watch_name") + .put("payload", ImmutableMap.of()) + .put("trigger", ImmutableMap.builder() + .put("scheduled_time", now) + .put("triggered_time", now) + .build()) + .build()) + .build(); + + String text = randomAsciiOfLength(10); + LoggingAction action = new LoggingAction(logger, actionLogger, "_category", level, new ValueTemplate(text) { + @Override + public String render(Map model) { + assertThat(model, equalTo((Object) expectedModel)); + return super.render(model); + } + }); + + Watch watch = mock(Watch.class); + when(watch.name()).thenReturn("_watch_name"); + WatchExecutionContext ctx = new WatchExecutionContext("_ctx_id", watch, now, new ScheduleTriggerEvent(now, now)); + + LoggingAction.Result result = action.execute("_id", ctx, new Payload.Simple()); + verifyLogger(actionLogger, level, text); + + assertThat(result, notNullValue()); + assertThat(result.success(), is(true)); + assertThat(result, instanceOf(LoggingAction.Result.Success.class)); + assertThat(((LoggingAction.Result.Success) result).loggedText(), is(text)); + } + + @Test @Repeat(iterations = 10) + public void testParser() throws Exception { + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + + XContentBuilder builder = jsonBuilder().startObject(); + builder.field("text", new ValueTemplate(text)); + String category = null; + if (randomBoolean()) { + category = randomAsciiOfLength(10); + builder.field("category", category); + } + LoggingLevel level = null; + if (randomBoolean()) { + level = randomFrom(LoggingLevel.values()); + builder.field("level", level); + } + builder.endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + LoggingAction action = parser.parse(xContentParser); + + assertThat(action, notNullValue()); + assertThat(action.category(), is(category)); + assertThat(action.level(), level == null ? is(LoggingLevel.INFO) : is(level)); + assertThat(action.logger(), notNullValue()); + assertThat(action.template(), notNullValue()); + assertThat(action.template().render(Collections.emptyMap()), is(text)); + } + + @Test @Repeat(iterations = 10) + public void testParser_SelfGenerated() throws Exception { + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + String category = randomAsciiOfLength(10); + LoggingAction action = new LoggingAction(logger, actionLogger, category, level, new ValueTemplate(text)); + XContentBuilder builder = jsonBuilder(); + action.toXContent(builder, Attachment.XContent.EMPTY_PARAMS); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + LoggingAction parsedAction = parser.parse(xContentParser); + + assertThat(parsedAction, equalTo(action)); + } + + @Test @Repeat(iterations = 10) + public void testParser_SourceBuilder() throws Exception { + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + LoggingAction.SourceBuilder sourceBuilder = loggingAction("_id", new ValueTemplate.SourceBuilder(text)); + String category = null; + if (randomBoolean()) { + category = randomAsciiOfLength(10); + sourceBuilder.category(category); + } + LoggingLevel level = null; + if (randomBoolean()) { + level = randomFrom(LoggingLevel.values()); + sourceBuilder.level(level); + } + + XContentBuilder builder = jsonBuilder(); + sourceBuilder.toXContent(builder, Attachment.XContent.EMPTY_PARAMS); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + + assertThat(xContentParser.nextToken(), is(XContentParser.Token.START_OBJECT)); + assertThat(xContentParser.nextToken(), is(XContentParser.Token.FIELD_NAME)); + assertThat(xContentParser.currentName(), is(LoggingAction.TYPE)); + assertThat(xContentParser.nextToken(), is(XContentParser.Token.START_OBJECT)); + LoggingAction action = parser.parse(xContentParser); + assertThat(action, notNullValue()); + assertThat(action.category(), is(category)); + assertThat(action.level(), level == null ? is(LoggingLevel.INFO) : is(level)); + assertThat(action.logger(), notNullValue()); + assertThat(action.template(), notNullValue()); + assertThat(action.template(), Matchers.instanceOf(ValueTemplate.class)); + assertThat(action.template().render(Collections.emptyMap()), is(text)); + } + + @Test(expected = ActionException.class) + public void testParser_Failure() throws Exception { + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + XContentBuilder builder = jsonBuilder() + .startObject().endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as there's no text + parser.parse(xContentParser); + } + + @Test @Repeat(iterations = 30) + public void testParser_Result_Success() throws Exception { + + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + XContentBuilder builder = jsonBuilder().startObject() + .field("success", true) + .field("logged_text", text) + .endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as there's no text + LoggingAction.Result result = parser.parseResult(xContentParser); + assertThat(result, Matchers.notNullValue()); + assertThat(result.success(), is(true)); + assertThat(result, Matchers.instanceOf(LoggingAction.Result.Success.class)); + assertThat(((LoggingAction.Result.Success) result).loggedText(), is(text)); + } + + @Test @Repeat(iterations = 30) + public void testParser_Result_Failure() throws Exception { + + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String reason = randomAsciiOfLength(10); + XContentBuilder builder = jsonBuilder().startObject() + .field("success", false) + .field("reason", reason) + .endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as there's no text + LoggingAction.Result result = parser.parseResult(xContentParser); + assertThat(result, Matchers.notNullValue()); + assertThat(result.success(), is(false)); + assertThat(result, Matchers.instanceOf(LoggingAction.Result.Failure.class)); + assertThat(((LoggingAction.Result.Failure) result).reason(), is(reason)); + } + + @Test(expected = ActionException.class) + public void testParser_Result_MissingSuccessField() throws Exception { + + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + XContentBuilder builder = jsonBuilder().startObject() + .field("logged_text", text) + .endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as there's no success boolean field + parser.parseResult(xContentParser); + } + + @Test(expected = ActionException.class) + public void testParser_Result_Failure_WithoutReason() throws Exception { + + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + XContentBuilder builder = jsonBuilder().startObject() + .field("success", false); + if (randomBoolean()) { + builder.field("logged_text", text); + } + builder.endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as the reason field is missing for the failure result + parser.parseResult(xContentParser); + } + + @Test(expected = ActionException.class) + public void testParser_Result_Success_WithoutLoggedText() throws Exception { + + Settings settings = ImmutableSettings.EMPTY; + ValueTemplate.Parser templateParser = new ValueTemplate.Parser(); + LoggingAction.Parser parser = new LoggingAction.Parser(settings, templateParser); + + String text = randomAsciiOfLength(10); + XContentBuilder builder = jsonBuilder().startObject() + .field("success", true); + if (randomBoolean()) { + builder.field("reason", text); + } + builder.endObject(); + + XContentParser xContentParser = JsonXContent.jsonXContent.createParser(builder.bytes()); + xContentParser.nextToken(); + + // will fail as the logged_text field is missing for the successful result + parser.parseResult(xContentParser); + } + + static void verifyLogger(ESLogger logger, LoggingLevel level, String text) { + switch (level) { + case ERROR: + verify(logger, times(1)).error(text); + break; + case WARN: + verify(logger, times(1)).warn(text); + break; + case INFO: + verify(logger, times(1)).info(text); + break; + case DEBUG: + verify(logger, times(1)).debug(text); + break; + case TRACE: + verify(logger, times(1)).trace(text); + break; + default: + fail("unhandled logging level [" + level.name() + "]"); + } + } + +} diff --git a/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java b/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java index 659a27415a5..975fb48d7a1 100644 --- a/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java +++ b/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java @@ -16,8 +16,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.ElasticsearchTestCase; -import org.elasticsearch.watcher.actions.Actions; import org.elasticsearch.watcher.actions.ActionWrapper; +import org.elasticsearch.watcher.actions.Actions; import org.elasticsearch.watcher.condition.simple.AlwaysTrueCondition; import org.elasticsearch.watcher.input.Input; import org.elasticsearch.watcher.input.InputBuilders; @@ -28,6 +28,7 @@ import org.elasticsearch.watcher.support.http.auth.BasicAuth; import org.elasticsearch.watcher.support.http.auth.HttpAuth; import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry; import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.support.template.ValueTemplate; import org.elasticsearch.watcher.trigger.schedule.IntervalSchedule; import org.elasticsearch.watcher.trigger.schedule.ScheduleTrigger; import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent; @@ -39,7 +40,6 @@ import org.hamcrest.Description; import org.junit.Before; import org.junit.Test; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Map; @@ -61,7 +61,7 @@ public class HttpInputTests extends ElasticsearchTestCase { @Before public void init() throws Exception { httpClient = mock(HttpClient.class); - Template.Parser templateParser = new MockTemplate.Parser(); + Template.Parser templateParser = new ValueTemplate.Parser(); HttpAuthRegistry registry = new HttpAuthRegistry(ImmutableMap.of("basic", new BasicAuth.Parser())); httpParser = new HttpInput.Parser( ImmutableSettings.EMPTY, httpClient, new HttpRequest.Parser(registry), new TemplatedHttpRequest.Parser(templateParser, registry) @@ -120,7 +120,7 @@ public class HttpInputTests extends ElasticsearchTestCase { .setScheme(scheme) .setMethod(httpMethod) .setPath(pathTemplate) - .setBody(body != null ? new MockTemplate(body) : null) + .setBody(body != null ? new ValueTemplate(body) : null) .setAuth(auth); if (params != null) { @@ -176,7 +176,7 @@ public class HttpInputTests extends ElasticsearchTestCase { .setMethod(httpMethod) .setHost("_host") .setPort(123) - .setBody(new MockTemplate(body)) + .setBody(new ValueTemplate(body)) .setHeaders(headers); Map payload = MapBuilder.newMapBuilder().put("x", "y").map(); @@ -202,56 +202,11 @@ public class HttpInputTests extends ElasticsearchTestCase { } private static Template mockTemplate(String value) { - return new MockTemplate(value); + return new ValueTemplate(value); } private static Template.SourceBuilder mockTemplateSourceBuilder(String value) { - return new Template.InstanceSourceBuilder(new MockTemplate(value)); - } - - private static class MockTemplate implements Template { - - private final String value; - - private MockTemplate(String value) { - this.value = value; - } - - @Override - public String render(Map model) { - return value; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.value(value); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MockTemplate that = (MockTemplate) o; - - if (!value.equals(that.value)) return false; - - return true; - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - static class Parser implements Template.Parser { - - @Override - public Template parse(XContentParser parser) throws IOException, ParseException { - String value = parser.text(); - return new MockTemplate(value); - } - } + return new Template.InstanceSourceBuilder(new ValueTemplate(value)); } static MockTemplateMatcher isTemplate(String value) { @@ -268,7 +223,7 @@ public class HttpInputTests extends ElasticsearchTestCase { @Override public boolean matches(Object item) { - return item instanceof MockTemplate && ((MockTemplate) item).value.equals(value); + return item instanceof ValueTemplate && ((ValueTemplate) item).value().equals(value); } @Override diff --git a/src/test/java/org/elasticsearch/watcher/support/template/ValueTemplate.java b/src/test/java/org/elasticsearch/watcher/support/template/ValueTemplate.java new file mode 100644 index 00000000000..65547fc9f5b --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/support/template/ValueTemplate.java @@ -0,0 +1,79 @@ +/* + * 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.watcher.support.template; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Map; + +/** + * + */ +public class ValueTemplate implements Template { + + private final String value; + + public ValueTemplate(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public String render(Map model) { + return value; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ValueTemplate that = (ValueTemplate) o; + + if (!value.equals(that.value)) return false; + + return true; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public static class Parser implements Template.Parser { + + @Override + public Template parse(XContentParser parser) throws IOException, ParseException { + String value = parser.text(); + return new ValueTemplate(value); + } + } + + public static class SourceBuilder implements Template.SourceBuilder { + + private final String value; + + public SourceBuilder(String value) { + this.value = value; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(value); + } + } +} diff --git a/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java b/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java index 253196335e0..4cceeccc7ac 100644 --- a/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java +++ b/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java @@ -35,6 +35,7 @@ import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.elasticsearch.watcher.actions.ActionBuilders.indexAction; +import static org.elasticsearch.watcher.actions.ActionBuilders.loggingAction; import static org.elasticsearch.watcher.client.WatchSourceBuilders.watchBuilder; import static org.elasticsearch.watcher.condition.ConditionBuilders.scriptCondition; import static org.elasticsearch.watcher.input.InputBuilders.searchInput; @@ -59,7 +60,12 @@ public class BasicWatcherTests extends AbstractWatcherIntegrationTests { .source(watchBuilder() .trigger(schedule(interval(5, IntervalSchedule.Interval.Unit.SECONDS))) .input(searchInput(searchRequest)) - .condition(scriptCondition("ctx.payload.hits.total == 1"))) + .condition(scriptCondition("ctx.payload.hits.total == 1")) + .addAction(loggingAction("_logger", + "\n\n************\n" + + "total hits: {{ctx.payload.hits.total}}\n" + + "************\n") + .category("_category"))) .get(); if (timeWarped()) {