diff --git a/chaos-monkey-docs/src/main/asciidoc/changes.adoc b/chaos-monkey-docs/src/main/asciidoc/changes.adoc index ae782172..a0c330e2 100644 --- a/chaos-monkey-docs/src/main/asciidoc/changes.adoc +++ b/chaos-monkey-docs/src/main/asciidoc/changes.adoc @@ -9,6 +9,7 @@ Built with Spring Boot {spring-boot-version} === Improvements // - https://github.com/codecentric/chaos-monkey-spring-boot/pull/xxx[#xxx] Added example entry. Please don't remove. +- https://github.com/codecentric/chaos-monkey-spring-boot/pull/411[#411] Allow custom exceptions to fall through in outgoing watchers (rest template, webclient). This could also slightly change the behaviour/output of outgoing watchers when not using a custom exception. === New Features // - https://github.com/codecentric/chaos-monkey-spring-boot/pull/xxx[#xxx] Added example entry. Please don't remove. diff --git a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyRestTemplateConfiguration.java b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyRestTemplateConfiguration.java index 8e94d996..d8a88244 100644 --- a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyRestTemplateConfiguration.java +++ b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyRestTemplateConfiguration.java @@ -43,7 +43,7 @@ public ChaosMonkeyRestTemplateCustomizer chaosMonkeyRestTemplateCustomizer(final @Bean @DependsOn("chaosMonkeyRequestScope") public ChaosMonkeyRestTemplateWatcher chaosMonkeyRestTemplateInterceptor(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, - final WatcherProperties watcherProperties, final AssaultProperties assaultProperties) { - return new ChaosMonkeyRestTemplateWatcher(chaosMonkeyRequestScope, watcherProperties, assaultProperties); + final WatcherProperties watcherProperties) { + return new ChaosMonkeyRestTemplateWatcher(chaosMonkeyRequestScope, watcherProperties); } } diff --git a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyWebClientConfiguration.java b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyWebClientConfiguration.java index 585d9b74..33b03b1b 100644 --- a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyWebClientConfiguration.java +++ b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/configuration/ChaosMonkeyWebClientConfiguration.java @@ -43,7 +43,7 @@ public ChaosMonkeyWebClientCustomizer chaosMonkeyWebClientCustomizer(final Chaos @Bean @DependsOn("chaosMonkeyRequestScope") public ChaosMonkeyWebClientWatcher chaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, - final WatcherProperties watcherProperties, final AssaultProperties assaultProperties) { - return new ChaosMonkeyWebClientWatcher(chaosMonkeyRequestScope, watcherProperties, assaultProperties); + final WatcherProperties watcherProperties) { + return new ChaosMonkeyWebClientWatcher(chaosMonkeyRequestScope, watcherProperties); } } diff --git a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java index 3248db0a..28c342e0 100644 --- a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java +++ b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java @@ -17,36 +17,25 @@ import de.codecentric.spring.boot.chaos.monkey.component.ChaosMonkeyRequestScope; import de.codecentric.spring.boot.chaos.monkey.component.ChaosTarget; -import de.codecentric.spring.boot.chaos.monkey.configuration.AssaultProperties; import de.codecentric.spring.boot.chaos.monkey.configuration.WatcherProperties; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; -import org.springframework.util.StreamUtils; -/** @author Marcel Becker */ +import java.io.IOException; + +/** + * @author Marcel Becker + */ public class ChaosMonkeyRestTemplateWatcher implements ClientHttpRequestInterceptor { private final WatcherProperties watcherProperties; private final ChaosMonkeyRequestScope chaosMonkeyRequestScope; - private final AssaultProperties assaultProperties; - public ChaosMonkeyRestTemplateWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties, - AssaultProperties assaultProperties) { + public ChaosMonkeyRestTemplateWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties) { this.chaosMonkeyRequestScope = chaosMonkeyRequestScope; this.watcherProperties = watcherProperties; - this.assaultProperties = assaultProperties; } @Override @@ -54,72 +43,8 @@ public ClientHttpResponse intercept(final HttpRequest httpRequest, byte[] bytes, throws IOException { ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes); if (watcherProperties.isRestTemplate()) { - try { - chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.REST_TEMPLATE, httpRequest.getURI().toString()); - } catch (final Exception exception) { - try { - if (exception.getClass().equals(assaultProperties.getException().getExceptionClass())) { - response = new ErrorResponse(); - } else { - throw exception; - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } + chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.REST_TEMPLATE, httpRequest.getURI().toString()); } return response; } - - static class ErrorResponse implements ClientHttpResponse { - - static final String ERROR_TEXT = "This error is generated by Chaos Monkey for Spring Boot"; - static final String ERROR_BODY = "{\"error\": \"This is a Chaos Monkey for Spring Boot generated failure\"}"; - - private static final Logger Logger = LoggerFactory.getLogger(ErrorResponse.class); - - @Nullable - private InputStream responseStream; - - @Override - public int getRawStatusCode() { - return HttpStatus.INTERNAL_SERVER_ERROR.value(); - } - - @Override - public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(getRawStatusCode()); - } - - @Override - public String getStatusText() { - return ERROR_TEXT; - } - - @Override - public void close() { - try { - if (this.responseStream == null) { - getBody(); - } - StreamUtils.drain(this.responseStream); - this.responseStream.close(); - } catch (Exception ex) { - Logger.debug("Exception while closing the error response", ex); - // ignore {@see - // #org.springframework.http.client.SimpleClientHttpResponse.close()} - } - } - - @Override - public InputStream getBody() { - responseStream = new ByteArrayInputStream(ERROR_BODY.getBytes(StandardCharsets.UTF_8)); - return responseStream; - } - - @Override - public HttpHeaders getHeaders() { - return new HttpHeaders(); - } - } } diff --git a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java index 5cee7746..b8264209 100644 --- a/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java +++ b/chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java @@ -17,29 +17,26 @@ import de.codecentric.spring.boot.chaos.monkey.component.ChaosMonkeyRequestScope; import de.codecentric.spring.boot.chaos.monkey.component.ChaosTarget; -import de.codecentric.spring.boot.chaos.monkey.configuration.AssaultProperties; import de.codecentric.spring.boot.chaos.monkey.configuration.WatcherProperties; -import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; import reactor.core.publisher.Mono; -/** @author Marcel Becker */ +/** + * @author Marcel Becker + */ public class ChaosMonkeyWebClientWatcher implements ExchangeFilterFunction { private final ChaosMonkeyRequestScope chaosMonkeyRequestScope; private final WatcherProperties watcherProperties; - private final AssaultProperties assaultProperties; private static final String ALREADY_FILTERED_SUFFIX = ".FILTERED"; - public ChaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties, - AssaultProperties assaultProperties) { + public ChaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties) { this.chaosMonkeyRequestScope = chaosMonkeyRequestScope; this.watcherProperties = watcherProperties; - this.assaultProperties = assaultProperties; } @Override @@ -48,19 +45,10 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction Mono response = exchangeFunction.exchange(requestFilterWrapper.clientRequest); if (requestFilterWrapper.filter) { if (watcherProperties.isWebClient()) { - try { + response = response.map((clientResponse) -> { chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.WEB_CLIENT, clientRequest.url().toString()); - } catch (final Exception exception) { - try { - if (exception.getClass().equals(assaultProperties.getException().getExceptionClass())) { - response = Mono.just(ErrorClientResponse.getResponse()); - } else { - throw exception; - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } + return clientResponse; + }); } } return response; @@ -82,13 +70,4 @@ private RequestFilterWrapper handleOncePerRequest(final ClientRequest clientRequ private record RequestFilterWrapper(ClientRequest clientRequest, Boolean filter) { } - - static class ErrorClientResponse { - - static final String ERROR_BODY = "{\"error\": \"This is a Chaos Monkey for Spring Boot generated failure\"}"; - - private static ClientResponse getResponse() { - return ClientResponse.create(HttpStatus.INTERNAL_SERVER_ERROR).body(ERROR_BODY).build(); - } - } } diff --git a/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcherIntegrationTest.java b/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcherIntegrationTest.java index d067065c..2e16ad9a 100644 --- a/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcherIntegrationTest.java +++ b/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package de.codecentric.spring.boot.chaos.monkey.watcher.outgoing; -import static de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplateWatcher.ErrorResponse.ERROR_BODY; -import static de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplateWatcher.ErrorResponse.ERROR_TEXT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -52,7 +50,10 @@ public void testInterceptorIsPresent() { } @SpringBootTest(properties = {"chaos.monkey.enabled=true", "chaos.monkey.watcher.rest-template=true", - "chaos.monkey.assaults.exceptions-active=true"}, classes = {ChaosDemoApplication.class}) + "chaos.monkey.assaults.exceptions-active=true", + "chaos.monkey.assaults.exception.type=org.springframework.web.client.HttpServerErrorException", + "chaos.monkey.assaults.exception.arguments[0].type=org.springframework.http.HttpStatusCode", + "chaos.monkey.assaults.exception.arguments[0].value=500"}, classes = {ChaosDemoApplication.class}) @ActiveProfiles("chaos-monkey") @Nested class ExceptionAssaultIntegrationTest { @@ -62,8 +63,7 @@ class ExceptionAssaultIntegrationTest { @Test public void testRestTemplateExceptionAssault() { - assertThatThrownBy(() -> this.demoRestTemplateService.callWithRestTemplate()) - .hasMessage(500 + " " + ERROR_TEXT + ": \"" + ERROR_BODY + '"'); + assertThatThrownBy(() -> this.demoRestTemplateService.callWithRestTemplate()).hasMessage("500 INTERNAL_SERVER_ERROR"); } } diff --git a/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcherIntegrationTest.java b/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcherIntegrationTest.java index 8b5406c0..9e75d16b 100644 --- a/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcherIntegrationTest.java +++ b/chaos-monkey-spring-boot/src/test/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,48 +15,51 @@ */ package de.codecentric.spring.boot.chaos.monkey.watcher.outgoing; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; - -import de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyWebClientWatcher.ErrorClientResponse; import de.codecentric.spring.boot.demo.chaos.monkey.ChaosDemoApplication; import de.codecentric.spring.boot.demo.chaos.monkey.service.DemoWebClientService; import io.netty.handler.timeout.ReadTimeoutException; -import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.reactive.function.client.WebClientResponseException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + class ChaosMonkeyWebClientWatcherIntegrationTest { - @SpringBootTest(properties = {"chaos.monkey.watcher.web-client=true", "chaos.monkey.enabled=true", - "chaos.monkey.assaults.exceptions-active=true"}, classes = {ChaosDemoApplication.class}) + @Nested + @SpringBootTest(properties = {"chaos.monkey.watcher.web-client=true", "chaos.monkey.enabled=true", "chaos.monkey.assaults.exceptions-active=true", + "chaos.monkey.assaults.exception.type=org.springframework.web.reactive.function.client.WebClientResponseException", + "chaos.monkey.assaults.exception.method=create", "chaos.monkey.assaults.exception.arguments[0].type=int", + "chaos.monkey.assaults.exception.arguments[0].value=500", "chaos.monkey.assaults.exception.arguments[1].type=java.lang.String", + "chaos.monkey.assaults.exception.arguments[1].value=Failed", + "chaos.monkey.assaults.exception.arguments[2].type=org.springframework.http.HttpHeaders", + "chaos.monkey.assaults.exception.arguments[2].value=null", "chaos.monkey.assaults.exception.arguments[3].type=byte[]", + "chaos.monkey.assaults.exception.arguments[3].value=[70,97,105,108]", // "Fail" in UTF8 + "chaos.monkey.assaults.exception.arguments[4].type=java.nio.charset.Charset", + "chaos.monkey.assaults.exception.arguments[4].value=null",}, classes = {ChaosDemoApplication.class}) @ActiveProfiles("chaos-monkey") - static class ExceptionAssaultIntegrationTest { + class ExceptionAssaultIntegrationTest { @Autowired private DemoWebClientService demoWebClientService; @Test public void testWebClientExceptionAssault() { - try { - this.demoWebClientService.callWithWebClient(); - fail("No WebClientResponseException occurred!"); - } catch (WebClientResponseException ex) { - String body = ex.getResponseBodyAsString(); - assertThat(body).isEqualTo(ErrorClientResponse.ERROR_BODY); - } + + assertThatThrownBy(() -> this.demoWebClientService.callWithWebClient()).isInstanceOf(WebClientResponseException.class) + .has(new Condition<>((ex) -> ((WebClientResponseException) ex).getResponseBodyAsString().equals("Fail"), "Body equals fail")); } } - @Slf4j + @Nested @SpringBootTest(properties = {"chaos.monkey.enabled=true", "chaos.monkey.watcher.web-client=true", "chaos.monkey.assaults.latency-active=true", "chaos.monkey.test.rest-template.time-out=20"}, classes = {ChaosDemoApplication.class}) @ActiveProfiles("chaos-monkey") - static class LatencyAssaultIntegrationTest { + class LatencyAssaultIntegrationTest { @Autowired private DemoWebClientService demoWebClientService;