|
| 1 | +use opentelemetry_api::metrics::Unit; |
| 2 | +use std::borrow::Cow; |
| 3 | + |
| 4 | +const NON_APPLICABLE_ON_PER_UNIT: [&str; 8] = ["1", "d", "h", "min", "s", "ms", "us", "ns"]; |
| 5 | + |
| 6 | +pub(crate) fn get_unit_suffixes(unit: &Unit) -> Option<Cow<'static, str>> { |
| 7 | + // no unit return early |
| 8 | + if unit.as_str().is_empty() { |
| 9 | + return None; |
| 10 | + } |
| 11 | + |
| 12 | + // direct match with known units |
| 13 | + if let Some(matched) = get_prom_units(unit.as_str()) { |
| 14 | + return Some(Cow::Borrowed(matched)); |
| 15 | + } |
| 16 | + |
| 17 | + // converting foo/bar to foo_per_bar |
| 18 | + // split the string by the first '/' |
| 19 | + // if the first part is empty, we just return the second part if it's a match with known per unit |
| 20 | + // e.g |
| 21 | + // "test/y" => "per_year" |
| 22 | + // "km/s" => "kilometers_per_second" |
| 23 | + if let Some((first, second)) = unit.as_str().split_once('/') { |
| 24 | + return match ( |
| 25 | + NON_APPLICABLE_ON_PER_UNIT.contains(&first), |
| 26 | + get_prom_units(first), |
| 27 | + get_prom_per_unit(second), |
| 28 | + ) { |
| 29 | + (true, _, Some(second_part)) | (false, None, Some(second_part)) => { |
| 30 | + Some(Cow::Owned(format!("per_{}", second_part))) |
| 31 | + } |
| 32 | + (false, Some(first_part), Some(second_part)) => { |
| 33 | + Some(Cow::Owned(format!("{}_per_{}", first_part, second_part))) |
| 34 | + } |
| 35 | + _ => None, |
| 36 | + }; |
| 37 | + } |
| 38 | + |
| 39 | + None |
| 40 | +} |
| 41 | + |
| 42 | +fn get_prom_units(unit: &str) -> Option<&'static str> { |
| 43 | + match unit { |
| 44 | + // Time |
| 45 | + "d" => Some("days"), |
| 46 | + "h" => Some("hours"), |
| 47 | + "min" => Some("minutes"), |
| 48 | + "s" => Some("seconds"), |
| 49 | + "ms" => Some("milliseconds"), |
| 50 | + "us" => Some("microseconds"), |
| 51 | + "ns" => Some("nanoseconds"), |
| 52 | + "By" => Some("bytes"), |
| 53 | + |
| 54 | + // Bytes |
| 55 | + "KiBy" => Some("kibibytes"), |
| 56 | + "MiBy" => Some("mebibytes"), |
| 57 | + "GiBy" => Some("gibibytes"), |
| 58 | + "TiBy" => Some("tibibytes"), |
| 59 | + "KBy" => Some("kilobytes"), |
| 60 | + "MBy" => Some("megabytes"), |
| 61 | + "GBy" => Some("gigabytes"), |
| 62 | + "TBy" => Some("terabytes"), |
| 63 | + "B" => Some("bytes"), |
| 64 | + "KB" => Some("kilobytes"), |
| 65 | + "MB" => Some("megabytes"), |
| 66 | + "GB" => Some("gigabytes"), |
| 67 | + "TB" => Some("terabytes"), |
| 68 | + |
| 69 | + // SI |
| 70 | + "m" => Some("meters"), |
| 71 | + "V" => Some("volts"), |
| 72 | + "A" => Some("amperes"), |
| 73 | + "J" => Some("joules"), |
| 74 | + "W" => Some("watts"), |
| 75 | + "g" => Some("grams"), |
| 76 | + |
| 77 | + // Misc |
| 78 | + "Cel" => Some("celsius"), |
| 79 | + "Hz" => Some("hertz"), |
| 80 | + "1" => Some("ratio"), |
| 81 | + "%" => Some("percent"), |
| 82 | + "$" => Some("dollars"), |
| 83 | + _ => None, |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +fn get_prom_per_unit(unit: &str) -> Option<&'static str> { |
| 88 | + match unit { |
| 89 | + "s" => Some("second"), |
| 90 | + "m" => Some("minute"), |
| 91 | + "h" => Some("hour"), |
| 92 | + "d" => Some("day"), |
| 93 | + "w" => Some("week"), |
| 94 | + "mo" => Some("month"), |
| 95 | + "y" => Some("year"), |
| 96 | + _ => None, |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +#[allow(clippy::ptr_arg)] |
| 101 | +pub(crate) fn sanitize_name(s: &Cow<'static, str>) -> Cow<'static, str> { |
| 102 | + // prefix chars to add in case name starts with number |
| 103 | + let mut prefix = ""; |
| 104 | + |
| 105 | + // Find first invalid char |
| 106 | + if let Some((replace_idx, _)) = s.char_indices().find(|(i, c)| { |
| 107 | + if *i == 0 && c.is_ascii_digit() { |
| 108 | + // first char is number, add prefix and replace reset of chars |
| 109 | + prefix = "_"; |
| 110 | + true |
| 111 | + } else { |
| 112 | + // keep checking |
| 113 | + !c.is_alphanumeric() && *c != '_' && *c != ':' |
| 114 | + } |
| 115 | + }) { |
| 116 | + // up to `replace_idx` have been validated, convert the rest |
| 117 | + let (valid, rest) = s.split_at(replace_idx); |
| 118 | + Cow::Owned( |
| 119 | + prefix |
| 120 | + .chars() |
| 121 | + .chain(valid.chars()) |
| 122 | + .chain(rest.chars().map(|c| { |
| 123 | + if c.is_ascii_alphanumeric() || c == '_' || c == ':' { |
| 124 | + c |
| 125 | + } else { |
| 126 | + '_' |
| 127 | + } |
| 128 | + })) |
| 129 | + .collect(), |
| 130 | + ) |
| 131 | + } else { |
| 132 | + s.clone() // no invalid chars found, return existing |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +pub(crate) fn sanitize_prom_kv(s: &str) -> String { |
| 137 | + s.chars() |
| 138 | + .map(|c| { |
| 139 | + if c.is_ascii_alphanumeric() || c == ':' { |
| 140 | + c |
| 141 | + } else { |
| 142 | + '_' |
| 143 | + } |
| 144 | + }) |
| 145 | + .collect() |
| 146 | +} |
| 147 | + |
| 148 | +#[cfg(test)] |
| 149 | +mod tests { |
| 150 | + use super::*; |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn name_sanitization() { |
| 154 | + let tests = vec![ |
| 155 | + ("nam€_with_3_width_rune", "nam__with_3_width_rune"), |
| 156 | + ("`", "_"), |
| 157 | + ( |
| 158 | + r##"! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWKYZ[]\^_abcdefghijklmnopqrstuvwkyz{|}~"##, |
| 159 | + "________________0123456789:______ABCDEFGHIJKLMNOPQRSTUVWKYZ_____abcdefghijklmnopqrstuvwkyz____", |
| 160 | + ), |
| 161 | + |
| 162 | + ("Avalid_23name", "Avalid_23name"), |
| 163 | + ("_Avalid_23name", "_Avalid_23name"), |
| 164 | + ("1valid_23name", "_1valid_23name"), |
| 165 | + ("avalid_23name", "avalid_23name"), |
| 166 | + ("Ava:lid_23name", "Ava:lid_23name"), |
| 167 | + ("a lid_23name", "a_lid_23name"), |
| 168 | + (":leading_colon", ":leading_colon"), |
| 169 | + ("colon:in:the:middle", "colon:in:the:middle"), |
| 170 | + ("", ""), |
| 171 | + ]; |
| 172 | + |
| 173 | + for (input, want) in tests { |
| 174 | + assert_eq!(want, sanitize_name(&input.into()), "input: {input}") |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + #[test] |
| 179 | + fn test_get_unit_suffixes() { |
| 180 | + let test_cases = vec![ |
| 181 | + // Direct match |
| 182 | + ("g", Some(Cow::Borrowed("grams"))), |
| 183 | + // Per unit |
| 184 | + ("test/y", Some(Cow::Owned("per_year".to_owned()))), |
| 185 | + ("1/y", Some(Cow::Owned("per_year".to_owned()))), |
| 186 | + ("m/s", Some(Cow::Owned("meters_per_second".to_owned()))), |
| 187 | + // No match |
| 188 | + ("invalid", None), |
| 189 | + ("", None), |
| 190 | + ]; |
| 191 | + for (unit_str, expected_suffix) in test_cases { |
| 192 | + let unit = Unit::new(unit_str); |
| 193 | + assert_eq!(get_unit_suffixes(&unit), expected_suffix); |
| 194 | + } |
| 195 | + } |
| 196 | +} |
0 commit comments