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}