diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptor.java new file mode 100644 index 0000000000..e0196c56ce --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptor.java @@ -0,0 +1,121 @@ +package com.predic8.membrane.core.interceptor.shadowing; + +import com.predic8.membrane.annot.MCChildElement; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.AbstractBody; +import com.predic8.membrane.core.http.Chunk; +import com.predic8.membrane.core.http.MessageObserver; +import com.predic8.membrane.core.http.Request; +import com.predic8.membrane.core.interceptor.AbstractInterceptor; +import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.rules.AbstractServiceProxy.Target; +import com.predic8.membrane.core.transport.http.HttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; +import static java.util.concurrent.Executors.newCachedThreadPool; + +@MCElement(name="shadowing") +public class ShadowingInterceptor extends AbstractInterceptor { + + private static final HttpClient client = new HttpClient(); + private static final Logger log = LoggerFactory.getLogger(ShadowingInterceptor.class); + + private List targets = new ArrayList<>(); + + @Override + public Outcome handleRequest(Exchange exc) throws Exception { + exc.getRequest().getBody().getObservers().add(new MessageObserver() { + @Override + public void bodyRequested(AbstractBody body) {} + @Override + public void bodyChunk(Chunk chunk) {} + @Override + public void bodyChunk(byte[] buffer, int offset, int length) {} + + @Override + public void bodyComplete(AbstractBody body) { + cloneRequestAndSend(body); + } + }); + return CONTINUE; + } + + @Override + public String getShortDescription() { + return "Sends requests to shadow hosts (processed in the background)."; + } + + public void cloneRequestAndSend(AbstractBody body) { + ExecutorService executor = newCachedThreadPool(); + for (Target target : targets) { + Exchange exc; + try { + exc = new Request.Builder() + .body(body.getContent()) + .get(getDestFromTarget(target, router.getParentProxy(this).getKey().getPath())) + .buildExchange(); + } catch (Exception e) { + log.error("Error creating request for target {}", target, e); + continue; + } + + executor.submit(() -> { + try { + Exchange res = performCall(exc); + if (res.getResponse().getStatusCode() >= 500) + log.info("{} returned StatusCode {}", res.getDestinations().get(0), res.getResponse().getStatusCode()); + } catch (Exception e) { + log.error("Error performing call for target {}", target, e); + } + }); + } + } + + + static String getDestFromTarget(Target t, String path) { + return (t.getUrl() != null) ? t.getUrl() : extracted(t, path); + } + + @SuppressWarnings("HttpUrlsUsage") + private static String extracted(Target t, String path) { + return ((t.getSslParser() != null) ? "https://" : "http://") + + t.getHost() + + ":" + + t.getPort() + + (path != null ? path : ""); + } + + static Exchange performCall(Exchange exchange) { + try { + return client.call(exchange); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the list of shadow hosts to which requests will be cloned and sent. + *

+ * Each target in the list represents a shadow host where the request will be forwarded. + * These shadow hosts are processed in the background, and if a response from any shadow host + * contains a 5XX status code, it will be logged. + *

+ * + * @param targets a list of {@link Target} objects representing the shadow hosts. + */ + @MCChildElement + public void setTargets(List targets) { + this.targets = targets; + } + + public List getTargets() { + return targets; + } +} diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptorTest.java new file mode 100644 index 0000000000..6ed225b897 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptorTest.java @@ -0,0 +1,37 @@ +package com.predic8.membrane.core.interceptor.shadowing; + +import com.predic8.membrane.core.rules.AbstractServiceProxy.Target; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class ShadowingInterceptorTest { + + @Mock + private Target mockTarget; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetDestFromTarget_WithUrl() { + when(mockTarget.getUrl()).thenReturn("http://example.com"); + String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path"); + assertEquals("http://example.com", result); + } + + @Test + void testGetDestFromTarget_WithoutUrl() { + when(mockTarget.getHost()).thenReturn("localhost"); + when(mockTarget.getPort()).thenReturn(8080); + String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path"); + assertEquals("http://localhost:8080/path", result); + } + +} diff --git a/distribution/examples/shadowing/README.md b/distribution/examples/shadowing/README.md new file mode 100644 index 0000000000..8834a872a5 --- /dev/null +++ b/distribution/examples/shadowing/README.md @@ -0,0 +1,41 @@ +# Shadowing Interceptor + +This example demonstrates how to send requests to multiple shadow hosts. A request is sent to the primary target, with additional requests concurrently sent to shadow hosts. The response from the primary target is returned immediately, while shadow requests are processed in the background. + +## Running the Example + +1. Run `service-proxy.bat` or `service-proxy.sh` +2. Open [localhost:2000](http://localhost:2000) in your browser or use `curl`: + + ``` + curl -v http://localhost:2000 + ``` + + The output should look like this: + + ```json + { + "apis": [ + { + "name": "Shop API Showcase", + "description":"API for REST exploration, test and demonstration. Feel free to manipulate the resources using the POST, PUT and DELETE methods. This API acts as a showcase for REST API design.", + "url":"/shop/v2/" + } + ] + } + ``` + +## Configuration +The targets specified in `shadowing` are shadow hosts. Responses from these hosts are ignored; however, if the returned status code is a 5XX, the endpoint that generated this response is logged. +```xml + + + + + + + + + + +``` \ No newline at end of file diff --git a/distribution/examples/shadowing/proxies.xml b/distribution/examples/shadowing/proxies.xml new file mode 100644 index 0000000000..fadf02754d --- /dev/null +++ b/distribution/examples/shadowing/proxies.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/examples/shadowing/service-proxy.bat b/distribution/examples/shadowing/service-proxy.bat new file mode 100644 index 0000000000..d2dec51288 --- /dev/null +++ b/distribution/examples/shadowing/service-proxy.bat @@ -0,0 +1,18 @@ +@echo off +if not "%MEMBRANE_HOME%" == "" goto homeSet +set "MEMBRANE_HOME=%cd%\..\.." +echo "%MEMBRANE_HOME%" +if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk + +:homeSet +if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk +echo Please set the MEMBRANE_HOME environment variable to point to +echo the directory where you have extracted the Membrane software. +exit + +:homeOk +set "CLASSPATH=%MEMBRANE_HOME%" +set "CLASSPATH=%MEMBRANE_HOME%/conf" +set "CLASSPATH=%CLASSPATH%;%MEMBRANE_HOME%/starter.jar" +echo Membrane Router running... +java -classpath "%CLASSPATH%" com.predic8.membrane.core.Starter -c proxies.xml \ No newline at end of file diff --git a/distribution/examples/shadowing/service-proxy.sh b/distribution/examples/shadowing/service-proxy.sh new file mode 100644 index 0000000000..132618c349 --- /dev/null +++ b/distribution/examples/shadowing/service-proxy.sh @@ -0,0 +1,35 @@ +#!/bin/bash +homeSet() { + echo "MEMBRANE_HOME variable is now set" + CLASSPATH="$MEMBRANE_HOME/conf" + CLASSPATH="$CLASSPATH:$MEMBRANE_HOME/starter.jar" + export CLASSPATH + echo Membrane Router running... + java -classpath "$CLASSPATH" com.predic8.membrane.core.Starter -c proxies.xml + +} + +terminate() { + echo "Starting of Membrane Router failed." + echo "Please execute this script from the appropriate subfolder of MEMBRANE_HOME/examples/" + +} + +homeNotSet() { + echo "MEMBRANE_HOME variable is not set" + + if [ -f "`pwd`/../../starter.jar" ] + then + export MEMBRANE_HOME="`pwd`/../.." + homeSet + else + terminate + fi +} + + +if [ "$MEMBRANE_HOME" ] + then homeSet + else homeNotSet +fi +