001package io.prometheus.client.exporter.common;
002
003import java.io.IOException;
004import java.io.StringWriter;
005import java.io.Writer;
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.Enumeration;
009import java.util.Map;
010import java.util.TreeMap;
011
012import io.prometheus.client.Collector;
013
014public class TextFormat {
015  /**
016   * Content-type for Prometheus text version 0.0.4.
017   */
018  public final static String CONTENT_TYPE_004 = "text/plain; version=0.0.4; charset=utf-8";
019
020  /**
021   * Content-type for Openmetrics text version 1.0.0.
022   *
023   * @since 0.10.0
024   */
025  public final static String CONTENT_TYPE_OPENMETRICS_100 = "application/openmetrics-text; version=1.0.0; charset=utf-8";
026
027  /**
028   * Return the content type that should be used for a given Accept HTTP header.
029   *
030   * @since 0.10.0
031   */
032  public static String chooseContentType(String acceptHeader) {
033    if (acceptHeader == null) {
034      return CONTENT_TYPE_004;
035    }
036
037    for (String accepts : acceptHeader.split(",")) {
038      if ("application/openmetrics-text".equals(accepts.split(";")[0].trim())) {
039        return CONTENT_TYPE_OPENMETRICS_100;
040      }
041    }
042
043    return CONTENT_TYPE_004;
044  }
045
046  /**
047   * Write out the given MetricFamilySamples in a format per the contentType.
048   *
049   * @since 0.10.0
050   */
051  public static void writeFormat(String contentType, Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
052    if (CONTENT_TYPE_004.equals(contentType)) {
053        write004(writer, mfs);
054        return;
055    }
056    if (CONTENT_TYPE_OPENMETRICS_100.equals(contentType)) {
057        writeOpenMetrics100(writer, mfs);
058        return;
059    }
060    throw new IllegalArgumentException("Unknown contentType " + contentType);
061  }
062
063  /**
064   * Write out the text version 0.0.4 of the given MetricFamilySamples.
065   */
066  public static void write004(Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
067    Map<String, Collector.MetricFamilySamples> omFamilies = new TreeMap<String, Collector.MetricFamilySamples>();
068    /* See http://prometheus.io/docs/instrumenting/exposition_formats/
069     * for the output format specification. */
070    while(mfs.hasMoreElements()) {
071      Collector.MetricFamilySamples metricFamilySamples = mfs.nextElement();
072      String name = metricFamilySamples.name;
073      writer.write("# HELP ");
074      writer.write(name);
075      if (metricFamilySamples.type == Collector.Type.COUNTER) {
076        writer.write("_total");
077      }
078      if (metricFamilySamples.type == Collector.Type.INFO) {
079        writer.write("_info");
080      }
081      writer.write(' ');
082      writeEscapedHelp(writer, metricFamilySamples.help);
083      writer.write('\n');
084
085      writer.write("# TYPE ");
086      writer.write(name);
087      if (metricFamilySamples.type == Collector.Type.COUNTER) {
088        writer.write("_total");
089      }
090      if (metricFamilySamples.type == Collector.Type.INFO) {
091        writer.write("_info");
092      }
093      writer.write(' ');
094      writer.write(typeString(metricFamilySamples.type));
095      writer.write('\n');
096
097      String createdName = name + "_created";
098      String gcountName = name + "_gcount";
099      String gsumName = name + "_gsum";
100      for (Collector.MetricFamilySamples.Sample sample: metricFamilySamples.samples) {
101        /* OpenMetrics specific sample, put in a gauge at the end. */
102        if (sample.name.equals(createdName)
103            || sample.name.equals(gcountName)
104            || sample.name.equals(gsumName)) {
105          Collector.MetricFamilySamples omFamily = omFamilies.get(sample.name);
106          if (omFamily == null) {
107            omFamily = new Collector.MetricFamilySamples(sample.name, Collector.Type.GAUGE, metricFamilySamples.help, new ArrayList<Collector.MetricFamilySamples.Sample>());
108            omFamilies.put(sample.name, omFamily);
109          }
110          omFamily.samples.add(sample);
111          continue;
112        }
113        writer.write(sample.name);
114        if (sample.labelNames.size() > 0) {
115          writer.write('{');
116          for (int i = 0; i < sample.labelNames.size(); ++i) {
117            writer.write(sample.labelNames.get(i));
118            writer.write("=\"");
119            writeEscapedLabelValue(writer, sample.labelValues.get(i));
120            writer.write("\",");
121          }
122          writer.write('}');
123        }
124        writer.write(' ');
125        writer.write(Collector.doubleToGoString(sample.value));
126        if (sample.timestampMs != null){
127          writer.write(' ');
128          writer.write(sample.timestampMs.toString());
129        }
130        writer.write('\n');
131      }
132    }
133    // Write out any OM-specific samples.
134    if (!omFamilies.isEmpty()) {
135      write004(writer, Collections.enumeration(omFamilies.values()));
136    }
137  }
138
139  private static void writeEscapedHelp(Writer writer, String s) throws IOException {
140    for (int i = 0; i < s.length(); i++) {
141      char c = s.charAt(i);
142      switch (c) {
143        case '\\':
144          writer.append("\\\\");
145          break;
146        case '\n':
147          writer.append("\\n");
148          break;
149        default:
150          writer.append(c);
151      }
152    }
153  }
154
155  private static void writeEscapedLabelValue(Writer writer, String s) throws IOException {
156    for (int i = 0; i < s.length(); i++) {
157      char c = s.charAt(i);
158      switch (c) {
159        case '\\':
160          writer.append("\\\\");
161          break;
162        case '\"':
163          writer.append("\\\"");
164          break;
165        case '\n':
166          writer.append("\\n");
167          break;
168        default:
169          writer.append(c);
170      }
171    }
172  }
173
174  private static String typeString(Collector.Type t) {
175    switch (t) {
176      case GAUGE:
177        return "gauge";
178      case COUNTER:
179        return "counter";
180      case SUMMARY:
181        return "summary";
182      case HISTOGRAM:
183        return "histogram";
184      case GAUGE_HISTOGRAM:
185        return "histogram";
186      case STATE_SET:
187        return "gauge";
188      case INFO:
189        return "gauge";
190      default:
191        return "untyped";
192    }
193  }
194
195  /**
196   * Write out the OpenMetrics text version 1.0.0 of the given MetricFamilySamples.
197   *
198   * @since 0.10.0
199   */
200  public static void writeOpenMetrics100(Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
201    while(mfs.hasMoreElements()) {
202      Collector.MetricFamilySamples metricFamilySamples = mfs.nextElement();
203      String name = metricFamilySamples.name;
204
205      writer.write("# TYPE ");
206      writer.write(name);
207      writer.write(' ');
208      writer.write(omTypeString(metricFamilySamples.type));
209      writer.write('\n');
210
211      if (!metricFamilySamples.unit.isEmpty()) {
212        writer.write("# UNIT ");
213        writer.write(name);
214        writer.write(' ');
215        writer.write(metricFamilySamples.unit);
216        writer.write('\n');
217      }
218
219      writer.write("# HELP ");
220      writer.write(name);
221      writer.write(' ');
222      writeEscapedLabelValue(writer, metricFamilySamples.help);
223      writer.write('\n');
224 
225      for (Collector.MetricFamilySamples.Sample sample: metricFamilySamples.samples) {
226        writer.write(sample.name);
227        if (sample.labelNames.size() > 0) {
228          writer.write('{');
229          for (int i = 0; i < sample.labelNames.size(); ++i) {
230            if (i > 0) {
231              writer.write(",");
232            }
233            writer.write(sample.labelNames.get(i));
234            writer.write("=\"");
235            writeEscapedLabelValue(writer, sample.labelValues.get(i));
236            writer.write("\"");
237          }
238          writer.write('}');
239        }
240        writer.write(' ');
241        writer.write(Collector.doubleToGoString(sample.value));
242        if (sample.timestampMs != null){
243          writer.write(' ');
244          omWriteTimestamp(writer, sample.timestampMs);
245        }
246        if (sample.exemplar != null) {
247          writer.write(" # {");
248          for (int i=0; i<sample.exemplar.getNumberOfLabels(); i++) {
249            if (i > 0) {
250              writer.write(",");
251            }
252            writer.write(sample.exemplar.getLabelName(i));
253            writer.write("=\"");
254            writeEscapedLabelValue(writer, sample.exemplar.getLabelValue(i));
255            writer.write("\"");
256          }
257          writer.write("} ");
258          writer.write(Collector.doubleToGoString(sample.exemplar.getValue()));
259          if (sample.exemplar.getTimestampMs() != null) {
260            writer.write(' ');
261            omWriteTimestamp(writer, sample.exemplar.getTimestampMs());
262          }
263        }
264        writer.write('\n');
265      }
266    }
267    writer.write("# EOF\n");
268  }
269
270  static void omWriteTimestamp(Writer writer, long timestampMs) throws IOException {
271    writer.write(Long.toString(timestampMs / 1000L));
272    writer.write(".");
273    long ms = timestampMs % 1000;
274    if (ms < 100) {
275      writer.write("0");
276    }
277    if (ms < 10) {
278      writer.write("0");
279    }
280    writer.write(Long.toString(timestampMs % 1000));
281  }
282
283  private static String omTypeString(Collector.Type t) {
284    switch (t) {
285      case GAUGE:
286        return "gauge";
287      case COUNTER:
288        return "counter";
289      case SUMMARY:
290        return "summary";
291      case HISTOGRAM:
292        return "histogram";
293      case GAUGE_HISTOGRAM:
294        return "gauge_histogram";
295      case STATE_SET:
296        return "stateset";
297      case INFO:
298        return "info";
299      default:
300        return "unknown";
301    }
302  }
303}