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 &lt; 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}