001package io.prometheus.client; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Collection; 006import java.util.List; 007import java.util.StringTokenizer; 008 009import static java.util.Collections.unmodifiableCollection; 010 011/** 012 * Filter samples (i.e. time series) by name. 013 */ 014public class SampleNameFilter implements Predicate<String> { 015 016 /** 017 * For convenience, a filter that allows all names. 018 */ 019 public static final Predicate<String> ALLOW_ALL = new AllowAll(); 020 021 private final Collection<String> nameIsEqualTo; 022 private final Collection<String> nameIsNotEqualTo; 023 private final Collection<String> nameStartsWith; 024 private final Collection<String> nameDoesNotStartWith; 025 026 @Override 027 public boolean test(String sampleName) { 028 return matchesNameEqualTo(sampleName) 029 && !matchesNameNotEqualTo(sampleName) 030 && matchesNameStartsWith(sampleName) 031 && !matchesNameDoesNotStartWith(sampleName); 032 } 033 034 /** 035 * Replacement for Java 8's {@code Predicate.and()} for compatibility with Java versions < 8. 036 */ 037 public Predicate<String> and(final Predicate<? super String> other) { 038 if (other == null) { 039 throw new NullPointerException(); 040 } 041 return new Predicate<String>() { 042 @Override 043 public boolean test(String s) { 044 return SampleNameFilter.this.test(s) && other.test(s); 045 } 046 }; 047 } 048 049 private boolean matchesNameEqualTo(String metricName) { 050 if (nameIsEqualTo.isEmpty()) { 051 return true; 052 } 053 return nameIsEqualTo.contains(metricName); 054 } 055 056 private boolean matchesNameNotEqualTo(String metricName) { 057 if (nameIsNotEqualTo.isEmpty()) { 058 return false; 059 } 060 return nameIsNotEqualTo.contains(metricName); 061 } 062 063 private boolean matchesNameStartsWith(String metricName) { 064 if (nameStartsWith.isEmpty()) { 065 return true; 066 } 067 for (String prefix : nameStartsWith) { 068 if (metricName.startsWith(prefix)) { 069 return true; 070 } 071 } 072 return false; 073 } 074 075 private boolean matchesNameDoesNotStartWith(String metricName) { 076 if (nameDoesNotStartWith.isEmpty()) { 077 return false; 078 } 079 for (String prefix : nameDoesNotStartWith) { 080 if (metricName.startsWith(prefix)) { 081 return true; 082 } 083 } 084 return false; 085 } 086 087 public static class Builder { 088 089 private final Collection<String> nameEqualTo = new ArrayList<String>(); 090 private final Collection<String> nameNotEqualTo = new ArrayList<String>(); 091 private final Collection<String> nameStartsWith = new ArrayList<String>(); 092 private final Collection<String> nameDoesNotStartWith = new ArrayList<String>(); 093 094 /** 095 * @see #nameMustBeEqualTo(Collection) 096 */ 097 public Builder nameMustBeEqualTo(String... names) { 098 return nameMustBeEqualTo(Arrays.asList(names)); 099 } 100 101 /** 102 * Only samples with one of the {@code names} will be included. 103 * <p> 104 * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name) 105 * and not the metric name. For instance, to retrieve all samples from a histogram, you must include the 106 * '_count', '_sum' and '_bucket' names. 107 * <p> 108 * This method should be used by HTTP exporters to implement the {@code ?name[]=} URL parameters. 109 * 110 * @param names empty means no restriction. 111 */ 112 public Builder nameMustBeEqualTo(Collection<String> names) { 113 nameEqualTo.addAll(names); 114 return this; 115 } 116 117 /** 118 * @see #nameMustNotBeEqualTo(Collection) 119 */ 120 public Builder nameMustNotBeEqualTo(String... names) { 121 return nameMustNotBeEqualTo(Arrays.asList(names)); 122 } 123 124 /** 125 * All samples that are not in {@code names} will be excluded. 126 * <p> 127 * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name) 128 * and not the metric name. For instance, to exclude all samples from a histogram, you must exclude the 129 * '_count', '_sum' and '_bucket' names. 130 * 131 * @param names empty means no name will be excluded. 132 */ 133 public Builder nameMustNotBeEqualTo(Collection<String> names) { 134 nameNotEqualTo.addAll(names); 135 return this; 136 } 137 138 /** 139 * @see #nameMustStartWith(Collection) 140 */ 141 public Builder nameMustStartWith(String... prefixes) { 142 return nameMustStartWith(Arrays.asList(prefixes)); 143 } 144 145 /** 146 * Only samples whose name starts with one of the {@code prefixes} will be included. 147 * @param prefixes empty means no restriction. 148 */ 149 public Builder nameMustStartWith(Collection<String> prefixes) { 150 nameStartsWith.addAll(prefixes); 151 return this; 152 } 153 154 /** 155 * @see #nameMustNotStartWith(Collection) 156 */ 157 public Builder nameMustNotStartWith(String... prefixes) { 158 return nameMustNotStartWith(Arrays.asList(prefixes)); 159 } 160 161 /** 162 * Samples with names starting with one of the {@code prefixes} will be excluded. 163 * @param prefixes empty means no time series will be excluded. 164 */ 165 public Builder nameMustNotStartWith(Collection<String> prefixes) { 166 nameDoesNotStartWith.addAll(prefixes); 167 return this; 168 } 169 170 public SampleNameFilter build() { 171 return new SampleNameFilter(nameEqualTo, nameNotEqualTo, nameStartsWith, nameDoesNotStartWith); 172 } 173 } 174 175 private SampleNameFilter(Collection<String> nameIsEqualTo, Collection<String> nameIsNotEqualTo, Collection<String> nameStartsWith, Collection<String> nameDoesNotStartWith) { 176 this.nameIsEqualTo = unmodifiableCollection(nameIsEqualTo); 177 this.nameIsNotEqualTo = unmodifiableCollection(nameIsNotEqualTo); 178 this.nameStartsWith = unmodifiableCollection(nameStartsWith); 179 this.nameDoesNotStartWith = unmodifiableCollection(nameDoesNotStartWith); 180 } 181 182 private static class AllowAll implements Predicate<String> { 183 184 private AllowAll() { 185 } 186 187 @Override 188 public boolean test(String s) { 189 return true; 190 } 191 } 192 193 /** 194 * Helper method to deserialize a {@code delimiter}-separated list of Strings into a {@code List<String>}. 195 * <p> 196 * {@code delimiter} is one of {@code , ; \t \n}. 197 * <p> 198 * This is implemented here so that exporters can provide a consistent configuration format for 199 * lists of allowed names. 200 */ 201 public static List<String> stringToList(String s) { 202 List<String> result = new ArrayList<String>(); 203 if (s != null) { 204 StringTokenizer tokenizer = new StringTokenizer(s, ",; \t\n"); 205 while (tokenizer.hasMoreTokens()) { 206 String token = tokenizer.nextToken(); 207 token = token.trim(); 208 if (token.length() > 0) { 209 result.add(token); 210 } 211 } 212 } 213 return result; 214 } 215 216 /** 217 * Helper method to compose a filter such that Sample names must 218 * <ul> 219 * <li>match the existing filter</li> 220 * <li>and be in the list of allowedNames</li> 221 * </ul> 222 * This should be used to implement the {@code names[]} query parameter in HTTP exporters. 223 * 224 * @param filter may be null, indicating that the resulting filter should just filter by {@code allowedNames}. 225 * @param allowedNames may be null or empty, indicating that {@code filter} is returned unmodified. 226 * @return a filter combining the exising {@code filter} and the {@code allowedNames}, or {@code null} 227 * if both parameters were {@code null}. 228 */ 229 public static Predicate<String> restrictToNamesEqualTo(Predicate<String> filter, Collection<String> allowedNames) { 230 if (allowedNames != null && !allowedNames.isEmpty()) { 231 SampleNameFilter allowedNamesFilter = new SampleNameFilter.Builder() 232 .nameMustBeEqualTo(allowedNames) 233 .build(); 234 if (filter == null) { 235 return allowedNamesFilter; 236 } else { 237 return allowedNamesFilter.and(filter); 238 } 239 } 240 return filter; 241 } 242}