/* * Copyright 2014 the original author or authors. * * 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.github.sps.metrics.opentsdb; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Scanner; import java.lang.IllegalArgumentException; import java.util.NoSuchElementException; /** * Representation of a metric. * * @author Sean Scanlon <[email protected]> * @author Adam Lugowski <[email protected]> * */ public class OpenTsdbMetric { private OpenTsdbMetric() { } /** * Convert a tag string into a tag map. * * @param tagString a space-delimited string of key-value pairs. For example, {@code "key1=value1 key_n=value_n"} * @return a tag {@link Map} * @throws IllegalArgumentException if the tag string is corrupted. */ public static Map<String, String> parseTags(final String tagString) throws IllegalArgumentException { // delimit by whitespace or '=' Scanner scanner = new Scanner(tagString).useDelimiter("\\s+|="); Map<String, String> tagMap = new HashMap<String, String>(); try { while (scanner.hasNext()) { String tagName = scanner.next(); String tagValue = scanner.next(); tagMap.put(tagName, tagValue); } } catch (NoSuchElementException e) { // The tag string is corrupted. throw new IllegalArgumentException("Invalid tag string '" + tagString + "'"); } finally { scanner.close(); } return tagMap; } /** * Convert a tag map into a space-delimited string. * * @param tagMap * @return a space-delimited string of key-value pairs. For example, {@code "key1=value1 key_n=value_n"} */ public static String formatTags(final Map<String, String> tagMap) { StringBuilder stringBuilder = new StringBuilder(); String delimeter = ""; for (Map.Entry<String, String> tag : tagMap.entrySet()) { stringBuilder.append(delimeter) .append(sanitize(tag.getKey())) .append("=") .append(sanitize(tag.getValue())); delimeter = " "; } return stringBuilder.toString(); } /** * Add TSDB tags to a CodaHale metric name. * * A CodaHale metric name is a single string, so there is no natural way to encode TSDB tags. * This function formats the tags into the metric name so they can later be parsed by the * Builder. The name is formatted such that it can be appended to create sub-metric names, as * happens with Meter and Histogram. * * @param name CodaHale metric name * @param tags A space-delimited string of TSDB key-value pair tags * @return A metric name encoded with tags. * @throws IllegalArgumentException if the tag string is invalid */ public static String encodeTagsInName(final String name, final String tags) throws IllegalArgumentException { return encodeTagsInName(name, parseTags(tags)); } /** * Add TSDB tags to a CodaHale metric name. * * A CodaHale metric name is a single string, so there is no natural way to encode TSDB tags. * This function formats the tags into the metric name so they can later be parsed by the * Builder. The name is formatted such that it can be appended to create sub-metric names, as * happens with Meter and Histogram. * * @param name CodaHale metric name * @param tags a {@link Map} of TSDB tags * @return A metric name encoded with tags. */ public static String encodeTagsInName(final String name, final Map<String, String> tags) { return String.format("TAG(%s)%s", formatTags(tags), sanitize(name)); } /** * Tests whether a name has been processed with {@code encodeTagsInName}. * * @param name a metric name * @return {@code true} if {@code name} has tags encoded, {@code false} otherwise. */ public static boolean hasEncodedTagInName(final String name) { if (name == null) return false; return name.startsWith("TAG("); } /** * Call this function whenever a potentially tag-encoded name is prefixed. * * @param name a metric name with encoded tag strings that has been prefixed. * @return a fixed metric name */ public static String fixEncodedTagsInNameAfterPrefix(final String name) { if (name == null) return name; int tagStart = name.indexOf("TAG("); if (tagStart == -1) return name; // no tags in this name if (tagStart == 0) return name; // tag string is already correct // extract the "TAG(...)" string from the middle of the name and put it at the front. int tagEnd = name.lastIndexOf(')'); if (tagEnd == -1) { throw new IllegalArgumentException("Tag definition missing closing parenthesis for metric '" + name + "'"); } String tagString = name.substring(tagStart, tagEnd+1); return tagString + name.substring(0, tagStart) + name.substring(tagEnd+1); } /** * Creates a Builder for a metric name. * * @param name name can contain either a pure CodaHale metric name, or a string returned by {@code encodeTagsInName}. * If it's the latter, the tags are parsed out and passed to {@code withTags}. * @return a {@link Builder} */ public static Builder named(String name) { /* A name can contain either a pure metric name, or a string returned by encodeTagsInName(). If it's the latter, it looks like "TAG(tag1=value1 tag2=value2)metricname". */ if (!hasEncodedTagInName(name)) { return new Builder(name); } // parse out the tags int tagEnd = name.lastIndexOf(')'); if (tagEnd == -1) { throw new IllegalArgumentException("Tag definition missing closing parenthesis for metric '" + name + "'"); } String tagString = name.substring(4, tagEnd); name = name.substring(tagEnd+1); return new Builder(name).withTags(parseTags(tagString)); } private String metric; private Long timestamp; private Object value; private Map<String, String> tags = new HashMap<String, String>(); @Override public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof OpenTsdbMetric)) { return false; } final OpenTsdbMetric rhs = (OpenTsdbMetric) o; return equals(metric, rhs.metric) && equals(timestamp, rhs.timestamp) && equals(value, rhs.value) && equals(tags, rhs.tags); } @Override public int hashCode() { return Arrays.hashCode(new Object[]{metric, timestamp, value, tags}); } public static class Builder { private final OpenTsdbMetric metric; public Builder(String name) { this.metric = new OpenTsdbMetric(); metric.metric = name; } public OpenTsdbMetric build() { return metric; } public Builder withValue(Object value) { metric.value = value; return this; } public Builder withTimestamp(Long timestamp) { metric.timestamp = timestamp; return this; } public Builder withTags(Map<String, String> tags) { if (tags != null) { metric.tags.putAll(tags); } return this; } } /** * Returns a JSON string version of this metric compatible with the HTTP API reporter. * * Example: * <pre><code> * { * "metric": "sys.cpu.nice", * "timestamp": 1346846400, * "value": 18, * "tags": { * "host": "web01", * "dc": "lga" * } * } * </code></pre> * @return a JSON string version of this metric compatible with the HTTP API reporter. */ @Override public String toString() { return this.getClass().getSimpleName() + "->metric: " + metric + ",value: " + value + ",timestamp: " + timestamp + ",tags: " + tags; } /** * Sanitizes a metric name, tag key, or tag value by removing characters not allowed by TSDB. * * Supported characters are {@code a-z A-Z 0-9 - _ . / } * * @param name a metric name, tag key, or tag value * @return {@code name} where unsupported characters are replaced with {@code "-"}. */ public static String sanitize(String name) { return name.replaceAll("[^a-zA-Z0-9\\-\\_\\.\\/]", "-"); } /** * Returns a put string version of this metric compatible with the telnet-style reporter. * * Format: * <pre><code> * put (metric-name) (timestamp) (value) (tags) * </code></pre> * * Example: * <pre><code> * put sys.cpu.nice 1346846400 18 host=web01 dc=lga * </code></pre> * * @return a string version of this metric compatible with the telnet reporter. */ public String toTelnetPutString() { String tagString = formatTags(tags); return String.format("put %s %d %s %s%n", metric, timestamp, value, tagString); } public String getMetric() { return metric; } public Long getTimestamp() { return timestamp; } public Object getValue() { return value; } public Map<String, String> getTags() { return tags; } private boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); } }