|
| 1 | +/* |
| 2 | + * Copyright The OpenTelemetry Authors |
| 3 | + * SPDX-License-Identifier: Apache-2.0 |
| 4 | + */ |
| 5 | + |
| 6 | +package io.opentelemetry.exporter.prometheus; |
| 7 | + |
| 8 | +import static io.opentelemetry.exporter.prometheus.NameSanitizer.SANITIZE_CONSECUTIVE_UNDERSCORES; |
| 9 | + |
| 10 | +import io.opentelemetry.api.internal.StringUtils; |
| 11 | +import java.util.regex.Pattern; |
| 12 | + |
| 13 | +/** |
| 14 | + * A utility class that contains helper function(s) to aid conversion from OTLP to Prometheus units. |
| 15 | + * |
| 16 | + * @see <a |
| 17 | + * href="https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units">OpenMetrics |
| 18 | + * specification for units</a> |
| 19 | + * @see <a href="https://prometheus.io/docs/practices/naming/#base-units">Prometheus best practices |
| 20 | + * for units</a> |
| 21 | + */ |
| 22 | +final class PrometheusUnitsHelper { |
| 23 | + |
| 24 | + private static final Pattern INVALID_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9]"); |
| 25 | + private static final Pattern CHARACTERS_BETWEEN_BRACES_PATTERN = Pattern.compile("\\{(.*?)}"); |
| 26 | + private static final Pattern SANITIZE_LEADING_UNDERSCORES = Pattern.compile("^_+"); |
| 27 | + private static final Pattern SANITIZE_TRAILING_UNDERSCORES = Pattern.compile("_+$"); |
| 28 | + |
| 29 | + private PrometheusUnitsHelper() { |
| 30 | + // Prevent object creation for utility classes |
| 31 | + } |
| 32 | + |
| 33 | + /** |
| 34 | + * A utility function that returns the equivalent Prometheus name for the provided OTLP metric |
| 35 | + * unit. |
| 36 | + * |
| 37 | + * @param rawMetricUnitName The raw metric unit for which Prometheus metric unit needs to be |
| 38 | + * computed. |
| 39 | + * @return the computed Prometheus metric unit equivalent of the OTLP metric un |
| 40 | + */ |
| 41 | + static String getEquivalentPrometheusUnit(String rawMetricUnitName) { |
| 42 | + if (StringUtils.isNullOrEmpty(rawMetricUnitName)) { |
| 43 | + return rawMetricUnitName; |
| 44 | + } |
| 45 | + // Drop units specified between curly braces |
| 46 | + String convertedMetricUnitName = removeUnitPortionInBraces(rawMetricUnitName); |
| 47 | + // Handling for the "per" unit(s), e.g. foo/bar -> foo_per_bar |
| 48 | + convertedMetricUnitName = convertRateExpressedToPrometheusUnit(convertedMetricUnitName); |
| 49 | + // Converting abbreviated unit names to full names |
| 50 | + return cleanUpString(getPrometheusUnit(convertedMetricUnitName)); |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * This method is used to convert the units expressed as a rate via '/' symbol in their name to |
| 55 | + * their expanded text equivalent. For instance, km/h => km_per_hour. The method operates on the |
| 56 | + * input by splitting it in 2 parts - before and after '/' symbol and will attempt to expand any |
| 57 | + * known unit abbreviation in both parts. Unknown abbreviations & unsupported characters will |
| 58 | + * remain unchanged in the final output of this function. |
| 59 | + * |
| 60 | + * @param rateExpressedUnit The rate unit input that needs to be converted to its text equivalent. |
| 61 | + * @return The text equivalent of unit expressed as rate. If the input does not contain '/', the |
| 62 | + * function returns it as-is. |
| 63 | + */ |
| 64 | + private static String convertRateExpressedToPrometheusUnit(String rateExpressedUnit) { |
| 65 | + if (!rateExpressedUnit.contains("/")) { |
| 66 | + return rateExpressedUnit; |
| 67 | + } |
| 68 | + String[] rateEntities = rateExpressedUnit.split("/", 2); |
| 69 | + // Only convert rate expressed units if it's a valid expression |
| 70 | + if (rateEntities[1].equals("")) { |
| 71 | + return rateExpressedUnit; |
| 72 | + } |
| 73 | + return getPrometheusUnit(rateEntities[0]) + "_per_" + getPrometheusPerUnit(rateEntities[1]); |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * This method drops all characters enclosed within '{}' (including the curly braces) by replacing |
| 78 | + * them with an empty string. Note that this method will not produce the intended effect if there |
| 79 | + * are nested curly braces within the outer enclosure of '{}'. |
| 80 | + * |
| 81 | + * <p>For instance, {packet{s}s} => s}. |
| 82 | + * |
| 83 | + * @param unit The input unit from which text within curly braces needs to be removed. |
| 84 | + * @return The resulting unit after removing the text within '{}'. |
| 85 | + */ |
| 86 | + private static String removeUnitPortionInBraces(String unit) { |
| 87 | + return CHARACTERS_BETWEEN_BRACES_PATTERN.matcher(unit).replaceAll(""); |
| 88 | + } |
| 89 | + |
| 90 | + /** |
| 91 | + * Replaces all characters that are not a letter or a digit with '_' to make the resulting string |
| 92 | + * Prometheus compliant. This method also removes leading and trailing underscores - this is done |
| 93 | + * to keep the resulting unit similar to what is produced from the collector's implementation. |
| 94 | + * |
| 95 | + * @param string The string input that needs to be made Prometheus compliant. |
| 96 | + * @return the cleaned-up Prometheus compliant string. |
| 97 | + */ |
| 98 | + private static String cleanUpString(String string) { |
| 99 | + return SANITIZE_LEADING_UNDERSCORES |
| 100 | + .matcher( |
| 101 | + SANITIZE_TRAILING_UNDERSCORES |
| 102 | + .matcher( |
| 103 | + SANITIZE_CONSECUTIVE_UNDERSCORES |
| 104 | + .matcher(INVALID_CHARACTERS_PATTERN.matcher(string).replaceAll("_")) |
| 105 | + .replaceAll("_")) |
| 106 | + .replaceAll("")) |
| 107 | + .replaceAll(""); |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * This method retrieves the expanded Prometheus unit name for known abbreviations. OTLP metrics |
| 112 | + * use the c/s notation as specified at <a href="https://ucum.org/ucum.html">UCUM</a>. The list of |
| 113 | + * mappings is adopted from <a |
| 114 | + * href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/9a9d4778bbbf242dba233db28e2fbcfda3416959/pkg/translator/prometheus/normalize_name.go#L30">OpenTelemetry |
| 115 | + * Collector Contrib</a>. |
| 116 | + * |
| 117 | + * @param unitAbbreviation The unit that name that needs to be expanded/converted to Prometheus |
| 118 | + * units. |
| 119 | + * @return The expanded/converted unit name if known, otherwise returns the input unit name as-is. |
| 120 | + */ |
| 121 | + private static String getPrometheusUnit(String unitAbbreviation) { |
| 122 | + switch (unitAbbreviation) { |
| 123 | + // Time |
| 124 | + case "d": |
| 125 | + return "days"; |
| 126 | + case "h": |
| 127 | + return "hours"; |
| 128 | + case "min": |
| 129 | + return "minutes"; |
| 130 | + case "s": |
| 131 | + return "seconds"; |
| 132 | + case "ms": |
| 133 | + return "milliseconds"; |
| 134 | + case "us": |
| 135 | + return "microseconds"; |
| 136 | + case "ns": |
| 137 | + return "nanoseconds"; |
| 138 | + // Bytes |
| 139 | + case "By": |
| 140 | + return "bytes"; |
| 141 | + case "KiBy": |
| 142 | + return "kibibytes"; |
| 143 | + case "MiBy": |
| 144 | + return "mebibytes"; |
| 145 | + case "GiBy": |
| 146 | + return "gibibytes"; |
| 147 | + case "TiBy": |
| 148 | + return "tibibytes"; |
| 149 | + case "KBy": |
| 150 | + return "kilobytes"; |
| 151 | + case "MBy": |
| 152 | + return "megabytes"; |
| 153 | + case "GBy": |
| 154 | + return "gigabytes"; |
| 155 | + case "TBy": |
| 156 | + return "terabytes"; |
| 157 | + case "B": |
| 158 | + return "bytes"; |
| 159 | + case "KB": |
| 160 | + return "kilobytes"; |
| 161 | + case "MB": |
| 162 | + return "megabytes"; |
| 163 | + case "GB": |
| 164 | + return "gigabytes"; |
| 165 | + case "TB": |
| 166 | + return "terabytes"; |
| 167 | + // SI |
| 168 | + case "m": |
| 169 | + return "meters"; |
| 170 | + case "V": |
| 171 | + return "volts"; |
| 172 | + case "A": |
| 173 | + return "amperes"; |
| 174 | + case "J": |
| 175 | + return "joules"; |
| 176 | + case "W": |
| 177 | + return "watts"; |
| 178 | + case "g": |
| 179 | + return "grams"; |
| 180 | + // Misc |
| 181 | + case "Cel": |
| 182 | + return "celsius"; |
| 183 | + case "Hz": |
| 184 | + return "hertz"; |
| 185 | + case "1": |
| 186 | + return ""; |
| 187 | + case "%": |
| 188 | + return "percent"; |
| 189 | + case "$": |
| 190 | + return "dollars"; |
| 191 | + default: |
| 192 | + return unitAbbreviation; |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + /** |
| 197 | + * This method retrieves the expanded Prometheus unit name to be used with "per" units for known |
| 198 | + * units. For example: s => per second (singular) |
| 199 | + * |
| 200 | + * @param perUnitAbbreviation The unit abbreviation used in a 'per' unit. |
| 201 | + * @return The expanded unit equivalent to be used in 'per' unit if the input is a known unit, |
| 202 | + * otherwise returns the input as-is. |
| 203 | + */ |
| 204 | + private static String getPrometheusPerUnit(String perUnitAbbreviation) { |
| 205 | + switch (perUnitAbbreviation) { |
| 206 | + case "s": |
| 207 | + return "second"; |
| 208 | + case "m": |
| 209 | + return "minute"; |
| 210 | + case "h": |
| 211 | + return "hour"; |
| 212 | + case "d": |
| 213 | + return "day"; |
| 214 | + case "w": |
| 215 | + return "week"; |
| 216 | + case "mo": |
| 217 | + return "month"; |
| 218 | + case "y": |
| 219 | + return "year"; |
| 220 | + default: |
| 221 | + return perUnitAbbreviation; |
| 222 | + } |
| 223 | + } |
| 224 | +} |
0 commit comments