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