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