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 }