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