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}