001package io.prometheus.client.exemplars; 002 003import java.util.Arrays; 004import java.util.Map; 005import java.util.regex.Pattern; 006 007/** 008 * Immutable data class holding an Exemplar. 009 */ 010public class Exemplar { 011 012 private final String[] labels; 013 private final double value; 014 private final Long timestampMs; 015 016 private static final Pattern labelNameRegex = Pattern.compile("[a-zA-Z_][a-zA-Z_0-9]*"); 017 018 /** 019 * Create an Exemplar without a timestamp 020 * 021 * @param value the observed value 022 * @param labels name/value pairs. Expecting an even number of strings. The combined length of the label names and 023 * values must not exceed 128 UTF-8 characters. Neither a label name nor a label value may be null. 024 */ 025 public Exemplar(double value, String... labels) { 026 this(value, null, labels); 027 } 028 029 /** 030 * Create an Exemplar 031 * 032 * @param value the observed value 033 * @param timestampMs as in {@link System#currentTimeMillis()} 034 * @param labels name/value pairs. Expecting an even number of strings. The combined length of the 035 * label names and values must not exceed 128 UTF-8 characters. Neither a label name 036 * nor a label value may be null. 037 */ 038 public Exemplar(double value, Long timestampMs, String... labels) { 039 this.labels = sortedCopy(labels); 040 this.value = value; 041 this.timestampMs = timestampMs; 042 } 043 044 /** 045 * Create an Exemplar 046 * 047 * @param value the observed value 048 * @param labels the labels. Must not be null. The combined length of the label names and values must not exceed 049 * 128 UTF-8 characters. Neither a label name nor a label value may be null. 050 */ 051 public Exemplar(double value, Map<String, String> labels) { 052 this(value, null, mapToArray(labels)); 053 } 054 055 /** 056 * Create an Exemplar 057 * 058 * @param value the observed value 059 * @param timestampMs as in {@link System#currentTimeMillis()} 060 * @param labels the labels. Must not be null. The combined length of the label names and values must not exceed 061 * 128 UTF-8 characters. Neither a label name nor a label value may be null. 062 */ 063 public Exemplar(double value, Long timestampMs, Map<String, String> labels) { 064 this(value, timestampMs, mapToArray(labels)); 065 } 066 067 public int getNumberOfLabels() { 068 return labels.length / 2; 069 } 070 071 /** 072 * Get the label name at index {@code i}. 073 * @param i the index, must be >= 0 and < {@link #getNumberOfLabels()}. 074 * @return the label name at index {@code i} 075 */ 076 public String getLabelName(int i) { 077 return labels[2 * i]; 078 } 079 080 /** 081 * Get the label value at index {@code i}. 082 * @param i the index, must be >= 0 and < {@link #getNumberOfLabels()}. 083 * @return the label value at index {@code i} 084 */ 085 public String getLabelValue(int i) { 086 return labels[2 * i + 1]; 087 } 088 089 public double getValue() { 090 return value; 091 } 092 093 /** 094 * @return Unix timestamp or {@code null} if no timestamp is present. 095 */ 096 public Long getTimestampMs() { 097 return timestampMs; 098 } 099 100 private String[] sortedCopy(String... labels) { 101 if (labels.length % 2 != 0) { 102 throw new IllegalArgumentException("labels are name/value pairs, expecting an even number"); 103 } 104 String[] result = new String[labels.length]; 105 int charsTotal = 0; 106 for (int i = 0; i < labels.length; i+=2) { 107 if (labels[i] == null) { 108 throw new IllegalArgumentException("labels[" + i + "] is null"); 109 } 110 if (labels[i+1] == null) { 111 throw new IllegalArgumentException("labels[" + (i+1) + "] is null"); 112 } 113 if (!labelNameRegex.matcher(labels[i]).matches()) { 114 throw new IllegalArgumentException(labels[i] + " is not a valid label name"); 115 } 116 result[i] = labels[i]; // name 117 result[i+1] = labels[i+1]; // value 118 charsTotal += labels[i].length() + labels[i+1].length(); 119 // Move the current tuple down while the previous name is greater than current name. 120 for (int j=i-2; j>=0; j-=2) { 121 int compareResult = result[j+2].compareTo(result[j]); 122 if (compareResult == 0) { 123 throw new IllegalArgumentException(result[j] + ": label name is not unique"); 124 } else if (compareResult < 0) { 125 String tmp = result[j]; 126 result[j] = result[j+2]; 127 result[j+2] = tmp; 128 tmp = result[j+1]; 129 result[j+1] = result[j+3]; 130 result[j+3] = tmp; 131 } else { 132 break; 133 } 134 } 135 } 136 if (charsTotal > 128) { 137 throw new IllegalArgumentException( 138 "the combined length of the label names and values must not exceed 128 UTF-8 characters"); 139 } 140 return result; 141 } 142 143 /** 144 * Convert the map to an array {@code [key1, value1, key2, value2, ...]}. 145 */ 146 public static String[] mapToArray(Map<String, String> labelMap) { 147 if (labelMap == null) { 148 return null; 149 } 150 String[] result = new String[2 * labelMap.size()]; 151 int i = 0; 152 for (Map.Entry<String, String> entry : labelMap.entrySet()) { 153 result[i] = entry.getKey(); 154 result[i + 1] = entry.getValue(); 155 i += 2; 156 } 157 return result; 158 } 159 160 @Override 161 public boolean equals(Object obj) { 162 if (this == obj) { 163 return true; 164 } 165 if (!(obj instanceof Exemplar)) { 166 return false; 167 } 168 Exemplar other = (Exemplar) obj; 169 return Arrays.equals(this.labels, other.labels) && 170 Double.compare(other.value, value) == 0 && 171 (timestampMs == null && other.timestampMs == null 172 || timestampMs != null && timestampMs.equals(other.timestampMs)); 173 } 174 175 @Override 176 public int hashCode() { 177 int hash = Arrays.hashCode(labels); 178 long d = Double.doubleToLongBits(value); 179 hash = 37 * hash + (int) (d ^ (d >>> 32)); 180 if (timestampMs != null) { 181 hash = 37 * hash + timestampMs.intValue(); 182 } 183 return hash; 184 } 185}