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