Code Integration
Get a 'GremlinService' instance
The main class you will interact with is com.gremlin.GremlinService
. This class abstracts all of the machinery to register with the Gremlin API, find and cache experiments, and report success back to the Gremlin API. An instance of this class is used in each place in your application where you would like to have the option of creating an attack.
IMPORTANT : This class is designed to be a singleton.
To create an instance of this class manually, create a GremlinServiceFactory
, then get the GremlinService
from that:
import com.gremlin.GremlinService;
import com.gremlin.GremlinServiceFactory;
private final GremlinServiceFactory gremlinServiceFactory = new GremlinServiceFactory();
private final GremlinService gremlinService = gremlinServiceFactory.getService();
To enforce the singleton-ness of this object, you may also use dependency-injection (DI). The following example uses hk2, but any library can represent this object graph.
import com.gremlin.GremlinService;
import com.gremlin.GremlinServiceFactory;
import org.glassfish.hk2.api.Factory;
import org.jvnet.hk2.annotations.Service;
import javax.inject.Inject;
@Service
public class GremlinHk2ServiceFactory extends GremlinServiceFactory implements Factory<GremlinService> {
// Cache `GremlinService` as a singleton here
private final GremlinService gremlinServiceSingleton;
@Inject
public GremlinHk2ServiceFactory() {
super();
this.gremlinServiceSingleton = this.getGremlinService();
}
@Override
public GremlinService provide() {
return gremlinServiceSingleton;
}
@Override
public void dispose(GremlinService instance) {
}
}
import com.gremlin.GremlinService;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import javax.inject.Singleton;
public class GremlinDiBinder extends AbstractBinder {
@Override
protected void configure() {
bind(GremlinHk2ServiceFactory.class).to(GremlinHk2ServiceFactory.class).in(Singleton.class);
bindFactory(GremlinHk2ServiceFactory.class).to(GremlinService.class).in(Singleton.class);
}
}
Initialize ApplicationCoordinates
An important concept in ALFI is that each application has a set of identifying attributes. This set of attributes is named ApplicationCoordinates and is used to determine when an application matches an attack. Some example sets of ApplicationCoordinates are:
{"type"="AwsLambda", "region"="us-west-1", "name"="event-handler"}
{"type"="MyServiceType", "region"="us-east-1", "service"="recommendations", "criticality"="2", "userfacing"="true"}
alfi-aws
includes integrations for running on AWS Lambda and EC2. In the case of AWS Lambda, the attributes type=AwsLambda
, name
, and region
can be set for you. In the case of AWS EC2, the attributes type=AwsEc2
, region
, az
, and instanceId
can be set for you. These attributes are inferred from the environment using AwsApplicationCoordinatesResolver.inferFromEnvironment()
.
If you have other facets of your application that would be useful for targeting, you may also create your own attributes. To do so, you need a subclass of GremlinCoordinatesProvider
. This abstract class has 2 methods. To create your own ApplicationCoordinates
, override initializeApplicationCoordinates()
. The auto-generated ApplicationCoordinates
(if any) are supplied as an argument to this method, so you can append to those if they exist. Here is an example of creating your own ApplicationCoordinates
:
import com.gremlin.ApplicationCoordinates;
import com.gremlin.GremlinCoordinatesProvider;
public class MyCoordinatesProvider extends GremlinCoordinatesProvider {
@Override
public ApplicationCoordinates initializeApplicationCoordinates() {
return AwsApplicationCoordinatesResolver.inferFromEnvironment().map(c -> {
c.putField("userfacing", "true");
return c;
}).orElseGet(() -> new ApplicationCoordinates.Builder()
.withType("MyServiceType")
.withField("name", "recommendations")
.withField("userfacing", "true")
.build());
}
}
This set of ApplicationCoordinates
are then used to match attacks. So if you create an attack that matches userfacing=true
, this application will be included in the attack. This is how you can start narrowing down the scope of your attack to only those applications which are useful in your scenario.
Configure TrafficCoordinates
Once you've described your application in terms of ApplicationCoordinates
, you can then start targeting individual requests within your application. This will allow you to further refine your attack. Any piece of data may be used as a facet, and therefore as something to match on when constructing an attack. Here are some example TrafficCoordinates
: {"type": "OutboundHttp", "name": "customer-api", "verb": "GET", "customerId": 456}
and {"type": "AwsDynamo", "table": "AuditLogs", "operation": "PutItem", "deviceType": "iPad"}
Gremlin provides a way to automatically construct TrafficCoordinates
for common request types, as well as a way to create your own from scratch.
Apache HTTP Client TrafficCoordinates
Here is an example of integrating fault-injection around Apache HTTP Client. This will populate TrafficCoordinates
with "type" = "OutboundHttp", "verb", and "clientName".
import com.gremlin.http.client.GremlinApacheHttpRequestInterceptor;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
final HttpRequestInterceptor gremlinHttpInterceptor =
new GremlinApacheHttpRequestInterceptor(gremlinService, "sample");
final HttpClient outgoingHttpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(RequestConfig
.custom()
.setConnectTimeout(500)
.setSocketTimeout(1000)
.build()
)
.addInterceptorLast(gremlinHttpInterceptor)
.build();
DynamoDB TrafficCoordinates
Here is an example of integrating fault-injection around Dynamo DB. This will populate TrafficCoordinates
with "type" = "AwsDynamo", "table", and "operation".
import com.amazonaws.ClientConfiguration;
import com.amazonaws.handlers.RequestHandler2;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.gremlin.db.dynamo.client.GremlinDynamoRequestInterceptor;
final RequestHandler2 gremlinDynamoInterceptor = new GremlinDynamoRequestInterceptor(gremlinService, 1500, 500));
final AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard()
.withRegion(region.getName())
.withClientConfiguration(new ClientConfiguration()
.withClientExecutionTimeout(1500)
.withConnectionTimeout(500)
.withMaxErrorRetry(2)
)
.withRequestHandlers(gremlinDynamoInterceptor)
);
Custom TrafficCoordinates
You may also create your own TrafficCoordinates
from scratch. In this case, you completely define the type of the TrafficCoordinates
and all attributes. For this code example, assume that the code looks like this prior to Gremlin integration:
final Customer leader = redisClient.getLeader(contestId);
You may want to simulate failures of the Redis client per-contest. That way, you could verify how your UI/monitoring tools/operators react to this situation. To accomplish that, a Gremlin integration would look like this:
import com.gremlin.TrafficCoordinates;
final TrafficCoordinates coordinates = new TrafficCoordinates.Builder()
.withType("Redis")
.withField("callType", "getLeader")
.withField("contestId", contestId)
.build();
final Customer leader = this.svc.execute(coordinates, () -> redisClient.getLeader(contestId));
Once you have written that code and deployed it, you may create attacks in the UI that fail specific Redis calls and pick out specific contestId
s.
Extend TrafficCoordinates with request-level attributes
Often, companies set up their infrastructure to maintain a per-request data structure and use this information to provide logging, monitoring, and observability data points. A common pattern is to set up a RequestContext
and have authentication filters put in information like customerId
or deviceId
into the RequestContext
object. This object then permits access from any later point, so that those attributes are easily available. These are often excellent facets on which to create attacks. If your system operates in this way, then you can set up a mapping to populate these values on all TrafficCoordinates
. This code lives in a concrete subclass of GremlinCoordinatesProvider
, which you've already seen in: Initialize Application Coordinates.
import com.gremlin.GremlinCoordinatesProvider;
import com.gremlin.TrafficCoordinates;
public class MyCoordinatesProvider extends GremlinCoordinatesProvider {
@Override
public TrafficCoordinates extendEachTrafficCoordinates(TrafficCoordinates incomingCoordinates) {
incomingCoordinates.putField("customerId", MyRequestContext.getCustomerId());
incomingCoordinates.putField("deviceId", MyRequestContext.getDeviceId());
incomingCoordinates.putField("country", MyRequestContext.getCountry());
return incomingCoordinates;
}
}
With this code wired into the construction of your GremlinService
instance, all TrafficCoordinates
will now get those 3 attributes and they are eligible to be matched for any type of traffic you'd like to attack.
Alternate configuration mechanism
As described above, the default configuration resolution mechanism is to use either properties defined in gremlin.properties
, or in environment variables where your application runs. If those don't fit your needs, then you can provide an alterate mechanism by subclassing GremlinConfigurationResolver
and supplying it to GremlinServiceFactory
at construction-time.