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 }