/*
* Copyright 2012 Nodeable 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.streamreduce.core.transformer.message;
import com.streamreduce.core.event.EventId;
import com.streamreduce.core.model.Event;
import com.streamreduce.core.model.messages.details.SobaMessageDetails;
import com.streamreduce.core.model.messages.details.nodebelly.NodebellyMessageDetails;
import com.streamreduce.core.model.messages.details.nodebelly.NodebellySummaryMessageDetails;
import com.streamreduce.util.MessageUtils;
import com.streamreduce.util.Pair;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* This is annoying right now because we have to build both the plain text string and MessageDetails object at the same time
*/
public class NodebellyMessageTransformer extends SobaMessageTransformer implements MessageTransformer {
protected transient Logger logger = LoggerFactory.getLogger(getClass());
private JSONObject metricConfig;
public NodebellyMessageTransformer(Properties messageProperties, SobaMessageDetails messageDetails, JSONObject metricConfig) {
super(messageProperties, messageDetails);
this.metricConfig = metricConfig;
}
/*
* This helper function grabs a subfield out of the JSON configuration
* for each metricname->RESOURCE_ID.METRIC_ID in METRIC_CONFIG_JSON.
*/
private String getMetricConfigValue(String metricName, Map<String, String> metricCriteria, String name) {
JSONObject item = (JSONObject) metricConfig.get(metricName);
if (item != null) {
String key2 = "";
if (metricCriteria != null && metricCriteria.containsKey("RESOURCE_ID") && metricCriteria.containsKey("METRIC_ID")) {
key2 = metricCriteria.get("RESOURCE_ID") + "." + metricCriteria.get("METRIC_ID");
}
item = (JSONObject) item.get(key2);
if (item != null) {
return item.getString(name);
}
}
logger.error("NB: metricNameMapping: miss: getMetricConfigValue:" + metricName + ", " + metricCriteria);
return null;
}
/*
* Wraps getMetricConfigValue() to return an empty non null string when null.
*/
private String getMetricConfigValueNotNull(String metricName, Map<String, String> metricCriteria, String name) {
String value = getMetricConfigValue(metricName, metricCriteria, name);
if(value == null)
value = "";
return value;
}
/*
* Formats the units string according to the metic and its value.
*/
protected Pair getUnitsLabel(String metricName, Map<String, String> metricCriteria, double value, boolean space) {
boolean plural = true;
if (value == 1.0) {
plural = false;
}
String unit = getMetricConfigValueNotNull(metricName, metricCriteria, (plural ? "units" : "unit"));
/*
* Divide down the value while stepping through incrementally
* larger units until it's no longer > 1K of the units.
*/
if (unit != null && unit.startsWith("Byte")) {
String prefixes = "KMGTPEZY";
for(int i = 0; i < prefixes.length(); i++) {
if (Math.abs(value / 1024.0) > 1.0) {
value = value / 1024.0;
unit = prefixes.substring(i, i+1) + "b";
}
}
}
if (space && unit.length() > 0 && !unit.equals("%")) {
unit = " " + unit;
}
return new Pair(value, unit);
}
@Override
public String doTransform(Event event) {
final EventId eventId = event.getEventId();
final Map<String, Object> meta = event.getMetadata();
final Date eventDate = new Date((Long) meta.get("timestamp"));
String title;
String details = "";
Map<String, String> topMetricCriteria = (Map<String, String>) meta.get("metricCriteria");
switch (eventId) {
case NODEBELLY_STATUS:
case NODEBELLY_SUMMARY:
String providerId = (String) meta.get("targetProviderId");
title = MessageFormat.format((String) messageProperties.get("message.nodebelly.summary"),
eventDate,
providerId);
// not all status/summary messages have been aggregated
if (meta.containsKey("items")) {
// make the names of each item human readable
List<Map<String, Object>> items = (List<Map<String, Object>>) meta.get("items");
boolean first = true;
for (Map<String, Object> item : items) {
Map<String, String> metricCriteria = (Map<String, String>) item.get("metricCriteria");
String metricName = (String) item.get("name");
float origValue = ((Number) item.get("value")).floatValue();
float origDiff = ((Number) item.get("diff")).floatValue();
Pair pair = getUnitsLabel(metricName, metricCriteria, ((Number) item.get("value")).doubleValue(), true);
item.put("value", ((Number)pair.first).floatValue());
Pair pairDiff = getUnitsLabel(metricName, metricCriteria, ((Number) item.get("diff")).doubleValue(), true);
item.put("diff", ((Number)pairDiff.first).floatValue());
/*
* Just render an explanation subheader for the first item since it
* has the highest stddev and will be selected by the client
*/
if (first) {
first = false;
String explanation = getMetricConfigValueNotNull(metricName, metricCriteria, "explanation");
double previous = origValue - origDiff;
if (metricName.equals("CONNECTION_RESOURCE_USAGE") && metricCriteria.containsKey("RESOURCE_ID")) { // TODO hack for IMG
explanation = metricCriteria.get("RESOURCE_ID") + " was previously at %.2f and is now at %.2f";
}
if (explanation.length() > 0) {
explanation = MessageFormat.format(explanation, String.valueOf(previous), String.valueOf(pair.first) );
}
item.put("subheader", explanation);
}
item.put("name", metricTypeNameReadable(metricName, metricCriteria));
item.put("metricname", metricName); // needed for debugging
item.put("unit", pair.second);
}
HashMap<String, Object> structure = new HashMap<>();
structure.put("accountId", meta.get("account"));
structure.put("total", meta.get("total"));
structure.put("diff", meta.get("diff"));
structure.put("type", providerId);
structure.put("items", items); // add the updated items
structure.put("granularity", meta.get("granularity"));
// set rich formatting properties
// the client will render the table in "structure" how it wants to
messageDetails = new NodebellySummaryMessageDetails.Builder()
.title(title)
.structure(structure)
.build();
// just print the key/value pairs for the plain text version
details = ((NodebellySummaryMessageDetails) messageDetails).toPlainText();
}
break;
case NODEBELLY_ANOMALY:
String connectionName = "";
if (meta.containsKey("targetConnectionAlias")) {
connectionName = ((String) meta.get("targetConnectionAlias"));
}
String inventoryName = "";
if (meta.containsKey("targetAlias")) {
inventoryName = " for " + ((String) meta.get("targetAlias"));
}
providerId = (String) meta.get("targetProviderId");
String metricName = (String) meta.get("name");
float fValue = ((Number) meta.get("value")).floatValue();
float fMean = ((Number) meta.get("mean")).floatValue();
float fStddev = ((Number) meta.get("stddev")).floatValue();
int nStdDev = Double.valueOf(Math.floor(Math.abs(fValue - fMean) / fStddev)).intValue();
Pair pair1 = getUnitsLabel(metricName, topMetricCriteria, fValue, true);
//Pair pair2 = getUnitsLabel(metricName, topMetricCriteria, fStddev, true);
Pair pair3 = getUnitsLabel(metricName, topMetricCriteria, fMean, true);
int severity = getSeverityLevel(fValue, fMean, fStddev);
title = MessageFormat.format((String) messageProperties.get("message.nodebelly.anomalyseverity" + severity),
metricTypeNameReadable(metricName, topMetricCriteria),
connectionName);
String numericDetails = MessageFormat.format((String) messageProperties.get("message.nodebelly.anomaly.numericdetails"),
MessageUtils.roundAndTruncate(((Number) pair1.first).doubleValue(), 2),
pair1.second,
nStdDev,
MessageUtils.roundAndTruncate(((Number) pair3.first).doubleValue(), 2),
pair3.second);
if (nStdDev > 25) {
numericDetails = MessageFormat.format((String) messageProperties.get("message.nodebelly.anomaly.numericdetailsBig"),
MessageUtils.roundAndTruncate(((Number) pair1.first).doubleValue(), 2),
pair1.second,
MessageUtils.roundAndTruncate(((Number) pair3.first).doubleValue(), 2),
pair3.second);
}
details = MessageFormat.format((String) messageProperties.get("message.nodebelly.anomaly.details"),
metricTypeNameReadable(metricName, topMetricCriteria),
numericDetails,
inventoryName);
HashMap<String, Object> structure = new HashMap<>();
structure.put("accountId", meta.get("account"));
structure.put("name", metricTypeNameReadable(metricName, topMetricCriteria));
structure.put("metricName", metricName);
structure.put("metricCriteria", topMetricCriteria);
structure.put("granularity", meta.get("granularity"));
structure.put("value", meta.get("value"));
structure.put("mean", meta.get("mean"));
structure.put("stddev", meta.get("stddev"));
structure.put("min", meta.get("min"));
structure.put("max", meta.get("max"));
structure.put("diff", meta.get("diff"));
structure.put("unit", pair1.first);
// set rich formatting properties
// the client will render the table in "structure" how it wants to
messageDetails = new NodebellyMessageDetails.Builder()
.title(title)
.details(details)
.structure(structure)
.build();
break;
default:
// there really isn't this....
title = super.doTransform(event);
}
// this is the plain text version, the rich message is set in the MessageDetails object
return title + " " + details;
}
/*
* Used for indexing into the correct headline for anomaly messages.
* The returned severity is 0, 1 or 2
*/
private int getSeverityLevel(float value, float mean, float stddev) {
if(Math.abs(mean - value) > (stddev * 3))
return 2;
if(Math.abs(mean - value) > (stddev * 2))
return 1;
return 0;
}
/*
* Make friendly names for the following. The rest are being
* blacklisted by JuggaloaderMessageGeneratorBolt
*
* INVENTORY_ITEM_RESOURCE_USAGE.ID.CPUUtilization.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.DiskReadBytes.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.DiskReadOps.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.WriteReadBytes.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.DiskWriteOps.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.NetworkIn.average
* INVENTORY_ITEM_RESOURCE_USAGE.ID.NetworkOut.average
* CONNECTION_ACTIVITY_COUNT.ID
* INVENTORY_ITEM_COUNT.ID.total
* USER_COUNT
*
*/
private String metricTypeNameReadable(String metricName, Map<String, String> metricCriteria) {
// IMG Connections
// TODO how do we handle explanation text and units?
if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && "gateway".equals(metricCriteria.get("PROVIDER_TYPE")))
return "Custom Connection Activity";
if (metricName.equals("CONNECTION_RESOURCE_USAGE") && metricCriteria.containsKey("RESOURCE_ID"))
return metricCriteria.get("RESOURCE_ID");
// Appcelerator hack. Don't remove unless you clear it with @NJH.
if (metricCriteria.containsKey("RESOURCE_ID") && (metricCriteria.get("RESOURCE_ID").startsWith("cloud.") || metricCriteria.get("RESOURCE_ID").startsWith("ti."))) {
return metricCriteria.get("RESOURCE_ID");
}
String nameFromJson = getMetricConfigValue(metricName, metricCriteria, "name");
if (nameFromJson != null)
return nameFromJson;
if (metricName.startsWith("ACCOUNT_COUNT")) {
metricName = "Account Count";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && "rss".equals(metricCriteria.get("PROVIDER_ID"))) {
metricName = "RSS Activity";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && "github".equals(metricCriteria.get("PROVIDER_ID"))) {
metricName = "Github Activity";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && "jira".equals(metricCriteria.get("PROVIDER_ID"))) {
metricName = "Jira Activity";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && "aws".equals(metricCriteria.get("PROVIDER_ID"))) {
metricName = "AWS Activity";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT") && metricCriteria.containsKey("CONNECTION_ID")) {
metricName = "Connection Activity";
} else if (metricName.equals("CONNECTION_ACTIVITY_COUNT")) {
metricName = "Account Level Connection Activity";
} else if (metricName.equals("CONNECTION_COUNT")) {
metricName = "Connection Count";
} else if (metricName.equals("INVENTORY_ITEM_COUNT")) {
metricName = "Inventory Item Count";
} else if (metricName.equals("INVENTORY_ITEM_ACTIVITY_COUNT")) {
metricName = "Inventory Activity Count";
} else if (metricName.equals("PENDING_USER_COUNT")) {
metricName = "Pending User Count";
} else if (metricName.equals("USER_COUNT")) {
metricName = "User Count";
}
logger.error("NB: metricNameMapping: miss: metricTypeNameReadable:" + metricName + ", " + metricCriteria);
return metricName;
}
}