001package io.prometheus.metrics.model.registry;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collection;
006import java.util.function.Predicate;
007
008import static java.util.Collections.unmodifiableCollection;
009
010/**
011 * Filter samples (i.e. time series) by name.
012 */
013public class MetricNameFilter implements Predicate<String> {
014
015    /**
016     * For convenience, a filter that allows all names.
017     */
018    public static final Predicate<String> ALLOW_ALL = name -> true;
019
020    private final Collection<String> nameIsEqualTo;
021    private final Collection<String> nameIsNotEqualTo;
022    private final Collection<String> nameStartsWith;
023    private final Collection<String> nameDoesNotStartWith;
024
025    private MetricNameFilter(Collection<String> nameIsEqualTo, Collection<String> nameIsNotEqualTo, Collection<String> nameStartsWith, Collection<String> nameDoesNotStartWith) {
026        this.nameIsEqualTo = unmodifiableCollection(new ArrayList<>(nameIsEqualTo));
027        this.nameIsNotEqualTo = unmodifiableCollection(new ArrayList<>(nameIsNotEqualTo));
028        this.nameStartsWith = unmodifiableCollection(new ArrayList<>(nameStartsWith));
029        this.nameDoesNotStartWith = unmodifiableCollection(new ArrayList<>(nameDoesNotStartWith));
030    }
031
032    @Override
033    public boolean test(String sampleName) {
034        return matchesNameEqualTo(sampleName)
035                && !matchesNameNotEqualTo(sampleName)
036                && matchesNameStartsWith(sampleName)
037                && !matchesNameDoesNotStartWith(sampleName);
038    }
039
040    private boolean matchesNameEqualTo(String metricName) {
041        if (nameIsEqualTo.isEmpty()) {
042            return true;
043        }
044        for (String name : nameIsEqualTo) {
045            // The following ignores suffixes like _total.
046            // "request_count" and "request_count_total" both match a metric named "request_count".
047            if (name.startsWith(metricName)) {
048                return true;
049            }
050        }
051        return false;
052    }
053
054    private boolean matchesNameNotEqualTo(String metricName) {
055        if (nameIsNotEqualTo.isEmpty()) {
056            return false;
057        }
058        for (String name : nameIsNotEqualTo) {
059            // The following ignores suffixes like _total.
060            // "request_count" and "request_count_total" both match a metric named "request_count".
061            if (name.startsWith(metricName)) {
062                return true;
063            }
064        }
065        return false;
066    }
067
068    private boolean matchesNameStartsWith(String metricName) {
069        if (nameStartsWith.isEmpty()) {
070            return true;
071        }
072        for (String prefix : nameStartsWith) {
073            if (metricName.startsWith(prefix)) {
074                return true;
075            }
076        }
077        return false;
078    }
079
080    private boolean matchesNameDoesNotStartWith(String metricName) {
081        if (nameDoesNotStartWith.isEmpty()) {
082            return false;
083        }
084        for (String prefix : nameDoesNotStartWith) {
085            if (metricName.startsWith(prefix)) {
086                return true;
087            }
088        }
089        return false;
090    }
091
092    public static Builder builder() {
093        return new Builder();
094    }
095
096    public static class Builder {
097
098        private final Collection<String> nameEqualTo = new ArrayList<>();
099        private final Collection<String> nameNotEqualTo = new ArrayList<>();
100        private final Collection<String> nameStartsWith = new ArrayList<>();
101        private final Collection<String> nameDoesNotStartWith = new ArrayList<>();
102
103        private Builder() {
104        }
105
106        /**
107         * @see #nameMustBeEqualTo(Collection)
108         */
109        public Builder nameMustBeEqualTo(String... names) {
110            return nameMustBeEqualTo(Arrays.asList(names));
111        }
112
113        /**
114         * Only samples with one of the {@code names} will be included.
115         * <p>
116         * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name)
117         * and not the metric name. For instance, to retrieve all samples from a histogram, you must include the
118         * '_count', '_sum' and '_bucket' names.
119         * <p>
120         * This method should be used by HTTP exporters to implement the {@code ?name[]=} URL parameters.
121         *
122         * @param names empty means no restriction.
123         */
124        public Builder nameMustBeEqualTo(Collection<String> names) {
125            if (names != null) {
126                nameEqualTo.addAll(names);
127            }
128            return this;
129        }
130
131        /**
132         * @see #nameMustNotBeEqualTo(Collection)
133         */
134        public Builder nameMustNotBeEqualTo(String... names) {
135            return nameMustNotBeEqualTo(Arrays.asList(names));
136        }
137
138        /**
139         * All samples that are not in {@code names} will be excluded.
140         * <p>
141         * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name)
142         * and not the metric name. For instance, to exclude all samples from a histogram, you must exclude the
143         * '_count', '_sum' and '_bucket' names.
144         *
145         * @param names empty means no name will be excluded.
146         */
147        public Builder nameMustNotBeEqualTo(Collection<String> names) {
148            if (names != null) {
149                nameNotEqualTo.addAll(names);
150            }
151            return this;
152        }
153
154        /**
155         * @see #nameMustStartWith(Collection)
156         */
157        public Builder nameMustStartWith(String... prefixes) {
158            return nameMustStartWith(Arrays.asList(prefixes));
159        }
160
161        /**
162         * Only samples whose name starts with one of the {@code prefixes} will be included.
163         *
164         * @param prefixes empty means no restriction.
165         */
166        public Builder nameMustStartWith(Collection<String> prefixes) {
167            if (prefixes != null) {
168                nameStartsWith.addAll(prefixes);
169            }
170            return this;
171        }
172
173        /**
174         * @see #nameMustNotStartWith(Collection)
175         */
176        public Builder nameMustNotStartWith(String... prefixes) {
177            return nameMustNotStartWith(Arrays.asList(prefixes));
178        }
179
180        /**
181         * Samples with names starting with one of the {@code prefixes} will be excluded.
182         *
183         * @param prefixes empty means no time series will be excluded.
184         */
185        public Builder nameMustNotStartWith(Collection<String> prefixes) {
186            if (prefixes != null) {
187                nameDoesNotStartWith.addAll(prefixes);
188            }
189            return this;
190        }
191
192        public MetricNameFilter build() {
193            return new MetricNameFilter(nameEqualTo, nameNotEqualTo, nameStartsWith, nameDoesNotStartWith);
194        }
195    }
196}