001 002package io.prometheus.client; 003 004import io.prometheus.client.exemplars.Exemplar; 005 006import java.util.*; 007import java.util.regex.Pattern; 008 009/** 010 * A collector for a set of metrics. 011 * <p> 012 * Normal users should use {@link Gauge}, {@link Counter}, {@link Summary} and {@link Histogram}. 013 * <p> 014 * Subclasssing Collector is for advanced uses, such as proxying metrics from another monitoring system. 015 * It is it the responsibility of subclasses to ensure they produce valid metrics. 016 * @see <a href="http://prometheus.io/docs/instrumenting/exposition_formats/">Exposition formats</a>. 017 */ 018public abstract class Collector { 019 020 /** 021 * Return all metrics of this Collector. 022 */ 023 public abstract List<MetricFamilySamples> collect(); 024 025 /** 026 * Like {@link #collect()}, but the result should only contain {@code MetricFamilySamples} where 027 * {@code sampleNameFilter.test(name)} is {@code true} for at least one Sample name. 028 * <p> 029 * The default implementation first collects all {@code MetricFamilySamples} and then discards the ones 030 * where {@code sampleNameFilter.test(name)} returns {@code false} for all names in 031 * {@link MetricFamilySamples#getNames()}. 032 * To improve performance, collector implementations should override this method to prevent 033 * {@code MetricFamilySamples} from being collected if they will be discarded anyways. 034 * See {@code ThreadExports} for an example. 035 * <p> 036 * Note that the resulting List may contain {@code MetricFamilySamples} where some Sample names return 037 * {@code true} for {@code sampleNameFilter.test(name)} but some Sample names return {@code false}. 038 * This is ok, because before we produce the output format we will call 039 * {@link MetricFamilySamples#filter(Predicate)} to strip all Samples where {@code sampleNameFilter.test(name)} 040 * returns {@code false}. 041 * 042 * @param sampleNameFilter may be {@code null}, indicating that all metrics should be collected. 043 */ 044 public List<MetricFamilySamples> collect(Predicate<String> sampleNameFilter) { 045 List<MetricFamilySamples> all = collect(); 046 if (sampleNameFilter == null) { 047 return all; 048 } 049 List<MetricFamilySamples> remaining = new ArrayList<MetricFamilySamples>(all.size()); 050 for (MetricFamilySamples mfs : all) { 051 for (String name : mfs.getNames()) { 052 if (sampleNameFilter.test(name)) { 053 remaining.add(mfs); 054 break; 055 } 056 } 057 } 058 return remaining; 059 } 060 061 public enum Type { 062 UNKNOWN, // This is untyped in Prometheus text format. 063 COUNTER, 064 GAUGE, 065 STATE_SET, 066 INFO, 067 HISTOGRAM, 068 GAUGE_HISTOGRAM, 069 SUMMARY, 070 } 071 072 /** 073 * A metric, and all of its samples. 074 */ 075 static public class MetricFamilySamples { 076 public final String name; 077 public final String unit; 078 public final Type type; 079 public final String help; 080 public final List<Sample> samples; // this list is modified when samples are added/removed. 081 082 public MetricFamilySamples(String name, Type type, String help, List<Sample> samples) { 083 this(name, "", type, help, samples); 084 } 085 086 public MetricFamilySamples(String name, String unit, Type type, String help, List<Sample> samples) { 087 if (!unit.isEmpty() && !name.endsWith("_" + unit)) { 088 throw new IllegalArgumentException("Metric's unit is not the suffix of the metric name: " + name); 089 } 090 if ((type == Type.INFO || type == Type.STATE_SET) && !unit.isEmpty()) { 091 throw new IllegalArgumentException("Metric is of a type that cannot have a unit: " + name); 092 } 093 List<Sample> mungedSamples = samples; 094 // Deal with _total from pre-OM automatically. 095 if (type == Type.COUNTER) { 096 if (name.endsWith("_total")) { 097 name = name.substring(0, name.length() - 6); 098 } 099 String withTotal = name + "_total"; 100 mungedSamples = new ArrayList<Sample>(samples.size()); 101 for (Sample s: samples) { 102 String n = s.name; 103 if (name.equals(n)) { 104 n = withTotal; 105 } 106 mungedSamples.add(new Sample(n, s.labelNames, s.labelValues, s.value, s.exemplar, s.timestampMs)); 107 } 108 } 109 this.name = name; 110 this.unit = unit; 111 this.type = type; 112 this.help = help; 113 this.samples = mungedSamples; 114 } 115 116 /** 117 * @param sampleNameFilter may be {@code null} indicating that the result contains the complete list of samples. 118 * @return A new MetricFamilySamples containing only the Samples matching the {@code sampleNameFilter}, 119 * or {@code null} if no Sample matches. 120 */ 121 public MetricFamilySamples filter(Predicate<String> sampleNameFilter) { 122 if (sampleNameFilter == null) { 123 return this; 124 } 125 List<Sample> remainingSamples = new ArrayList<Sample>(samples.size()); 126 for (Sample sample : samples) { 127 if (sampleNameFilter.test(sample.name)) { 128 remainingSamples.add(sample); 129 } 130 } 131 if (remainingSamples.isEmpty()) { 132 return null; 133 } 134 return new MetricFamilySamples(name, unit, type, help, remainingSamples); 135 } 136 137 /** 138 * List of names that are reserved for Samples in these MetricsFamilySamples. 139 * <p> 140 * This is used in two places: 141 * <ol> 142 * <li>To check potential name collisions in {@link CollectorRegistry#register(Collector)}. 143 * <li>To check if a collector may contain metrics matching the metric name filter 144 * in {@link Collector#collect(Predicate)}. 145 * </ol> 146 * Note that {@code getNames()} always includes the name without suffix, even though some 147 * metrics types (like Counter) will not have a Sample with that name. 148 * The reason is that the name without suffix is used in the metadata comments ({@code # TYPE}, {@code # UNIT}, 149 * {@code # HELP}), and as this name <a href="https://github.com/prometheus/common/issues/319">must be unique</a> 150 * we include the name without suffix here as well. 151 */ 152 public String[] getNames() { 153 switch (type) { 154 case COUNTER: 155 return new String[]{ 156 name + "_total", 157 name + "_created", 158 name 159 }; 160 case SUMMARY: 161 return new String[]{ 162 name + "_count", 163 name + "_sum", 164 name + "_created", 165 name 166 }; 167 case HISTOGRAM: 168 return new String[]{ 169 name + "_count", 170 name + "_sum", 171 name + "_bucket", 172 name + "_created", 173 name 174 }; 175 case GAUGE_HISTOGRAM: 176 return new String[]{ 177 name + "_gcount", 178 name + "_gsum", 179 name + "_bucket", 180 name 181 }; 182 case INFO: 183 return new String[]{ 184 name + "_info", 185 name 186 }; 187 default: 188 return new String[]{name}; 189 } 190 } 191 192 193 @Override 194 public boolean equals(Object obj) { 195 if (!(obj instanceof MetricFamilySamples)) { 196 return false; 197 } 198 MetricFamilySamples other = (MetricFamilySamples) obj; 199 200 return other.name.equals(name) 201 && other.unit.equals(unit) 202 && other.type.equals(type) 203 && other.help.equals(help) 204 && other.samples.equals(samples); 205 } 206 207 @Override 208 public int hashCode() { 209 int hash = 1; 210 hash = 37 * hash + name.hashCode(); 211 hash = 37 * hash + unit.hashCode(); 212 hash = 37 * hash + type.hashCode(); 213 hash = 37 * hash + help.hashCode(); 214 hash = 37 * hash + samples.hashCode(); 215 return hash; 216 } 217 218 @Override 219 public String toString() { 220 return "Name: " + name + " Unit:" + unit + " Type: " + type + " Help: " + help + 221 " Samples: " + samples; 222 } 223 224 /** 225 * A single Sample, with a unique name and set of labels. 226 */ 227 public static class Sample { 228 public final String name; 229 public final List<String> labelNames; 230 public final List<String> labelValues; // Must have same length as labelNames. 231 public final double value; 232 public final Exemplar exemplar; 233 public final Long timestampMs; // It's an epoch format with milliseconds value included (this field is subject to change). 234 235 public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar, Long timestampMs) { 236 this.name = name; 237 this.labelNames = labelNames; 238 this.labelValues = labelValues; 239 this.value = value; 240 this.exemplar = exemplar; 241 this.timestampMs = timestampMs; 242 } 243 244 public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Long timestampMs) { 245 this(name, labelNames, labelValues, value, null, timestampMs); 246 } 247 248 public Sample(String name, List<String> labelNames, List<String> labelValues, double value, Exemplar exemplar) { 249 this(name, labelNames, labelValues, value, exemplar, null); 250 } 251 252 public Sample(String name, List<String> labelNames, List<String> labelValues, double value) { 253 this(name, labelNames, labelValues, value, null, null); 254 } 255 256 @Override 257 public boolean equals(Object obj) { 258 if (!(obj instanceof Sample)) { 259 return false; 260 } 261 Sample other = (Sample) obj; 262 263 return other.name.equals(name) && 264 other.labelNames.equals(labelNames) && 265 other.labelValues.equals(labelValues) && 266 other.value == value && 267 (exemplar == null && other.exemplar == null || other.exemplar != null && other.exemplar.equals(exemplar)) && 268 (timestampMs == null && other.timestampMs == null || other.timestampMs != null && other.timestampMs.equals(timestampMs)); 269 } 270 271 @Override 272 public int hashCode() { 273 int hash = 1; 274 hash = 37 * hash + name.hashCode(); 275 hash = 37 * hash + labelNames.hashCode(); 276 hash = 37 * hash + labelValues.hashCode(); 277 long d = Double.doubleToLongBits(value); 278 hash = 37 * hash + (int)(d ^ (d >>> 32)); 279 if (timestampMs != null) { 280 hash = 37 * hash + timestampMs.hashCode(); 281 } 282 if (exemplar != null) { 283 hash = 37 * exemplar.hashCode(); 284 } 285 return hash; 286 } 287 288 @Override 289 public String toString() { 290 return "Name: " + name + " LabelNames: " + labelNames + " labelValues: " + labelValues + 291 " Value: " + value + " TimestampMs: " + timestampMs; 292 } 293 } 294 } 295 296 /** 297 * Register the Collector with the default registry. 298 */ 299 public <T extends Collector> T register() { 300 return register(CollectorRegistry.defaultRegistry); 301 } 302 303 /** 304 * Register the Collector with the given registry. 305 */ 306 public <T extends Collector> T register(CollectorRegistry registry) { 307 registry.register(this); 308 return (T)this; 309 } 310 311 public interface Describable { 312 /** 313 * Provide a list of metric families this Collector is expected to return. 314 * 315 * These should exclude the samples. This is used by the registry to 316 * detect collisions and duplicate registrations. 317 * 318 * Usually custom collectors do not have to implement Describable. If 319 * Describable is not implemented and the CollectorRegistry was created 320 * with auto describe enabled (which is the case for the default registry) 321 * then {@link #collect} will be called at registration time instead of 322 * describe. If this could cause problems, either implement a proper 323 * describe, or if that's not practical have describe return an empty 324 * list. 325 */ 326 List<MetricFamilySamples> describe(); 327 } 328 329 330 /* Various utility functions for implementing Collectors. */ 331 332 /** 333 * Number of nanoseconds in a second. 334 */ 335 public static final double NANOSECONDS_PER_SECOND = 1E9; 336 /** 337 * Number of milliseconds in a second. 338 */ 339 public static final double MILLISECONDS_PER_SECOND = 1E3; 340 341 private static final Pattern METRIC_NAME_RE = Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*"); 342 private static final Pattern METRIC_LABEL_NAME_RE = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); 343 private static final Pattern RESERVED_METRIC_LABEL_NAME_RE = Pattern.compile("__.*"); 344 345 /** 346 * Throw an exception if the metric name is invalid. 347 */ 348 protected static void checkMetricName(String name) { 349 if (!METRIC_NAME_RE.matcher(name).matches()) { 350 throw new IllegalArgumentException("Invalid metric name: " + name); 351 } 352 } 353 354 /** 355 * Sanitize metric name 356 */ 357 public static String sanitizeMetricName(String metricName) { 358 int length = metricName.length(); 359 char[] sanitized = new char[length]; 360 for(int i = 0; i < length; i++) { 361 char ch = metricName.charAt(i); 362 if(ch == ':' || 363 (ch >= 'a' && ch <= 'z') || 364 (ch >= 'A' && ch <= 'Z') || 365 (i > 0 && ch >= '0' && ch <= '9')) { 366 sanitized[i] = ch; 367 } else { 368 sanitized[i] = '_'; 369 } 370 } 371 return new String(sanitized); 372 } 373 374 /** 375 * Throw an exception if the metric label name is invalid. 376 */ 377 protected static void checkMetricLabelName(String name) { 378 if (!METRIC_LABEL_NAME_RE.matcher(name).matches()) { 379 throw new IllegalArgumentException("Invalid metric label name: " + name); 380 } 381 if (RESERVED_METRIC_LABEL_NAME_RE.matcher(name).matches()) { 382 throw new IllegalArgumentException("Invalid metric label name, reserved for internal use: " + name); 383 } 384 } 385 386 /** 387 * Convert a double to its string representation in Go. 388 */ 389 public static String doubleToGoString(double d) { 390 if (d == Double.POSITIVE_INFINITY) { 391 return "+Inf"; 392 } 393 if (d == Double.NEGATIVE_INFINITY) { 394 return "-Inf"; 395 } 396 return Double.toString(d); 397 } 398}