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