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 &gt;= 0 and &lt; {@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 &gt;= 0 and &lt; {@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}