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 }