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. Accepts letters, digits, underscores, hyphens,
020  // colons, and glob wildcards (*) to support a broad range of metric naming conventions.
021  private static final String METRIC_PART_REGEX = "[a-zA-Z_0-9*][a-zA-Z0-9_:*-]*";
022  // Simplified GLOB: accepts single-level names, dot-separated names, and glob patterns with '*'.
023  // The pattern requires at least one non-empty segment and does not allow empty segments (double
024  // dots) or empty/whitespace-only strings. The '**' glob is rejected separately in validateMatch.
025  static final String METRIC_GLOB_REGEX =
026      "^(" + METRIC_PART_REGEX + ")(\\." + METRIC_PART_REGEX + ")*$";
027  // Labels validation.
028  private static final String LABEL_REGEX = "^[a-zA-Z_][a-zA-Z0-9_]+$";
029  private static final Pattern MATCH_EXPRESSION_PATTERN = Pattern.compile(METRIC_GLOB_REGEX);
030  private static final Pattern LABEL_PATTERN = Pattern.compile(LABEL_REGEX);
031
032  /**
033   * Regex used to match incoming metric name. Uses a simplified glob syntax where only '*' are
034   * allowed. E.g: org.company.controller.*.status.* Will be used to match
035   * org.company.controller.controller1.status.200 and org.company.controller.controller2.status.400
036   */
037  @Nullable private String match;
038
039  /**
040   * New metric name. Can contain placeholders to be replaced with actual values from the incoming
041   * metric name. Placeholders are in the ${n} format where n is the zero based index of the group
042   * to extract from the original metric name. E.g.: match: test.dispatcher.*.*.* name:
043   * dispatcher_events_total_${1}
044   *
045   * <p>A metric "test.dispatcher.old.test.yay" will be converted in a new metric with name
046   * "dispatcher_events_total_test"
047   */
048  @Nullable private String name;
049
050  /**
051   * Labels to be extracted from the metric name. They should contain placeholders to be replaced
052   * with actual values from the incoming metric name. Placeholders are in the ${n} format where n
053   * is the zero based index of the group to extract from the original metric name. E.g.: match:
054   * test.dispatcher.*.* name: dispatcher_events_total_${0} labels: label1: ${1}_t
055   *
056   * <p>A metric "test.dispatcher.sp1.yay" will be converted in a new metric with name
057   * "dispatcher_events_total_sp1" with label {label1: yay_t}
058   *
059   * <p>Label names have to match the regex ^[a-zA-Z_][a-zA-Z0-9_]+$
060   */
061  private Map<String, String> labels = new HashMap<>();
062
063  public MapperConfig() {
064    // empty constructor
065  }
066
067  // for tests
068  MapperConfig(String match) {
069    validateMatch(match);
070    this.match = match;
071  }
072
073  public MapperConfig(String match, String name, Map<String, String> labels) {
074    this.name = name;
075    validateMatch(match);
076    this.match = match;
077    validateLabels(labels);
078    this.labels = labels;
079  }
080
081  @Override
082  public String toString() {
083    return String.format("MapperConfig{match=%s, name=%s, labels=%s}", match, name, labels);
084  }
085
086  @Nullable
087  public String getMatch() {
088    return match;
089  }
090
091  public void setMatch(String match) {
092    validateMatch(match);
093    this.match = match;
094  }
095
096  @Nullable
097  public String getName() {
098    return name;
099  }
100
101  public void setName(String name) {
102    this.name = name;
103  }
104
105  public Map<String, String> getLabels() {
106    return labels;
107  }
108
109  public void setLabels(Map<String, String> labels) {
110    validateLabels(labels);
111    this.labels = labels;
112  }
113
114  private void validateMatch(String match) {
115    if (match.contains("**")) {
116      throw new IllegalArgumentException(
117          String.format("Match expression [%s] must not contain '**' (double-star glob)", match));
118    }
119    if (!MATCH_EXPRESSION_PATTERN.matcher(match).matches()) {
120      throw new IllegalArgumentException(
121          String.format(
122              "Match expression [%s] does not match required pattern %s",
123              match, MATCH_EXPRESSION_PATTERN));
124    }
125  }
126
127  private void validateLabels(Map<String, String> labels) {
128    if (labels != null) {
129      for (String key : labels.keySet()) {
130        if (!LABEL_PATTERN.matcher(key).matches()) {
131          throw new IllegalArgumentException(
132              String.format("Label [%s] does not match required pattern %s", match, LABEL_PATTERN));
133        }
134      }
135    }
136  }
137
138  @Override
139  public boolean equals(Object o) {
140    if (this == o) {
141      return true;
142    }
143    if (o == null || getClass() != o.getClass()) {
144      return false;
145    }
146
147    final MapperConfig that = (MapperConfig) o;
148
149    if (match != null ? !match.equals(that.match) : that.match != null) {
150      return false;
151    }
152    if (name != null ? !name.equals(that.name) : that.name != null) {
153      return false;
154    }
155    return labels != null ? labels.equals(that.labels) : that.labels == null;
156  }
157
158  @Override
159  public int hashCode() {
160    int result = match != null ? match.hashCode() : 0;
161    result = 31 * result + (name != null ? name.hashCode() : 0);
162    result = 31 * result + (labels != null ? labels.hashCode() : 0);
163    return result;
164  }
165}