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}