/*
*
* Copyright 2012 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.simianarmy.aws;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.CreateDomainRequest;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.ListDomainsResult;
import com.amazonaws.services.simpledb.model.PutAttributesRequest;
import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
import com.amazonaws.services.simpledb.model.SelectRequest;
import com.amazonaws.services.simpledb.model.SelectResult;
import com.netflix.simianarmy.EventType;
import com.netflix.simianarmy.MonkeyRecorder;
import com.netflix.simianarmy.MonkeyType;
import com.netflix.simianarmy.NamedType;
import com.netflix.simianarmy.basic.BasicRecorderEvent;
import com.netflix.simianarmy.client.aws.AWSClient;
/**
* The Class SimpleDBRecorder. Records events to and fetched events from a Amazon SimpleDB table (default SIMIAN_ARMY)
*/
@SuppressWarnings("serial")
public class SimpleDBRecorder implements MonkeyRecorder {
/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBRecorder.class);
private final AmazonSimpleDB simpleDBClient;
private final String region;
/** The domain. */
private final String domain;
/**
* The Enum Keys.
*/
private enum Keys {
/** The event id. */
id,
/** The event time. */
eventTime,
/** The region. */
region,
/** The record type. */
recordType,
/** The monkey type. */
monkeyType,
/** The event type. */
eventType;
/** The Constant KEYSET. */
public static final Set<String> KEYSET = Collections.unmodifiableSet(new HashSet<String>() {
{
for (Keys k : Keys.values()) {
add(k.toString());
}
}
});
};
/**
* Instantiates a new simple db recorder.
*
* @param awsClient
* the AWS client
* @param domain
* the domain
*/
public SimpleDBRecorder(AWSClient awsClient, String domain) {
Validate.notNull(awsClient);
Validate.notNull(domain);
this.simpleDBClient = awsClient.sdbClient();
this.region = awsClient.region();
this.domain = domain;
}
/**
* simple client. abstracted to aid testing
*
* @return the amazon simple db
*/
protected AmazonSimpleDB sdbClient() {
return simpleDBClient;
}
/**
* Enum to value. Converts an enum to "name|type" string
*
* @param e
* the e
* @return the string
*/
private static String enumToValue(NamedType e) {
return String.format("%s|%s", e.name(), e.getClass().getName());
}
/**
* Value to enum. Converts a "name|type" string back to an enum.
*
* @param value
* the value
* @return the enum
*/
private static <T extends NamedType> T valueToEnum(
Class<T> type, String value) {
// parts = [enum value, enum class type]
String[] parts = value.split("\\|", 2);
if (parts.length < 2) {
throw new RuntimeException("value " + value + " does not appear to be an internal enum format");
}
Class<?> enumClass;
try {
enumClass = Class.forName(parts[1]);
} catch (ClassNotFoundException e) {
throw new RuntimeException("class for enum value " + value + " not found");
}
if (!enumClass.isEnum()) {
throw new RuntimeException("value " + value + " does not appear to be of an enum type");
}
if (!type.isAssignableFrom(enumClass)) {
throw new RuntimeException("value " + value + " cannot be assigned to a variable of this type: "
+ type.getCanonicalName());
}
@SuppressWarnings("rawtypes")
Class<? extends Enum> enumType = enumClass.asSubclass(Enum.class);
@SuppressWarnings("unchecked")
T enumValue = (T) Enum.valueOf(enumType, parts[0]);
return enumValue;
}
/** {@inheritDoc} */
@Override
public Event newEvent(MonkeyType monkeyType, EventType eventType, String reg, String id) {
return new BasicRecorderEvent(monkeyType, eventType, reg, id);
}
/** {@inheritDoc} */
@Override
public void recordEvent(Event evt) {
String evtTime = String.valueOf(evt.eventTime().getTime());
List<ReplaceableAttribute> attrs = new LinkedList<ReplaceableAttribute>();
attrs.add(new ReplaceableAttribute(Keys.id.name(), evt.id(), true));
attrs.add(new ReplaceableAttribute(Keys.eventTime.name(), evtTime, true));
attrs.add(new ReplaceableAttribute(Keys.region.name(), evt.region(), true));
attrs.add(new ReplaceableAttribute(Keys.recordType.name(), "MonkeyEvent", true));
attrs.add(new ReplaceableAttribute(Keys.monkeyType.name(), enumToValue(evt.monkeyType()), true));
attrs.add(new ReplaceableAttribute(Keys.eventType.name(), enumToValue(evt.eventType()), true));
for (Map.Entry<String, String> pair : evt.fields().entrySet()) {
if (pair.getValue() == null || pair.getValue().equals("") || Keys.KEYSET.contains(pair.getKey())) {
continue;
}
attrs.add(new ReplaceableAttribute(pair.getKey(), pair.getValue(), true));
}
// Let pk contain the timestamp so that the same resource can have multiple events.
String pk = String.format("%s-%s-%s-%s", evt.monkeyType().name(), evt.id(), region, evtTime);
PutAttributesRequest putReq = new PutAttributesRequest(domain, pk, attrs);
sdbClient().putAttributes(putReq);
}
/**
* Find events.
*
* @param queryMap
* the query map
* @param after
* the start time to query for all events after
* @return the list
*/
protected List<Event> findEvents(Map<String, String> queryMap, long after) {
StringBuilder query = new StringBuilder(
String.format("select * from `%s` where region = '%s'", domain, region));
for (Map.Entry<String, String> pair : queryMap.entrySet()) {
query.append(String.format(" and %s = '%s'", pair.getKey(), pair.getValue()));
}
query.append(String.format(" and eventTime > '%d'", after));
// always return with most recent record first
query.append(" order by eventTime desc");
List<Event> list = new LinkedList<Event>();
SelectRequest request = new SelectRequest(query.toString());
request.setConsistentRead(Boolean.TRUE);
SelectResult result = new SelectResult();
do {
result = sdbClient().select(request.withNextToken(result.getNextToken()));
for (Item item : result.getItems()) {
Map<String, String> fields = new HashMap<String, String>();
Map<String, String> res = new HashMap<String, String>();
for (Attribute attr : item.getAttributes()) {
if (Keys.KEYSET.contains(attr.getName())) {
res.put(attr.getName(), attr.getValue());
} else {
fields.put(attr.getName(), attr.getValue());
}
}
String eid = res.get(Keys.id.name());
String ereg = res.get(Keys.region.name());
MonkeyType monkeyType = valueToEnum(MonkeyType.class, res.get(Keys.monkeyType.name()));
EventType eventType = valueToEnum(EventType.class, res.get(Keys.eventType.name()));
long eventTime = Long.parseLong(res.get(Keys.eventTime.name()));
list.add(new BasicRecorderEvent(monkeyType, eventType, ereg, eid, eventTime).addFields(fields));
}
} while (result.getNextToken() != null);
return list;
}
/** {@inheritDoc} */
@Override
public List<Event> findEvents(Map<String, String> query, Date after) {
return findEvents(query, after.getTime());
}
/** {@inheritDoc} */
@Override
public List<Event> findEvents(MonkeyType monkeyType, Map<String, String> query, Date after) {
Map<String, String> copy = new LinkedHashMap<String, String>(query);
copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));
return findEvents(copy, after);
}
/** {@inheritDoc} */
@Override
public List<Event> findEvents(MonkeyType monkeyType, EventType eventType, Map<String, String> query, Date after) {
Map<String, String> copy = new LinkedHashMap<String, String>(query);
copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));
copy.put(Keys.eventType.name(), enumToValue(eventType));
return findEvents(copy, after);
}
/**
* Creates the SimpleDB domain, if it does not already exist.
*/
public void init() {
try {
if (this.region == null || this.region.equals("region-null")) {
// This is a mock with an invalid region; avoid a slow timeout
LOGGER.debug("Region=null; skipping SimpleDB domain creation");
return;
}
ListDomainsResult listDomains = sdbClient().listDomains();
for (String d : listDomains.getDomainNames()) {
if (d.equals(domain)) {
LOGGER.debug("SimpleDB domain found: {}", domain);
return;
}
}
LOGGER.info("Creating SimpleDB domain: {}", domain);
CreateDomainRequest createDomainRequest = new CreateDomainRequest(
domain);
sdbClient().createDomain(createDomainRequest);
} catch (AmazonClientException e) {
LOGGER.warn("Error while trying to auto-create SimpleDB domain", e);
}
}
}