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}