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