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