package org.stagemonitor.core.metrics.metrics2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.stagemonitor.core.util.GraphiteSanitizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Represents a metrics 2.0 name that consists of a name and arbitrary tags (a set of key-value-pairs).
* <p/>
* To create a new {@link MetricName}, use the static {@link MetricName}.{@link #name(String)} method. Example:
* <code>name("api_request_duration").tag("stage", "transform").build()</code>
* <p/>
* This is needed for example for InfluxDB's data model (see https://influxdb.com/docs/v0.9/concepts/schema_and_data_layout.html)
* and to store metrics into Elasticsearch (see https://www.elastic.co/blog/elasticsearch-as-a-time-series-data-store).
* See also http://metrics20.org/
* <p/>
* The cool thing is that it is completely backwards compatible to graphite metric names and can also automatically
* replace characters disallowed in graphite (see {@link #toGraphiteName()}).
* <p/>
* This class is immutable
*/
public class MetricName {
@JsonIgnore
private int hashCode;
private final String name;
// The insertion order is important for the correctness of #toGraphiteName
private final LinkedHashMap<String, String> tags;
private MetricName(String name, LinkedHashMap<String, String> tags) {
this.name = name;
this.tags = tags;
}
@JsonCreator
private MetricName(@JsonProperty("name") String name, @JsonProperty("tags") Map<String, String> tags) {
this.name = name;
this.tags = new LinkedHashMap<String, String>(tags);
}
/**
* Returns a copy of this name and appends a single tag
* <p/>
* Note that this method does not override existing tags
*
* @param key the key of the tag
* @param value the value of the tag
* @return a copy of this name including the provided tag
*/
public MetricName withTag(String key, String value) {
return name(name).tags(tags).tag(key, value).build();
}
/**
* Constructs a new {@link Builder} with the provided name.
* <p/>
* After adding tags with {@link Builder#tag(String, Object)}, call {@link Builder#build()} to get the
* immutable {@link MetricName}
* <p/>
* The metric name should only contain alphanumerical chars and underscores.
* <p/>
* When in doubt how to name a metic, take a look at https://prometheus.io/docs/practices/naming/
*
* @param name the metric name
* @return a {@link Builder} with the provided name
*/
public static Builder name(String name) {
return new Builder(name);
}
public String getName() {
return name;
}
public Map<String, String> getTags() {
return Collections.unmodifiableMap(tags);
}
@JsonIgnore
public List<String> getTagKeys() {
return new ArrayList<String>(tags.keySet());
}
@JsonIgnore
public List<String> getTagValues() {
return new ArrayList<String>(tags.values());
}
/**
* Converts a metrics 2.0 name into a graphite compliant name by appending all tag values to the metric name
*
* @return A graphite compliant name
*/
public String toGraphiteName() {
StringBuilder sb = new StringBuilder(GraphiteSanitizer.sanitizeGraphiteMetricSegment(name));
for (String value : tags.values()) {
sb.append('.').append(GraphiteSanitizer.sanitizeGraphiteMetricSegment(value));
}
return sb.toString();
}
/**
* A {@link MetricName} is only considered equal to another {@link MetricName} if the tags are in the same order.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MetricName that = (MetricName) o;
return name.equals(that.name) && tags.equals(that.tags);
}
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = name.hashCode();
result = 31 * result + tags.hashCode();
hashCode = result;
}
return result;
}
public boolean matches(MetricName other) {
return name.equals(other.name) && containsAllTags(other.tags);
}
private boolean containsAllTags(Map<String, String> tags) {
for (Map.Entry<String, String> entry : tags.entrySet()) {
if (!entry.getValue().equals(this.tags.get(entry.getKey()))) {
return false;
}
}
return true;
}
/**
* A {@link MetricNameTemplate} lets you efficiently create similar {@link MetricName}s so that if a {@link
* MetricName} has already been {@link #build(String)} for the same value(s), the previous instance is reused.
* <p/>
* In other words, this is a cache for {@link MetricName}s
* <p/>
* Example:
* <pre>
* MetricName.MetricNameTemplate timerMetricNameTemplate = name("response_time_server")
* .tag("request_name", "")
* .layer("All")
* .templateFor("request_name");
* MetricName metricName = timerMetricNameTemplate.build("Search Products");
* </pre>
*/
public static class MetricNameTemplate {
private final ConcurrentMap<Object, MetricName> metricNameCache = new ConcurrentHashMap<Object, MetricName>();
private final MetricName template;
private final String key;
private final List<String> keys;
private MetricNameTemplate(MetricName template, String key) {
this.template = template;
this.key = key;
this.keys = null;
}
private MetricNameTemplate(MetricName template, String... keys) {
this.template = template;
this.key = null;
this.keys = Arrays.asList(keys);
}
/**
* Creates a new or reused {@link MetricName} according to the {@link #template} with the given {@link #key}
* and the provided value
*
* @param value The tag value
* @return A {@link MetricName} according to the {@link #template}
* @throws IllegalArgumentException When this template is intended for multiple values i.e. was initialized via
* {@link MetricNameTemplate#MetricNameTemplate(MetricName, String...)}
*/
public MetricName build(String value) {
if (key == null) {
throw new IllegalArgumentException("Size of key does not match size of values");
}
MetricName metricName = metricNameCache.get(value);
if (metricName == null) {
metricName = template.withTag(key, value);
metricNameCache.put(value, metricName);
}
return metricName;
}
/**
* Creates a new or reused {@link MetricName} according to the {@link #template} with the given {@link #keys}
* and the provided values
*
* @param values The tag values (must match the size of {@link #keys}
* @return A {@link MetricName} according to the {@link #template}
* @throws IllegalArgumentException When number of {@link #keys} does not match the number of provided values or
* this template is intended for a single value i.e. was initialized via {@link
* MetricNameTemplate#MetricNameTemplate(MetricName, String)}
*/
public MetricName build(String... values) {
if (keys == null || keys.size() != values.length) {
throw new IllegalArgumentException("Size of key does not match size of values");
}
List<String> valuesList = Arrays.asList(values);
MetricName metricName = metricNameCache.get(valuesList);
if (metricName == null) {
Builder builder = name(template.name).tags(template.tags);
for (int i = 0; i < keys.size(); i++) {
builder.tag(keys.get(i), valuesList.get(i));
}
metricName = builder.build();
metricNameCache.put(valuesList, metricName);
}
return metricName;
}
}
public static class Builder {
private final String name;
private final LinkedHashMap<String, String> tags = new LinkedHashMap<String, String>(8);
public Builder(String name) {
this.name = name;
}
/**
* Adds a tag to the metric name.
*
* @param key The key should only contain alphanumerical chars and underscores.
* @param value The value can contain unicode characters, but it is recommended to not use white spaces.
* @return <code>this</code> for chaining
*/
public Builder tag(String key, String value) {
this.tags.put(key, value);
return this;
}
public Builder type(String value) {
return tag("type", value);
}
public Builder tier(String value) {
return tag("tier", value);
}
public Builder layer(String value) {
return tag("layer", value);
}
public Builder unit(String value) {
return tag("unit", value);
}
public Builder tags(Map<String, String> tags) {
this.tags.putAll(tags);
return this;
}
public MetricName build() {
return new MetricName(name, tags);
}
/**
* Creates a {@link MetricNameTemplate} with a single tag
*
* @param key The template tag key
* @return The {@link MetricNameTemplate}
*/
public MetricNameTemplate templateFor(String key) {
return new MetricNameTemplate(build(), key);
}
/**
* Creates a {@link MetricNameTemplate} with multiple tags
*
* @param keys The template tag keys
* @return The {@link MetricNameTemplate}
*/
public MetricNameTemplate templateFor(String... keys) {
return new MetricNameTemplate(build(), keys);
}
}
@Override
public String toString() {
return "name='" + name + '\'' + ", tags=" + getTags();
}
}