Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented ShadowingInterceptor #1258

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Target> 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.
* <p>
* 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.
* </p>
*
* @param targets a list of {@link Target} objects representing the shadow hosts.
*/
@MCChildElement
public void setTargets(List<Target> targets) {
this.targets = targets;
}

public List<Target> getTargets() {
return targets;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
41 changes: 41 additions & 0 deletions distribution/examples/shadowing/README.md
Original file line number Diff line number Diff line change
@@ -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
<api port="2000">
<shadowing>
<target host="localhost" port="3000" />
<target host="localhost" port="3001" />
<target host="localhost" port="3002" />
</shadowing>
<target host="api.predic8.de" port="443">
<ssl/>
</target>
</api>
```
35 changes: 35 additions & 0 deletions distribution/examples/shadowing/proxies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<spring:beans xmlns="http://membrane-soa.org/proxies/1/"
xmlns:spring="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd">

<router>

<api port="2000">
<shadowing>
<target host="localhost" port="3000" />
<target host="localhost" port="3001" />
<target host="localhost" port="3002" />
</shadowing>
<target host="api.predic8.de" port="443">
<ssl/>
</target>
</api>

<api port="3000">
<log />
<return statusCode="200"/>
</api>
<api port="3001">
<log />
<return statusCode="201"/>
</api>
<api port="3002">
<log />
<return statusCode="202"/>
</api>

</router>

</spring:beans>
18 changes: 18 additions & 0 deletions distribution/examples/shadowing/service-proxy.bat
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions distribution/examples/shadowing/service-proxy.sh
Original file line number Diff line number Diff line change
@@ -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

Loading