001package io.prometheus.metrics.instrumentation.dropwizard5.labels;
002
003import java.util.HashMap;
004import java.util.Map;
005import java.util.regex.Pattern;
006
007/**
008 * POJO containing info on how to map a graphite metric to a prometheus one.
009 * Example mapping in yaml format:
010 * <p>
011 * match: test.dispatcher.*.*.*
012 * name: dispatcher_events_total
013 * labels:
014 * action: ${1}
015 * outcome: ${2}_out
016 * processor: ${0}
017 * status: ${1}_${2}
018 * <p>
019 * Dropwizard metrics that match the "match" pattern will be further processed to have a new name and new labels based on this config.
020 */
021public class MapperConfig {
022    // each part of the metric name between dots
023    private static final String METRIC_PART_REGEX = "[a-zA-Z_0-9](-?[a-zA-Z0-9_])+";
024    // Simplified GLOB: we can have "*." at the beginning and "*" only at the end
025    static final String METRIC_GLOB_REGEX = "^(\\*\\.|" + METRIC_PART_REGEX + "\\.)+(\\*|" + METRIC_PART_REGEX + ")$";
026    // Labels validation.
027    private static final String LABEL_REGEX = "^[a-zA-Z_][a-zA-Z0-9_]+$";
028    private static final Pattern MATCH_EXPRESSION_PATTERN = Pattern.compile(METRIC_GLOB_REGEX);
029    private static final Pattern LABEL_PATTERN = Pattern.compile(LABEL_REGEX);
030
031    /**
032     * Regex used to match incoming metric name.
033     * Uses a simplified glob syntax where only '*' are allowed.
034     * E.g:
035     * org.company.controller.*.status.*
036     * Will be used to match
037     * org.company.controller.controller1.status.200
038     * and
039     * org.company.controller.controller2.status.400
040     */
041    private String match;
042
043    /**
044     * New metric name. Can contain placeholders to be replaced with actual values from the incoming metric name.
045     * Placeholders are in the ${n} format where n is the zero based index of the group to extract from the original metric name.
046     * E.g.:
047     * match: test.dispatcher.*.*.*
048     * name: dispatcher_events_total_${1}
049     * <p>
050     * A metric "test.dispatcher.old.test.yay" will be converted in a new metric with name "dispatcher_events_total_test"
051     */
052    private String name;
053
054    /**
055     * Labels to be extracted from the metric name.
056     * They should contain placeholders to be replaced with actual values from the incoming metric name.
057     * Placeholders are in the ${n} format where n is the zero based index of the group to extract from the original metric name.
058     * E.g.:
059     * match: test.dispatcher.*.*
060     * name: dispatcher_events_total_${0}
061     * labels:
062     * label1: ${1}_t
063     * <p>
064     * A metric "test.dispatcher.sp1.yay" will be converted in a new metric with name "dispatcher_events_total_sp1" with label {label1: yay_t}
065     * <p>
066     * Label names have to match the regex ^[a-zA-Z_][a-zA-Z0-9_]+$
067     */
068
069    private Map<String, String> labels = new HashMap<String, String>();
070
071    public MapperConfig() {
072        // empty constructor
073    }
074
075    // for tests
076    MapperConfig(final String match) {
077        validateMatch(match);
078        this.match = match;
079    }
080
081    public MapperConfig(final String match, final String name, final Map<String, String> labels) {
082        this.name = name;
083        validateMatch(match);
084        this.match = match;
085        validateLabels(labels);
086        this.labels = labels;
087    }
088
089    @Override
090    public String toString() {
091        return String.format("MapperConfig{match=%s, name=%s, labels=%s}", match, name, labels);
092    }
093
094    public String getMatch() {
095        return match;
096    }
097
098    public void setMatch(final String match) {
099        validateMatch(match);
100        this.match = match;
101    }
102
103    public String getName() {
104        return name;
105    }
106
107    public void setName(final String name) {
108        this.name = name;
109
110    }
111
112    public Map<String, String> getLabels() {
113        return labels;
114    }
115
116    public void setLabels(final Map<String, String> labels) {
117        validateLabels(labels);
118        this.labels = labels;
119    }
120
121    private void validateMatch(final String match)
122    {
123        if (!MATCH_EXPRESSION_PATTERN.matcher(match).matches()) {
124            throw new IllegalArgumentException(String.format("Match expression [%s] does not match required pattern %s", match, MATCH_EXPRESSION_PATTERN));
125        }
126    }
127
128    private void validateLabels(final Map<String, String> labels)
129    {
130        if (labels != null) {
131            for (final String key : labels.keySet()) {
132                if (!LABEL_PATTERN.matcher(key).matches()) {
133                    throw new IllegalArgumentException(String.format("Label [%s] does not match required pattern %s", match, LABEL_PATTERN));
134                }
135            }
136
137        }
138    }
139
140    @Override
141    public boolean equals(final Object o) {
142        if (this == o) return true;
143        if (o == null || getClass() != o.getClass()) return false;
144
145        final MapperConfig that = (MapperConfig) o;
146
147        if (match != null ? !match.equals(that.match) : that.match != null) return false;
148        if (name != null ? !name.equals(that.name) : that.name != null) return false;
149        return labels != null ? labels.equals(that.labels) : that.labels == null;
150    }
151
152    @Override
153    public int hashCode() {
154        int result = match != null ? match.hashCode() : 0;
155        result = 31 * result + (name != null ? name.hashCode() : 0);
156        result = 31 * result + (labels != null ? labels.hashCode() : 0);
157        return result;
158    }
159}