001package io.prometheus.metrics.model.snapshots;
002
003import io.prometheus.metrics.annotations.StableApi;
004import io.prometheus.metrics.config.EscapingScheme;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.List;
008import javax.annotation.Nullable;
009
010/** Immutable snapshot of a Counter. */
011@StableApi
012public class CounterSnapshot extends MetricSnapshot {
013
014  /**
015   * To create a new {@link CounterSnapshot}, you can either call the constructor directly or use
016   * the builder with {@link CounterSnapshot#builder()}.
017   *
018   * @param metadata the metric name in metadata must not include the {@code _total} suffix. See
019   *     {@link MetricMetadata} for more naming conventions.
020   * @param dataPoints the constructor will create a sorted copy of the collection.
021   */
022  public CounterSnapshot(MetricMetadata metadata, Collection<CounterDataPointSnapshot> dataPoints) {
023    this(metadata, dataPoints, false);
024  }
025
026  private CounterSnapshot(
027      MetricMetadata metadata, Collection<CounterDataPointSnapshot> dataPoints, boolean internal) {
028    super(metadata, dataPoints, internal);
029  }
030
031  @SuppressWarnings("unchecked")
032  @Override
033  public List<CounterDataPointSnapshot> getDataPoints() {
034    return (List<CounterDataPointSnapshot>) dataPoints;
035  }
036
037  @SuppressWarnings("unchecked")
038  @Override
039  MetricSnapshot escape(
040      EscapingScheme escapingScheme, List<? extends DataPointSnapshot> dataPointSnapshots) {
041    return new CounterSnapshot(
042        getMetadata().escape(escapingScheme),
043        (List<CounterDataPointSnapshot>) dataPointSnapshots,
044        true);
045  }
046
047  public static class CounterDataPointSnapshot extends DataPointSnapshot {
048
049    private final double value;
050    @Nullable private final Exemplar exemplar;
051
052    /** Optional metric name used only in validation error messages. */
053    @Nullable private final String metricName;
054
055    /**
056     * To create a new {@link CounterDataPointSnapshot}, you can either call the constructor
057     * directly or use the Builder with {@link CounterDataPointSnapshot#builder()}.
058     *
059     * @param value the counter value. Must not be negative.
060     * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels.
061     * @param exemplar may be null.
062     * @param createdTimestampMillis timestamp (as in {@link System#currentTimeMillis()}) when the
063     *     time series (this specific set of labels) was created (or reset to zero). It's optional.
064     *     Use {@code 0L} if there is no created timestamp.
065     */
066    public CounterDataPointSnapshot(
067        double value, Labels labels, @Nullable Exemplar exemplar, long createdTimestampMillis) {
068      this(value, labels, exemplar, createdTimestampMillis, 0, false, null);
069    }
070
071    /**
072     * Constructor with an additional scrape timestamp. This is only useful in rare cases as the
073     * scrape timestamp is usually set by the Prometheus server during scraping. Exceptions include
074     * mirroring metrics with given timestamps from other metric sources.
075     */
076    @SuppressWarnings("this-escape")
077    public CounterDataPointSnapshot(
078        double value,
079        Labels labels,
080        @Nullable Exemplar exemplar,
081        long createdTimestampMillis,
082        long scrapeTimestampMillis) {
083      this(value, labels, exemplar, createdTimestampMillis, scrapeTimestampMillis, false, null);
084    }
085
086    @SuppressWarnings("this-escape")
087    public CounterDataPointSnapshot(
088        double value,
089        Labels labels,
090        @Nullable Exemplar exemplar,
091        long createdTimestampMillis,
092        long scrapeTimestampMillis,
093        boolean internal) {
094      this(value, labels, exemplar, createdTimestampMillis, scrapeTimestampMillis, internal, null);
095    }
096
097    @SuppressWarnings("this-escape")
098    private CounterDataPointSnapshot(
099        double value,
100        Labels labels,
101        @Nullable Exemplar exemplar,
102        long createdTimestampMillis,
103        long scrapeTimestampMillis,
104        boolean internal,
105        @Nullable String metricName) {
106      super(labels, createdTimestampMillis, scrapeTimestampMillis, internal);
107      this.value = value;
108      this.exemplar = exemplar;
109      this.metricName = metricName;
110      if (!internal) {
111        validate();
112      }
113    }
114
115    public double getValue() {
116      return value;
117    }
118
119    @Nullable
120    public Exemplar getExemplar() {
121      return exemplar;
122    }
123
124    protected void validate() {
125      if (value < 0.0) {
126        StringBuilder message = new StringBuilder();
127        if (metricName != null && !metricName.isEmpty()) {
128          message.append(metricName).append('=');
129        }
130        message.append(value).append(": counters cannot have a negative value");
131        Labels labels = getLabels();
132        if (labels != null && !labels.isEmpty()) {
133          message.append(" (labels=").append(labels).append(')');
134        }
135        throw new IllegalArgumentException(message.toString());
136      }
137    }
138
139    @Override
140    DataPointSnapshot escape(EscapingScheme escapingScheme) {
141      return new CounterSnapshot.CounterDataPointSnapshot(
142          value,
143          SnapshotEscaper.escapeLabels(getLabels(), escapingScheme),
144          SnapshotEscaper.escapeExemplar(exemplar, escapingScheme),
145          getCreatedTimestampMillis(),
146          getScrapeTimestampMillis(),
147          true,
148          metricName);
149    }
150
151    public static Builder builder() {
152      return new Builder();
153    }
154
155    public static class Builder extends DataPointSnapshot.Builder<Builder> {
156
157      @Nullable private Exemplar exemplar = null;
158      @Nullable private Double value = null;
159      private long createdTimestampMillis = 0L;
160      @Nullable private String metricName = null;
161
162      private Builder() {}
163
164      /** Counter value. This is required. The value must not be negative. */
165      public Builder value(double value) {
166        this.value = value;
167        return this;
168      }
169
170      public Builder exemplar(@Nullable Exemplar exemplar) {
171        this.exemplar = exemplar;
172        return this;
173      }
174
175      public Builder createdTimestampMillis(long createdTimestampMillis) {
176        this.createdTimestampMillis = createdTimestampMillis;
177        return this;
178      }
179
180      /**
181       * Optional metric name included in the exception message when {@link #value(double)} is
182       * negative. Does not change the snapshot identity.
183       */
184      public Builder metricName(@Nullable String metricName) {
185        this.metricName = metricName;
186        return this;
187      }
188
189      public CounterDataPointSnapshot build() {
190        if (value == null) {
191          throw new IllegalArgumentException("Missing required field: value is null.");
192        }
193        return new CounterDataPointSnapshot(
194            value,
195            labels,
196            exemplar,
197            createdTimestampMillis,
198            scrapeTimestampMillis,
199            false,
200            metricName);
201      }
202
203      @Override
204      protected Builder self() {
205        return this;
206      }
207    }
208  }
209
210  public static Builder builder() {
211    return new Builder();
212  }
213
214  public static class Builder extends MetricSnapshot.Builder<Builder> {
215
216    private final List<CounterDataPointSnapshot> dataPoints = new ArrayList<>();
217
218    private Builder() {}
219
220    /** Add a data point. Can be called multiple times to add multiple data points. */
221    public Builder dataPoint(CounterDataPointSnapshot dataPoint) {
222      dataPoints.add(dataPoint);
223      return this;
224    }
225
226    @Override
227    protected MetricMetadata buildMetadata() {
228      if (name == null) {
229        throw new IllegalArgumentException("Missing required field: name is null");
230      }
231      return MetricMetadataSupport.counterMetadata(name, help, unit);
232    }
233
234    @Override
235    public CounterSnapshot build() {
236      return new CounterSnapshot(buildMetadata(), dataPoints);
237    }
238
239    @Override
240    protected Builder self() {
241      return this;
242    }
243  }
244}