/**
* The MIT License
* Copyright © 2010 JmxTrans team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.googlecode.jmxtrans.model.output.support.opentsdb;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.googlecode.jmxtrans.exceptions.LifecycleException;
import com.googlecode.jmxtrans.model.NamingStrategy;
import com.googlecode.jmxtrans.model.Result;
import com.googlecode.jmxtrans.model.Server;
import com.googlecode.jmxtrans.model.naming.ClassAttributeNamingStrategy;
import com.googlecode.jmxtrans.model.naming.JexlNamingStrategy;
import com.googlecode.jmxtrans.model.naming.typename.TypeNameValue;
import com.googlecode.jmxtrans.model.naming.typename.TypeNameValuesStringBuilder;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.apache.commons.jexl2.JexlException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static com.google.common.collect.FluentIterable.from;
import static com.googlecode.jmxtrans.util.NumberUtils.isNumeric;
@EqualsAndHashCode
@ToString
public class OpenTSDBMessageFormatter {
private static final Logger log = LoggerFactory.getLogger(OpenTSDBMessageFormatter.class);
public static final String DEFAULT_TAG_NAME = "type";
private final ImmutableList<String> typeNames;
private final ImmutableMap<String, String> tags;
private final String tagName;
private final NamingStrategy metricNameStrategy;
private final boolean mergeTypeNamesTags;
private final boolean hostnameTag;
public OpenTSDBMessageFormatter(@Nonnull ImmutableList<String> typeNames,
@Nonnull ImmutableMap<String, String> tags) throws LifecycleException {
this(typeNames, tags, DEFAULT_TAG_NAME, null, true, true);
}
public OpenTSDBMessageFormatter(@Nonnull ImmutableList<String> typeNames,
@Nonnull ImmutableMap<String, String> tags,
@Nonnull String tagName,
@Nullable String metricNamingExpression,
boolean mergeTypeNamesTags,
boolean hostnameTag) throws LifecycleException {
this.typeNames = typeNames;
this.tags = tags;
this.tagName = tagName;
if (metricNamingExpression != null) {
try {
metricNameStrategy = new JexlNamingStrategy(metricNamingExpression);
} catch (JexlException jexlExc) {
throw new LifecycleException("failed to setup naming strategy", jexlExc);
}
} else {
metricNameStrategy = new ClassAttributeNamingStrategy();
}
this.mergeTypeNamesTags = mergeTypeNamesTags;
this.hostnameTag = hostnameTag;
}
/**
* Add tags to the given result string, including a "host" tag with the name of the server and all of the tags
* defined in the "settings" entry in the configuration file within the "tag" element.
*
* @param resultString - the string containing the metric name, timestamp, value, and possibly other content.
*/
void addTags(StringBuilder resultString, Server server) {
if (hostnameTag) {
addTag(resultString, "host", server.getLabel());
}
// Add the constant tag names and values.
for (Map.Entry<String, String> tagEntry : tags.entrySet()) {
addTag(resultString, tagEntry.getKey(), tagEntry.getValue());
}
}
/**
* Add one tag, with the provided name and value, to the given result string.
*
* @param resultString - the string containing the metric name, timestamp, value, and possibly other content.
* @return String - the new result string with the tag appended.
*/
void addTag(StringBuilder resultString, String tagName, String tagValue) {
resultString.append(" ");
resultString.append(sanitizeString(tagName));
resultString.append("=");
resultString.append(sanitizeString(tagValue));
}
/**
* Format the result string given the class name and attribute name of the source value, the timestamp, and the
* value.
*
* @param epoch - the timestamp of the metric.
* @param value - value of the attribute to use as the metric value.
* @return String - the formatted result string.
*/
private void formatResultString(StringBuilder resultString, String metricName, long epoch, Object value) {
resultString.append(sanitizeString(metricName));
resultString.append(" ");
resultString.append(Long.toString(epoch));
resultString.append(" ");
resultString.append(sanitizeString(value.toString()));
}
/**
* Parse one of the results of a Query and return a list of strings containing metric details ready for sending to
* OpenTSDB.
*
* @param result - one results from the Query.
* @param server - Server object for importing hostname
* @return List<String> - the list of strings containing metric details ready for sending to OpenTSDB.
*/
/*
private List<String> formatResult(Result result) {
return this.formatResult(result, null);
}
*/
private List<String> formatResult(Result result, Server server) {
List<String> resultStrings = new LinkedList<>();
Map<String, Object> values = result.getValues();
String attributeName = result.getAttributeName();
if (values.containsKey(attributeName) && values.size() == 1) {
processOneMetric(resultStrings, server, result, values.get(attributeName), null, null);
} else {
for (Map.Entry<String, Object> valueEntry : values.entrySet()) {
processOneMetric(resultStrings, server, result, valueEntry.getValue(), tagName, valueEntry.getKey());
}
}
return resultStrings;
}
public Iterable<String> formatResults(Iterable<Result> results, final Server server) {
return from(results).transformAndConcat(new Function<Result, List<String>>() {
@Override
public List<String> apply(Result input) {
return formatResult(input, server);
}
}).toList();
}
/**
* Process a single metric from the given JMX query result with the specified value.
*/
protected void processOneMetric(List<String> resultStrings, Server server, Result result, Object value, String addTagName,
String addTagValue) {
String metricName = this.metricNameStrategy.formatName(result);
//
// Skip any non-numeric values since OpenTSDB only supports numeric metrics.
//
if (isNumeric(value)) {
StringBuilder resultString = new StringBuilder();
formatResultString(resultString, metricName, result.getEpoch() / 1000L, value);
addTags(resultString, server);
if (addTagName != null) {
addTag(resultString, addTagName, addTagValue);
}
if (!typeNames.isEmpty()) {
this.addTypeNamesTags(resultString, result);
}
resultStrings.add(resultString.toString());
} else {
log.debug("Skipping non-numeric value for metric {}; value={}", metricName, value);
}
}
/**
* Add the tag(s) for typeNames.
*
* @param result - the result of the JMX query.
* @param resultString - current form of the metric string.
* @return String - the updated metric string with the necessary tag(s) added.
*/
protected void addTypeNamesTags(StringBuilder resultString, Result result) {
if (mergeTypeNamesTags) {
// Produce a single tag with all the TypeName keys concatenated and all the values joined with '_'.
String typeNameValues = TypeNameValuesStringBuilder.getDefaultBuilder().build(typeNames, result.getTypeName());
addTag(resultString, StringUtils.join(typeNames, ""), typeNameValues);
} else {
Map<String, String> typeNameMap = TypeNameValue.extractMap(result.getTypeName());
for (String oneTypeName : typeNames) {
String value = typeNameMap.get(oneTypeName);
if (value == null)
value = "";
addTag(resultString, oneTypeName, value);
}
}
}
/**
* VALID CHARACTERS:
* METRIC, TAGNAME, AND TAG-VALUE:
* [-_./a-zA-Z0-9]+
* <p/>
* <p/>
* SANITIZATION:
* - Discard Quotes.
* - Replace all other invalid characters with '_'.
*/
protected String sanitizeString(String unSanitized) {
return unSanitized.
replaceAll("[\"']", "").
replaceAll("[^-_./a-zA-Z0-9]", "_");
}
}