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   * Return all of the metrics of this Collector.
022   */
023  public abstract List<MetricFamilySamples> collect();
024  public enum Type {
025    UNKNOWN, // This is untyped in Prometheus text format.
026    COUNTER,
027    GAUGE,
028    STATE_SET,
029    INFO,
030    HISTOGRAM,
031    GAUGE_HISTOGRAM,
032    SUMMARY,
033  }
034
035  /**
036   * A metric, and all of its samples.
037   */
038  static public class MetricFamilySamples {
039    public final String name;
040    public final String unit;
041    public final Type type;
042    public final String help;
043    public final List<Sample> samples;
044
045    public MetricFamilySamples(String name, String unit, Type type, String help, List<Sample> samples) {
046      if (!unit.isEmpty() && !name.endsWith("_" + unit)) {
047        throw new IllegalArgumentException("Metric's unit is not the suffix of the metric name: " + name);
048      }
049      if ((type == Type.INFO || type == Type.STATE_SET) && !unit.isEmpty()) {
050        throw new IllegalArgumentException("Metric is of a type that cannot have a unit: " + name);
051      }
052      List<Sample> mungedSamples = samples;
053      // Deal with _total from pre-OM automatically.
054      if (type == Type.COUNTER) {
055        if (name.endsWith("_total")) {
056          name = name.substring(0, name.length() - 6);
057        }
058        String withTotal = name + "_total";
059        mungedSamples = new ArrayList<Sample>(samples.size());
060        for (Sample s: samples) {
061          String n = s.name;
062          if (name.equals(n)) {
063            n = withTotal;
064          }
065          mungedSamples.add(new Sample(n, s.labelNames, s.labelValues, s.value, s.exemplar, s.timestampMs));
066        }
067      }
068      this.name = name;
069      this.unit = unit;
070      this.type = type;
071      this.help = help;
072      this.samples = mungedSamples;
073    }
074
075    public MetricFamilySamples(String name, Type type, String help, List<Sample> samples) {
076      this(name, "", type, help, samples);
077    }
078
079    @Override
080    public boolean equals(Object obj) {
081      if (!(obj instanceof MetricFamilySamples)) {
082        return false;
083      }
084      MetricFamilySamples other = (MetricFamilySamples) obj;
085      
086      return other.name.equals(name)
087        && other.unit.equals(unit)
088        && other.type.equals(type)
089        && other.help.equals(help)
090        && other.samples.equals(samples);
091    }
092
093    @Override
094    public int hashCode() {
095      int hash = 1;
096      hash = 37 * hash + name.hashCode();
097      hash = 37 * hash + unit.hashCode();
098      hash = 37 * hash + type.hashCode();
099      hash = 37 * hash + help.hashCode();
100      hash = 37 * hash + samples.hashCode();
101      return hash;
102    }
103
104    @Override
105    public String toString() {
106      return "Name: " + name + " Unit:" + unit + " Type: " + type + " Help: " + help +
107        " Samples: " + samples;
108    }
109
110  /**
111   * A single Sample, with a unique name and set of labels.
112   */
113    public static class Sample {
114      public final String name;
115      public final List<String> labelNames;
116      public final List<String> labelValues;  // Must have same length as labelNames.
117      public final double value;
118      public final Exemplar exemplar;
119      public final Long timestampMs;  // It's an epoch format with milliseconds value included (this field is subject to change).
120
121      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar, Long timestampMs) {
122        this.name = name;
123        this.labelNames = labelNames;
124        this.labelValues = labelValues;
125        this.value = value;
126        this.exemplar = exemplar;
127        this.timestampMs = timestampMs;
128      }
129
130      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Long timestampMs) {
131        this(name, labelNames, labelValues, value, null, timestampMs);
132      }
133
134      public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar) {
135        this(name, labelNames, labelValues, value, exemplar, null);
136      }
137
138      public Sample(String name, List<String> labelNames, List<String> labelValues, double value) {
139        this(name, labelNames, labelValues, value, null, null);
140      }
141
142      @Override
143      public boolean equals(Object obj) {
144        if (!(obj instanceof Sample)) {
145          return false;
146        }
147        Sample other = (Sample) obj;
148
149        return other.name.equals(name) &&
150            other.labelNames.equals(labelNames) &&
151            other.labelValues.equals(labelValues) &&
152            other.value == value &&
153            (exemplar == null && other.exemplar == null || other.exemplar != null && other.exemplar.equals(exemplar)) &&
154            (timestampMs == null && other.timestampMs == null || other.timestampMs != null && other.timestampMs.equals(timestampMs));
155      }
156
157      @Override
158      public int hashCode() {
159        int hash = 1;
160        hash = 37 * hash + name.hashCode();
161        hash = 37 * hash + labelNames.hashCode();
162        hash = 37 * hash + labelValues.hashCode();
163        long d = Double.doubleToLongBits(value);
164        hash = 37 * hash + (int)(d ^ (d >>> 32));
165        if (timestampMs != null) {
166          hash = 37 * hash + timestampMs.hashCode();
167        }
168        if (exemplar != null) {
169          hash = 37 * exemplar.hashCode();
170        }
171        return hash;
172      }
173
174      @Override
175      public String toString() {
176        return "Name: " + name + " LabelNames: " + labelNames + " labelValues: " + labelValues +
177          " Value: " + value + " TimestampMs: " + timestampMs;
178      }
179    }
180  }
181
182  /**
183   * Register the Collector with the default registry.
184   */
185  public <T extends Collector> T register() {
186    return register(CollectorRegistry.defaultRegistry);
187  }
188
189  /**
190   * Register the Collector with the given registry.
191   */
192  public <T extends Collector> T register(CollectorRegistry registry) {
193    registry.register(this);
194    return (T)this;
195  }
196
197  public interface Describable {
198    /**
199     *  Provide a list of metric families this Collector is expected to return.
200     *
201     *  These should exclude the samples. This is used by the registry to
202     *  detect collisions and duplicate registrations.
203     *
204     *  Usually custom collectors do not have to implement Describable. If
205     *  Describable is not implemented and the CollectorRegistry was created
206     *  with auto describe enabled (which is the case for the default registry)
207     *  then {@link collect} will be called at registration time instead of
208     *  describe. If this could cause problems, either implement a proper
209     *  describe, or if that's not practical have describe return an empty
210     *  list.
211     */
212    List<MetricFamilySamples> describe();
213  }
214
215
216  /* Various utility functions for implementing Collectors. */
217
218  /**
219   * Number of nanoseconds in a second.
220   */
221  public static final double NANOSECONDS_PER_SECOND = 1E9;
222  /**
223   * Number of milliseconds in a second.
224   */
225  public static final double MILLISECONDS_PER_SECOND = 1E3;
226
227  private static final Pattern METRIC_NAME_RE = Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*");
228  private static final Pattern METRIC_LABEL_NAME_RE = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
229  private static final Pattern RESERVED_METRIC_LABEL_NAME_RE = Pattern.compile("__.*");
230
231  /**
232   * Throw an exception if the metric name is invalid.
233   */
234  protected static void checkMetricName(String name) {
235    if (!METRIC_NAME_RE.matcher(name).matches()) {
236      throw new IllegalArgumentException("Invalid metric name: " + name);
237    }
238  }
239
240  private static final Pattern SANITIZE_PREFIX_PATTERN = Pattern.compile("^[^a-zA-Z_:]");
241  private static final Pattern SANITIZE_BODY_PATTERN = Pattern.compile("[^a-zA-Z0-9_:]");
242
243  /**
244   * Sanitize metric name
245   */
246  public static String sanitizeMetricName(String metricName) {
247    return SANITIZE_BODY_PATTERN.matcher(
248            SANITIZE_PREFIX_PATTERN.matcher(metricName).replaceFirst("_")
249    ).replaceAll("_");
250  }
251
252  /**
253   * Throw an exception if the metric label name is invalid.
254   */
255  protected static void checkMetricLabelName(String name) {
256    if (!METRIC_LABEL_NAME_RE.matcher(name).matches()) {
257      throw new IllegalArgumentException("Invalid metric label name: " + name);
258    }
259    if (RESERVED_METRIC_LABEL_NAME_RE.matcher(name).matches()) {
260      throw new IllegalArgumentException("Invalid metric label name, reserved for internal use: " + name);
261    }
262  }
263
264  /**
265   * Convert a double to its string representation in Go.
266   */
267  public static String doubleToGoString(double d) {
268    if (d == Double.POSITIVE_INFINITY) {
269      return "+Inf";
270    } 
271    if (d == Double.NEGATIVE_INFINITY) {
272      return "-Inf";
273    }
274    return Double.toString(d);
275  }
276}