1 /******************************************************************************* 2 3 Contains methods that format primitive data members from structs or classes 4 into strings that can be exported as responses to prometheus queries. 5 6 Copyright: 7 Copyright (c) 2019 dunnhumby Germany GmbH. 8 All rights reserved 9 10 License: 11 Boost Software License Version 1.0. See LICENSE.txt for details. 12 13 *******************************************************************************/ 14 15 module ocean.util.prometheus.collector.StatFormatter; 16 17 import ocean.math.IEEE; 18 import ocean.meta.codegen.Identifier; 19 import ocean.meta.traits.Basic; 20 import ocean.meta.types.Qualifiers; 21 import ocean.text.convert.Formatter; 22 23 /******************************************************************************* 24 25 Format data members from a struct or a class into a string that can be 26 added to a stat collection buffer for exporting to prometheus. 27 28 The names and values of the members of the given struct are formatted as 29 stat names and values, respectively. 30 31 Params: 32 ValuesT = The struct or class type to fetch the stat names from. 33 values = The struct or class to fetch stat values from. 34 buffer = The buffer to add the formatted stats to. 35 36 *******************************************************************************/ 37 38 public static void formatStats ( ValuesT ) ( 39 ValuesT values, ref mstring buffer ) 40 { 41 static assert (is(ValuesT == struct) || is(ValuesT == class), 42 "'values' parameter must be a struct or a class."); 43 44 foreach (i, ValMemberT; typeof(ValuesT.tupleof)) 45 { 46 static if (isPrimitiveType!(ValMemberT)) 47 { 48 sformat(buffer, "{}", identifier!(ValuesT.tupleof[i])); 49 appendValue(values.tupleof[i], buffer); 50 } 51 } 52 } 53 54 /******************************************************************************* 55 56 Format data members from a struct or a class, with a additional label name 57 and value, into a string that can be added to a stat collection buffer for 58 exporting to prometheus. 59 60 Params: 61 LabelName = The name of the label to annotate the stats with. 62 ValuesT = The struct or class type to fetch the stat names from. 63 LabelT = The type of the label's value. 64 values = The struct or class to fetch stat values from. 65 label_val = The label value to annotate the stats with. 66 buffer = The buffer to add the formatted stats to. 67 68 *******************************************************************************/ 69 70 public static void formatStats ( string LabelName, ValuesT, LabelT ) ( 71 ValuesT values, LabelT label_val, ref mstring buffer ) 72 { 73 static assert (is(ValuesT == struct) || is(ValuesT == class), 74 "'values' parameter must be a struct or a class."); 75 76 foreach (i, ValMemberT; typeof(ValuesT.tupleof)) 77 { 78 static if (isPrimitiveType!(ValMemberT)) 79 { 80 sformat(buffer, "{} {{", identifier!(ValuesT.tupleof[i])); 81 82 appendLabel!(LabelName)(label_val, buffer); 83 84 sformat(buffer, "}"); 85 appendValue(values.tupleof[i], buffer); 86 } 87 } 88 } 89 90 91 /******************************************************************************* 92 93 Format data members from a struct or a class, with an additional struct or 94 class to fetch label names and values from, into a string that can be added 95 to a stat collection buffer for exporting to prometheus. 96 97 Params: 98 ValuesT = The struct or class type to fetch the stat names from. 99 LabelsT = The struct or class type to fetch the label names from. 100 values = The struct or class to fetch stat values from. 101 labels = The struct or class holding the label values to annotate 102 the stats with. 103 buffer = The buffer to add the formatted stats to. 104 105 *******************************************************************************/ 106 107 public static void formatStats ( ValuesT, LabelsT ) ( ValuesT values, 108 LabelsT labels, ref mstring buffer ) 109 { 110 static assert (is(ValuesT == struct) || is(ValuesT == class), 111 "'values' parameter must be a struct or a class."); 112 static assert (is(LabelsT == struct) || is(LabelsT == class), 113 "'labels' parameter must be a struct or a class."); 114 115 foreach (i, ValMemberT; typeof(ValuesT.tupleof)) 116 { 117 static if (isPrimitiveType!(ValMemberT)) 118 { 119 sformat(buffer, "{} {{", identifier!(ValuesT.tupleof[i])); 120 121 bool first_label = true; 122 123 foreach (j, LabelMemberT; typeof(LabelsT.tupleof)) 124 { 125 if (first_label) 126 { 127 first_label = false; 128 } 129 else 130 { 131 sformat(buffer, ","); 132 } 133 134 appendLabel!(identifier!(LabelsT.tupleof[j]))( 135 labels.tupleof[j], buffer); 136 } 137 138 sformat(buffer, "}"); 139 appendValue(values.tupleof[i], buffer); 140 } 141 } 142 } 143 144 /******************************************************************************* 145 146 Appends a label name and value to a given buffer. If the value is of a 147 floating-point type, then appends it with a precision upto 6 decimal places. 148 149 Params: 150 LabelName = The name of the label to annotate the stats with. 151 LabelT = The type of the label's value. 152 label_val = The label value to annotate the stats with. 153 buffer = The buffer to append the label to. 154 155 *******************************************************************************/ 156 157 private static void appendLabel ( string LabelName, LabelT ) ( 158 LabelT label_val, ref mstring buffer ) 159 { 160 static if (isFloatingPointType!(LabelT)) 161 { 162 sformat(buffer, "{}=\"{:6.}\"", LabelName, 163 getSanitized(label_val)); 164 } 165 else 166 { 167 sformat(buffer, "{}=\"{}\"", LabelName, label_val); 168 } 169 } 170 171 // Test appending a label with a value of type string 172 unittest 173 { 174 mstring buffer; 175 appendLabel!("labelname")("labelval", buffer); 176 test!("==")(buffer, "labelname=\"labelval\""); 177 } 178 179 // Test appending a label with an integer type value 180 unittest 181 { 182 mstring buffer; 183 appendLabel!("labelname")(2345678UL, buffer); 184 test!("==")(buffer, "labelname=\"2345678\""); 185 } 186 187 // Test appending a label with a floting point type value 188 unittest 189 { 190 mstring buffer; 191 appendLabel!("labelname")(3.1415926, buffer); 192 test!("==")(buffer, "labelname=\"3.141593\""); 193 } 194 195 /******************************************************************************* 196 197 Appends a stat value to a given buffer. If the value is of a floating-point 198 type, then appends it with a precision of upto 6 decimal places. 199 200 Params: 201 T = The datatype of the value. 202 value = The value to append to the buffer. 203 buffer = The buffer to which the stat value will be appended. 204 205 *******************************************************************************/ 206 207 private static void appendValue ( T ) ( T value, ref mstring buffer ) 208 { 209 static if (isFloatingPointType!(T)) 210 { 211 sformat(buffer, " {:6.}\n", getSanitized(value)); 212 } 213 else 214 { 215 sformat(buffer, " {}\n", value); 216 } 217 } 218 219 // Test appending a non-floating-point values 220 unittest 221 { 222 mstring buffer; 223 appendValue(32768UL, buffer); 224 test!("==")(buffer, " 32768\n"); 225 } 226 227 // Test appending a floating-point value having less than 6 decimal places 228 unittest 229 { 230 mstring buffer; 231 appendValue(3.14159, buffer); 232 test!("==")(buffer, " 3.14159\n"); 233 } 234 235 // Test appending a floating-point value having more than 6 decimal places 236 unittest 237 { 238 mstring buffer; 239 appendValue(3.1415926, buffer); 240 test!("==")(buffer, " 3.141593\n"); 241 } 242 243 /******************************************************************************* 244 245 Sanitizes floating-point type values. 246 247 If the datatype of the value is a floating-point type, then returns 0.0 248 for NaN and the datatype's maximum value for +/-Inf. 249 If the datatype of the value is not a floating-point type, then returns 250 the given value without any modification. 251 252 Params: 253 T = The datatype of the value. 254 value = The value to append to sanitize. 255 256 Returns: 257 The sanitized value, if the input is of a floating-point type, 258 otherwise the given value itself. 259 260 *******************************************************************************/ 261 262 private static T getSanitized ( T ) ( T val ) 263 { 264 static if (isFloatingPointType!(T)) 265 { 266 if (isNaN(val)) 267 { 268 return 0.0; 269 } 270 else if (isInfinity(val)) 271 { 272 return T.max; 273 } 274 } 275 276 return val; 277 } 278 279 // Test sanitization of a floating type value as NaN 280 unittest 281 { 282 test!("==")(getSanitized(double.init), 0.0); 283 } 284 285 // Test sanitization of a floating point value as infinity 286 unittest 287 { 288 test!("==")(getSanitized(double.infinity), double.max); 289 } 290 291 // Test that sanitization does not alter any floating point value that is 292 // not NaN or Inf. 293 unittest 294 { 295 double pi = 3.141592; 296 test!("==")(getSanitized(pi), 3.141592); 297 } 298 299 300 version (unittest) 301 { 302 import ocean.core.Test; 303 import ocean.meta.types.Qualifiers; 304 305 package struct Statistics 306 { 307 ulong up_time_s; 308 size_t count; 309 float ratio; 310 double fraction; 311 real very_real; 312 } 313 314 package struct Labels 315 { 316 hash_t id; 317 cstring job; 318 float perf; 319 } 320 } 321 322 /// Test collecting populated stats, but without any label. 323 unittest 324 { 325 auto expected = "up_time_s 3600\ncount 347\nratio 3.14\nfraction 6.023\n" ~ 326 "very_real 0.43\n"; 327 328 mstring actual; 329 formatStats(Statistics(3600, 347, 3.14, 6.023, 0.43), actual); 330 331 test!("==")(actual, expected); 332 } 333 334 /// Test collecting populated stats with one label 335 unittest 336 { 337 auto expected = 338 "up_time_s {id=\"123.034\"} 3600\ncount {id=\"123.034\"} 347\n" ~ 339 "ratio {id=\"123.034\"} 3.14\nfraction {id=\"123.034\"} 6.023\n" ~ 340 "very_real {id=\"123.034\"} 0.43\n"; 341 342 mstring actual; 343 formatStats!("id")( 344 Statistics(3600, 347, 3.14, 6.023, 0.43), 123.034, actual); 345 346 test!("==")(actual, expected); 347 } 348 349 /// Test collecting stats having initial values with multiple labels 350 unittest 351 { 352 auto expected = 353 "up_time_s {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 3600\n" ~ 354 "count {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 347\n" ~ 355 "ratio {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 3.14\n" ~ 356 "fraction {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 6.023\n" ~ 357 "very_real {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 0.43\n"; 358 359 mstring actual; 360 formatStats(Statistics(3600, 347, 3.14, 6.023, 0.43), 361 Labels(1_235_813, "ocean", 3.14159), actual); 362 363 test!("==")(actual, expected); 364 }