001
002package io.prometheus.client;
003
004import io.prometheus.client.exemplars.Exemplar;
005
006import java.util.*;
007import java.util.regex.Pattern;
008
009/**
010 * A collector for a set of metrics.
011 * <p>
012 * Normal users should use {@link Gauge}, {@link Counter}, {@link Summary} and {@link Histogram}.
013 * <p>
014 * Subclasssing Collector is for advanced uses, such as proxying metrics from another monitoring system.
015 * It is it the responsibility of subclasses to ensure they produce valid metrics.
016 * @see <a href="http://prometheus.io/docs/instrumenting/exposition_formats/">Exposition formats</a>.
017 */
018public abstract class Collector {
019
020  /**
021   * Return all metrics of this Collector.
022   */
023  public abstract List<MetricFamilySamples> collect();
024
025  /**
026   * Like {@link #collect()}, but the result should only contain {@code MetricFamilySamples} where
027   * {@code sampleNameFilter.test(name)} is {@code true} for at least one Sample name.
028   * <p>
029   * The default implementation first collects all {@code MetricFamilySamples} and then discards the ones
030   * where {@code sampleNameFilter.test(name)} returns {@code false} for all names in
031   * {@link MetricFamilySamples#getNames()}.
032   * To improve performance, collector implementations should override this method to prevent
033   * {@code MetricFamilySamples} from being collected if they will be discarded anyways.
034   * See {@code ThreadExports} for an example.
035   * <p>
036   * Note that the resulting List may contain {@code MetricFamilySamples} where some Sample names return
037   * {@code true} for {@code sampleNameFilter.test(name)} but some Sample names return {@code false}.
038   * This is ok, because before we produce the output format we will call
039   * {@link MetricFamilySamples#filter(Predicate)} to strip all Samples where {@code sampleNameFilter.test(name)}
040   * returns {@code false}.
041   *
042   * @param sampleNameFilter may be {@code null}, indicating that all metrics should be collected.
043   */
044  public List<MetricFamilySamples> collect(Predicate<String> sampleNameFilter) {
045    List<MetricFamilySamples> all = collect();
046    if (sampleNameFilter == null) {
047      return all;
048    }
049    List<MetricFamilySamples> remaining = new ArrayList<MetricFamilySamples>(all.size());
050    for (MetricFamilySamples mfs : all) {
051      for (String name : mfs.getNames()) {
052        if (sampleNameFilter.test(name)) {
053          remaining.add(mfs);
054          break;
055        }
056      }
057    }
058    return remaining;
059  }
060
061  public enum Type {
062    UNKNOWN, // This is untyped in Prometheus text format.
063    COUNTER,
064    GAUGE,
065    STATE_SET,
066    INFO,
067    HISTOGRAM,
068    GAUGE_HISTOGRAM,
069    SUMMARY,
070  }
071
072  /**
073   * A metric, and all of its samples.
074   */
075  static public class MetricFamilySamples {
076    public final String name;
077    public final String unit;
078    public final Type type;
079    public final String help;
080    public final List<Sample> samples; // this list is modified when samples are added/removed.
081
082    public MetricFamilySamples(String name, Type type, String help, List<Sample> samples) {
083      this(name, "", type, help, samples);
084    }
085
086    public MetricFamilySamples(String name, String unit, Type type, String help, List<Sample> samples) {
087      if (!unit.isEmpty() && !name.endsWith("_" + unit)) {
088        throw new IllegalArgumentException("Metric's unit is not the suffix of the metric name: " + name);
089      }
090      if ((type == Type.INFO || type == Type.STATE_SET) && !unit.isEmpty()) {
091        throw new IllegalArgumentException("Metric is of a type that cannot have a unit: " + name);
092      }
093      List<Sample> mungedSamples = samples;
094      // Deal with _total from pre-OM automatically.
095      if (type == Type.COUNTER) {
096        if (name.endsWith("_total")) {
097          name = name.substring(0, name.length() - 6);
098        }
099        String withTotal = name + "_total";
100        mungedSamples = new ArrayList<Sample>(samples.size());
101        for (Sample s: samples) {
102          String n = s.name;
103          if (name.equals(n)) {
104            n = withTotal;
105          }
106          mungedSamples.add(new Sample(n, s.labelNames, s.labelValues, s.value, s.exemplar, s.timestampMs));
107        }
108      }
109      this.name = name;
110      this.unit = unit;
111      this.type = type;
112      this.help = help;
113      this.samples = mungedSamples;
114    }
115
116    /**
117     * @param sampleNameFilter may be {@code null} indicating that the result contains the complete list of samples.
118     * @return A new MetricFamilySamples containing only the Samples matching the {@code sampleNameFilter},
119     *         or {@code null} if no Sample matches.
120     */
121    public MetricFamilySamples filter(Predicate<String> sampleNameFilter) {
122      if (sampleNameFilter == null) {
123        return this;
124      }
125      List<Sample> remainingSamples = new ArrayList<Sample>(samples.size());
126      for (Sample sample : samples) {
127        if (sampleNameFilter.test(sample.name)) {
128          remainingSamples.add(sample);
129        }
130      }
131      if (remainingSamples.isEmpty()) {
132        return null;
133      }
134      return new MetricFamilySamples(name, unit, type, help, remainingSamples);
135    }
136
137    /**
138     * List of names that are reserved for Samples in these MetricsFamilySamples.
139     * <p>
140     * This is used in two places:
141     * <ol>
142     *     <li>To check potential name collisions in {@link CollectorRegistry#register(Collector)}.
143     *     <li>To check if a collector may contain metrics matching the metric name filter
144     *         in {@link Collector#collect(Predicate)}.
145     * </ol>
146     * Note that {@code getNames()} always includes the name without suffix, even though some
147     * metrics types (like Counter) will not have a Sample with that name.
148     * The reason is that the name without suffix is used in the metadata comments ({@code # TYPE}, {@code # UNIT},
149     * {@code # HELP}), and as this name <a href="https://github.com/prometheus/common/issues/319">must be unique</a>
150     * we include the name without suffix here as well.
151     */
152    public String[] getNames() {
153      switch (type) {
154        case COUNTER:
155          return new String[]{
156                  name + "_total",
157                  name + "_created",
158                  name
159          };
160        case SUMMARY:
161          return new String[]{
162                  name + "_count",
163                  name + "_sum",
164                  name + "_created",
165                  name
166          };
167        case HISTOGRAM:
168          return new String[]{
169                  name + "_count",
170                  name + "_sum",
171                  name + "_bucket",
172                  name + "_created",
173                  name
174          };
175        case GAUGE_HISTOGRAM:
176          return new String[]{
177                  name + "_gcount",
178                  name + "_gsum",
179                  name + "_bucket",
180                  name
181          };
182        case INFO:
183          return new String[]{
184                  name + "_info",
185                  name
186          };
187        default:
188          return new String[]{name};
189      }
190    }
191
192
193    @Override
194    public boolean equals(Object obj) {
195      if (!(obj instanceof MetricFamilySamples)) {
196        return false;
197      }
198      MetricFamilySamples other = (MetricFamilySamples) obj;
199      
200      return other.name.equals(name)
201        && other.unit.equals(unit)
202        && other.type.equals(type)
203        && other.help.equals(help)
204        && other.samples.equals(samples);
205    }
206
207    @Override
208    public int hashCode() {
209      int hash = 1;
210      hash = 37 * hash + name.hashCode();
211      hash = 37 * hash + unit.hashCode();
212      hash = 37 * hash + type.hashCode();
213      hash = 37 * hash + help.hashCode();
214      hash = 37 * hash + samples.hashCode();
215      return hash;
216    }
217
218    @Override
219    public String toString() {
220      return "Name: " + name + " Unit:" + unit + " Type: " + type + " Help: " + help +
221        " Samples: " + samples;
222    }
223
224  /**
225   * A single Sample, with a unique name and set of labels.
226   */
227    public static class Sample {
228      public final String name;
229      public final List<String> labelNames;
230      public final List<String> labelValues;  // Must have same length as labelNames.
231      public final double value;
232      public final Exemplar exemplar;
233      public final Long timestampMs;  // It's an epoch format with milliseconds value included (this field is subject to change).
234
235      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar, Long timestampMs) {
236        this.name = name;
237        this.labelNames = labelNames;
238        this.labelValues = labelValues;
239        this.value = value;
240        this.exemplar = exemplar;
241        this.timestampMs = timestampMs;
242      }
243
244      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Long timestampMs) {
245        this(name, labelNames, labelValues, value, null, timestampMs);
246      }
247
248      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar) {
249        this(name, labelNames, labelValues, value, exemplar, null);
250      }
251
252      public Sample(String name, List<String> labelNames, List<String> labelValues, double value) {
253        this(name, labelNames, labelValues, value, null, null);
254      }
255
256      @Override
257      public boolean equals(Object obj) {
258        if (!(obj instanceof Sample)) {
259          return false;
260        }
261        Sample other = (Sample) obj;
262
263        return other.name.equals(name) &&
264            other.labelNames.equals(labelNames) &&
265            other.labelValues.equals(labelValues) &&
266            other.value == value &&
267            (exemplar == null && other.exemplar == null || other.exemplar != null && other.exemplar.equals(exemplar)) &&
268            (timestampMs == null && other.timestampMs == null || other.timestampMs != null && other.timestampMs.equals(timestampMs));
269      }
270
271      @Override
272      public int hashCode() {
273        int hash = 1;
274        hash = 37 * hash + name.hashCode();
275        hash = 37 * hash + labelNames.hashCode();
276        hash = 37 * hash + labelValues.hashCode();
277        long d = Double.doubleToLongBits(value);
278        hash = 37 * hash + (int)(d ^ (d >>> 32));
279        if (timestampMs != null) {
280          hash = 37 * hash + timestampMs.hashCode();
281        }
282        if (exemplar != null) {
283          hash = 37 * exemplar.hashCode();
284        }
285        return hash;
286      }
287
288      @Override
289      public String toString() {
290        return "Name: " + name + " LabelNames: " + labelNames + " labelValues: " + labelValues +
291          " Value: " + value + " TimestampMs: " + timestampMs;
292      }
293    }
294  }
295
296  /**
297   * Register the Collector with the default registry.
298   */
299  public <T extends Collector> T register() {
300    return register(CollectorRegistry.defaultRegistry);
301  }
302
303  /**
304   * Register the Collector with the given registry.
305   */
306  public <T extends Collector> T register(CollectorRegistry registry) {
307    registry.register(this);
308    return (T)this;
309  }
310
311  public interface Describable {
312    /**
313     *  Provide a list of metric families this Collector is expected to return.
314     *
315     *  These should exclude the samples. This is used by the registry to
316     *  detect collisions and duplicate registrations.
317     *
318     *  Usually custom collectors do not have to implement Describable. If
319     *  Describable is not implemented and the CollectorRegistry was created
320     *  with auto describe enabled (which is the case for the default registry)
321     *  then {@link #collect} will be called at registration time instead of
322     *  describe. If this could cause problems, either implement a proper
323     *  describe, or if that's not practical have describe return an empty
324     *  list.
325     */
326    List<MetricFamilySamples> describe();
327  }
328
329
330  /* Various utility functions for implementing Collectors. */
331
332  /**
333   * Number of nanoseconds in a second.
334   */
335  public static final double NANOSECONDS_PER_SECOND = 1E9;
336  /**
337   * Number of milliseconds in a second.
338   */
339  public static final double MILLISECONDS_PER_SECOND = 1E3;
340
341  private static final Pattern METRIC_NAME_RE = Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*");
342  private static final Pattern METRIC_LABEL_NAME_RE = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
343  private static final Pattern RESERVED_METRIC_LABEL_NAME_RE = Pattern.compile("__.*");
344
345  /**
346   * Throw an exception if the metric name is invalid.
347   */
348  protected static void checkMetricName(String name) {
349    if (!METRIC_NAME_RE.matcher(name).matches()) {
350      throw new IllegalArgumentException("Invalid metric name: " + name);
351    }
352  }
353
354  /**
355   * Sanitize metric name
356   */
357  public static String sanitizeMetricName(String metricName) {
358    int length = metricName.length();
359    char[] sanitized = new char[length];
360    for(int i = 0; i < length; i++) {
361      char ch = metricName.charAt(i);
362      if(ch == ':' ||
363          (ch >= 'a' && ch <= 'z') ||
364          (ch >= 'A' && ch <= 'Z') ||
365          (i > 0 && ch >= '0' && ch <= '9')) {
366        sanitized[i] = ch;
367      } else {
368        sanitized[i] = '_';
369      }
370    }
371    return new String(sanitized);
372  }
373
374  /**
375   * Throw an exception if the metric label name is invalid.
376   */
377  protected static void checkMetricLabelName(String name) {
378    if (!METRIC_LABEL_NAME_RE.matcher(name).matches()) {
379      throw new IllegalArgumentException("Invalid metric label name: " + name);
380    }
381    if (RESERVED_METRIC_LABEL_NAME_RE.matcher(name).matches()) {
382      throw new IllegalArgumentException("Invalid metric label name, reserved for internal use: " + name);
383    }
384  }
385
386  /**
387   * Convert a double to its string representation in Go.
388   */
389  public static String doubleToGoString(double d) {
390    if (d == Double.POSITIVE_INFINITY) {
391      return "+Inf";
392    } 
393    if (d == Double.NEGATIVE_INFINITY) {
394      return "-Inf";
395    }
396    return Double.toString(d);
397  }
398}