001package io.prometheus.client; 002 003import java.util.concurrent.ConcurrentHashMap; 004import java.util.concurrent.ConcurrentMap; 005import java.util.Arrays; 006import java.util.List; 007 008/** 009 * Common functionality for {@link Gauge}, {@link Counter} and {@link Summary}. 010 * <p> 011 * This class handles common initlization and label logic for the standard metrics. 012 * You should never need to subclass this class. 013 * <p> 014 * <h2>Initilization</h2> 015 * After calling build() on a subclass, {@link Builder#name(String) name}, 016 * {@link SimpleCollector.Builder#help(String) help}, 017 * {@link SimpleCollector.Builder#labelNames(String...) labelNames}, 018 * {@link SimpleCollector.Builder#namespace(String) namespace} and 019 * {@link SimpleCollector.Builder#subsystem(String) subsystem} can be called to configure the Collector. 020 * These return <code>this</code> to allow calls to be chained. 021 * Once configured, call {@link SimpleCollector.Builder#create create} 022 * (which is also called by {@link SimpleCollector.Builder#register register}). 023 * <p> 024 * The fullname of the metric is <code>namespace_subsystem_name</code>, but only <code>name</code> is required. 025 * 026 * <h2>Labels</h2> 027 * {@link SimpleCollector.Builder#labelNames labelNames} specifies which (if any) labels the metrics will have, and 028 * {@link #labels} returns the Child of the metric that represents that particular set of labels. 029 * {@link Gauge}, {@link Counter} and {@link Summary} all offer convienence methods to avoid needing to call 030 * {@link #labels} for metrics with no labels. 031 * <p> 032 * {@link #remove} and {@link #clear} can be used to remove children. 033 * <p> 034 * <em>Warning #1:</em> Metrics that don't always export something are difficult to monitor, if you know in advance 035 * what labels will be in use you should initilise them be calling {@link #labels}. 036 * This is done for you for metrics with no labels. 037 * <p> 038 * <em>Warning #2:</em> While labels are very powerful, avoid overly granular metric labels. 039 * The combinatorial explosion of breaking out a metric in many dimensions can produce huge numberts 040 * of timeseries, which will then take longer and more resources to process. 041 * <p> 042 * As a rule of thumb aim to keep the cardinality of metrics below ten, and limit where the 043 * cardinality exceeds that value. For example rather than breaking out latency 044 * by customer and endpoint in one metric, you might have two metrics with one breaking out 045 * by each. If the cardinality is in the hundreds, you may wish to consider removing the breakout 046 * by one of the dimensions altogether. 047 */ 048public abstract class SimpleCollector<Child> extends Collector { 049 protected final String fullname; 050 protected final String help; 051 protected final List<String> labelNames; 052 053 protected final ConcurrentMap<List<String>, Child> children = new ConcurrentHashMap<List<String>, Child>();; 054 protected Child noLabelsChild; 055 056 /** 057 * Return the Child with the given labels, creating it if needed. 058 * <p> 059 * Must be passed the same number of labels are were passed to {@link #labelNames}. 060 */ 061 public Child labels(String... labelValues) { 062 if (labelValues.length != labelNames.size()) { 063 throw new IllegalArgumentException("Incorrect number of labels."); 064 } 065 for (String label: labelValues) { 066 if (label == null) { 067 throw new IllegalArgumentException("Label cannot be null."); 068 } 069 } 070 List<String> key = Arrays.asList(labelValues); 071 Child c = children.get(key); 072 if (c != null) { 073 return c; 074 } 075 children.putIfAbsent(key, newChild()); 076 return children.get(key); 077 } 078 079 /** 080 * Remove the Child with the given labels. 081 * <p> 082 * Any references to the Child are invalidated. 083 */ 084 public void remove(String... labelValues) { 085 children.remove(Arrays.asList(labelValues)); 086 initializeNoLabelsChild(); 087 } 088 089 /** 090 * Remove all children. 091 * <p> 092 * Any references to any children are invalidated. 093 */ 094 public void clear() { 095 children.clear(); 096 initializeNoLabelsChild(); 097 } 098 099 /** 100 * Initialize the child with no labels. 101 */ 102 protected void initializeNoLabelsChild() { 103 // Initlize metric if it has no labels. 104 if (labelNames.size() == 0) { 105 noLabelsChild = labels(); 106 } 107 } 108 109 /** 110 * Replace the Child with the given labels. 111 * <p> 112 * This is intended for advanced uses, in particular proxying metrics 113 * from another monitoring system. This allows for callbacks for returning 114 * values for {@link Counter} and {@link Gauge} without having to implement 115 * a full {@link Collector}. 116 * <p> 117 * An example with {@link Gauge}: 118 * <pre> 119 * {@code 120 * Gauge.build().name("current_time").help("Current unixtime.").create() 121 * .setChild(new Gauge.Child() { 122 * public double get() { 123 * return System.currentTimeMillis() / MILLISECONDS_PER_SECOND; 124 * } 125 * }).register(); 126 * } 127 * </pre> 128 * <p> 129 * Any references any previous Child with these labelValues are invalidated. 130 * A metric should be either all callbacks, or none. 131 */ 132 public <T extends Collector> T setChild(Child child, String... labelValues) { 133 if (labelValues.length != labelNames.size()) { 134 throw new IllegalArgumentException("Incorrect number of labels."); 135 } 136 children.put(Arrays.asList(labelValues), child); 137 return (T)this; 138 } 139 140 /** 141 * Return a new child, workaround for Java generics limitations. 142 */ 143 protected abstract Child newChild(); 144 145 protected SimpleCollector(Builder b) { 146 if (b.name.isEmpty()) throw new IllegalStateException("Name hasn't been set."); 147 String name = b.name; 148 if (!b.subsystem.isEmpty()) { 149 name = b.subsystem + '_' + name; 150 } 151 if (!b.namespace.isEmpty()) { 152 name = b.namespace + '_' + name; 153 } 154 fullname = name; 155 checkMetricName(fullname); 156 if (b.help.isEmpty()) throw new IllegalStateException("Help hasn't been set."); 157 help = b.help; 158 labelNames = Arrays.asList(b.labelNames); 159 160 for (String n: labelNames) { 161 checkMetricLabelName(n); 162 } 163 164 if (!b.dontInitializeNoLabelsChild) { 165 initializeNoLabelsChild(); 166 } 167 } 168 169 /** 170 * Builders let you configure and then create collectors. 171 */ 172 public abstract static class Builder<B extends Builder<B, C>, C extends SimpleCollector> { 173 String namespace = ""; 174 String subsystem = ""; 175 String name = ""; 176 String fullname = ""; 177 String help = ""; 178 String[] labelNames = new String[]{}; 179 // Some metrics require additional setup before the initialization can be done. 180 boolean dontInitializeNoLabelsChild; 181 182 /** 183 * Set the name of the metric. Required. 184 */ 185 public B name(String name) { 186 this.name = name; 187 return (B)this; 188 } 189 /** 190 * Set the subsystem of the metric. Optional. 191 */ 192 public B subsystem(String subsystem) { 193 this.subsystem = subsystem; 194 return (B)this; 195 } 196 /** 197 * Set the namespace of the metric. Optional. 198 */ 199 public B namespace(String namespace) { 200 this.namespace = namespace; 201 return (B)this; 202 } 203 /** 204 * Set the help string of the metric. Required. 205 */ 206 public B help(String help) { 207 this.help = help; 208 return (B)this; 209 } 210 /** 211 * Set the labelNames of the metric. Optional, defaults to no labels. 212 */ 213 public B labelNames(String... labelNames) { 214 this.labelNames = labelNames; 215 return (B)this; 216 } 217 218 /** 219 * Return the constructed collector. 220 * <p> 221 * Abstract due to generics limitations. 222 */ 223 public abstract C create(); 224 225 /** 226 * Create and register the Collector with the default registry. 227 */ 228 public C register() { 229 return register(CollectorRegistry.defaultRegistry); 230 } 231 232 /** 233 * Create and register the Collector with the given registry. 234 */ 235 public C register(CollectorRegistry registry) { 236 C sc = create(); 237 registry.register(sc); 238 return sc; 239 } 240 } 241}