1 /*******************************************************************************
2 
3     Contains methods that collect stats from primitive data members of structs
4     or classes to respond to Prometheus queries with.
5 
6     The metric designs specified by Prometheus
7     (`https://prometheus.io/docs/concepts/metric_types/`) have not been
8     implemented yet. So, as of now, this collector can process only primitive
9     data-type members from a struct or a class. However, the current Prometheus
10     stat collection framework could be enhanced to support metrics with a very
11     little effort.
12 
13     In Prometheus' data model, the stats that are measured are called Metrics,
14     and the dimensions along which stats are measured are called Labels.
15 
16     Metrics can be any measurable value, e.g., CPU or memory consumption.
17 
18     Labels resemble key-value pairs, where the key is referred to as a label's
19     name, and the value as a label's value. A label name would refer to the name
20     of a dimension across which we want to measure stats. Correspondingly, a
21     label value would refer to a point along the said dimension. A stat can have
22     more than one label, if it is intended to be measured across multiple
23     dimensions.
24 
25     Stats with labels look like the following example
26     `
27     promhttp_metric_handler_requests_total{code="200"} 3
28     promhttp_metric_handler_requests_total{code="500"} 0
29     promhttp_metric_handler_requests_total{code="503"} 0
30     `
31     Here `promhttp_metric_handler_requests_total` is the metric, `code` is
32     the label name, `"200"`, `"500"` and `"503"` are the label values, and `3`,
33     `0` and `0` are the respective metric values.
34 
35     On the data visualization side of Prometheus, fetching stats using queries
36     is analogous to calling functions. A metric name is analogous to a function
37     name, and a label is analogous to a function parameter. Continuing with the
38     above example, the following query will return `3`:
39     `
40     promhttp_metric_handler_requests_total {code="200"}
41     `
42 
43     (For detailed information about Prometheus-specific terminology, e.g.,
44     Metrics and Labels, please refer to
45     `https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels`.)
46 
47     This module contains a class which can be used to collect metrics as well as
48     labels from composite-type values, and format them in a way acceptable for
49     Prometheus.
50 
51     Copyright:
52         Copyright (c) 2019 dunnhumby Germany GmbH.
53         All rights reserved
54 
55     License:
56         Boost Software License Version 1.0. See LICENSE.txt for details.
57 
58 *******************************************************************************/
59 
60 module ocean.util.prometheus.collector.Collector;
61 
62 version (unittest) import ocean.core.Test;
63 import ocean.meta.types.Qualifiers;
64 import ocean.util.prometheus.collector.StatFormatter;
65 
66 /*******************************************************************************
67 
68     This class provides methods to collect metrics with no label, one label, or
69     multiple labels, using overloaded definitions of the `collect` method.
70 
71     Once the desired stats have been collected, the accumulated response
72     string can be collected using the `getCollection` method.
73 
74     Additionally, the `reset` method can be used to reset stat
75     collection, when the desired stats have been collected from a Collector
76     instance.
77 
78 *******************************************************************************/
79 
80 public class Collector
81 {
82     /// A buffer used for storing collected stats. Is cleared when the `reset`
83     /// method is called.
84     private mstring collect_buf;
85 
86     /***************************************************************************
87 
88         Returns:
89             The stats collected since the last call to the `reset` method, in a
90             textual respresentation that can be readily added to a response
91             message body.
92             The specifications of the format of the collected stats can be found
93             at `https://prometheus.io/docs/instrumenting/exposition_formats/`.
94 
95     ***************************************************************************/
96 
97     public cstring getCollection ( )
98     {
99         return this.collect_buf;
100     }
101 
102     /// Reset the length of the stat collection buffer to 0.
103     public void reset ( )
104     {
105         this.collect_buf.length = 0;
106         assumeSafeAppend(this.collect_buf);
107     }
108 
109     /***************************************************************************
110 
111         Collect stats from the data members of a struct or a class and prepare
112         them to be fetched upon the next call to `getCollection`. The
113         specifications of the format of the collected stats can be found at
114         `https://prometheus.io/docs/instrumenting/exposition_formats/`.
115 
116         Params:
117             ValuesT = The struct or class type to fetch the stat names from.
118             values  = The struct or class to fetch stat values from.
119 
120     ***************************************************************************/
121 
122     public void collect ( ValuesT ) ( ValuesT values )
123     {
124         static assert (is(ValuesT == struct) || is(ValuesT == class),
125             "'values' parameter must be a struct or a class.");
126 
127         formatStats(values, this.collect_buf);
128     }
129 
130     /***************************************************************************
131 
132         Collect stats from the data members of a struct or a class, annotate
133         them with a given label name and value, and prepare them to be fetched
134         upon the next call to `getCollection`.
135         The specifications of the format of the collected stats can be found
136         at `https://prometheus.io/docs/instrumenting/exposition_formats/`.
137 
138         Params:
139             LabelName = The name of the label to annotate the stats with.
140             ValuesT   = The struct or class type to fetch the stat names from.
141             LabelT    = The type of the label's value.
142             values    = The struct or class to fetch stat values from.
143             label_val = The label value to annotate the stats with.
144 
145     ***************************************************************************/
146 
147     public void collect ( string LabelName, ValuesT, LabelT ) (
148         ValuesT values, LabelT label_val )
149     {
150         static assert (is(ValuesT == struct) || is(ValuesT == class),
151             "'values' parameter must be a struct or a class.");
152 
153         formatStats!(LabelName)(values, label_val, this.collect_buf);
154     }
155 
156     /***************************************************************************
157 
158         Collect stats from the data members of a struct or a class, annotate
159         them with labels from the data members of another struct or class, and
160         prepare them to be fetched upon the next call to `getCollection`.
161         The specifications of the format of the collected stats can be found
162         at `https://prometheus.io/docs/instrumenting/exposition_formats/`.
163 
164         Params:
165             ValuesT = The struct or class type to fetch the stat names from.
166             LabelsT = The struct or class type to fetch the label names from.
167             values  = The struct or class to fetch stat values from.
168             labels  = The struct or class holding the label values to annotate
169                       the stats with.
170 
171     ***************************************************************************/
172 
173     public void collect ( ValuesT, LabelsT ) ( ValuesT values, LabelsT labels )
174     {
175         static assert (is(ValuesT == struct) || is(ValuesT == class),
176             "'values' parameter must be a struct or a class.");
177         static assert (is(LabelsT == struct) || is(LabelsT == class),
178             "'labels' parameter must be a struct or a class.");
179 
180         formatStats(values, labels, this.collect_buf);
181     }
182 }
183 
184 /// Test collecting populated stats, but without any label.
185 unittest
186 {
187     auto test_collector = new Collector();
188     test_collector.collect(Statistics(3600, 347, 3.14, 6.023, 0.43));
189 
190     test!("==")(test_collector.getCollection(),
191         "up_time_s 3600\ncount 347\nratio 3.14\nfraction 6.023\n" ~
192         "very_real 0.43\n");
193 }
194 
195 /// Test collecting populated stats with one label
196 unittest
197 {
198     auto test_collector = new Collector();
199     test_collector.collect!("id")(
200         Statistics(3600, 347, 3.14, 6.023, 0.43), 123.034);
201 
202     test!("==")(test_collector.getCollection(),
203         "up_time_s {id=\"123.034\"} 3600\ncount {id=\"123.034\"} 347\n" ~
204         "ratio {id=\"123.034\"} 3.14\nfraction {id=\"123.034\"} 6.023\n" ~
205         "very_real {id=\"123.034\"} 0.43\n");
206 }
207 
208 /// Test collecting stats having initial values with multiple labels
209 unittest
210 {
211     auto test_collector = new Collector();
212     test_collector.collect(Statistics(3600, 347, 3.14, 6.023, 0.43),
213         Labels(1_235_813, "ocean", 3.14159));
214 
215     test!("==")(test_collector.getCollection(),
216         "up_time_s {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 3600\n" ~
217         "count {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 347\n" ~
218         "ratio {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 3.14\n" ~
219         "fraction {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 6.023\n" ~
220         "very_real {id=\"1235813\",job=\"ocean\",perf=\"3.14159\"} 0.43\n");
221 }
222 
223 /// Test resetting collected stats
224 unittest
225 {
226     auto test_collector = new Collector();
227     test_collector.collect!("id")(Statistics.init, 123);
228 
229     test!("==")(test_collector.getCollection(),
230         "up_time_s {id=\"123\"} 0\ncount {id=\"123\"} 0\n" ~
231         "ratio {id=\"123\"} 0\nfraction {id=\"123\"} 0\n" ~
232         "very_real {id=\"123\"} 0\n");
233 
234     test_collector.reset();
235 
236     test!("==")(test_collector.getCollection(), "");
237 }
238 
239 // Test collecting stats having initial values, but without any label.
240 unittest
241 {
242     auto test_collector = new Collector();
243     test_collector.collect(Statistics.init);
244 
245     test!("==")(test_collector.getCollection(),
246         "up_time_s 0\ncount 0\nratio 0\nfraction 0\nvery_real 0\n");
247 }
248 
249 // Test collecting stats having initial values with one label
250 unittest
251 {
252     auto test_collector = new Collector();
253     test_collector.collect!("id")(Statistics.init, 123);
254 
255     test!("==")(test_collector.getCollection(),
256         "up_time_s {id=\"123\"} 0\ncount {id=\"123\"} 0\n" ~
257         "ratio {id=\"123\"} 0\nfraction {id=\"123\"} 0\n" ~
258         "very_real {id=\"123\"} 0\n");
259 }
260 
261 // Test collecting stats having initial values with multiple labels
262 unittest
263 {
264     auto test_collector = new Collector();
265     test_collector.collect(Statistics.init, Labels.init);
266 
267     test!("==")(test_collector.getCollection(),
268         "up_time_s {id=\"0\",job=\"\",perf=\"0\"} 0\n" ~
269         "count {id=\"0\",job=\"\",perf=\"0\"} 0\n" ~
270         "ratio {id=\"0\",job=\"\",perf=\"0\"} 0\n" ~
271         "fraction {id=\"0\",job=\"\",perf=\"0\"} 0\n" ~
272         "very_real {id=\"0\",job=\"\",perf=\"0\"} 0\n");
273 }
274 
275 // Test accumulation of collected stats
276 unittest
277 {
278     auto test_collector = new Collector();
279     test_collector.collect!("id")(Statistics.init, 123);
280 
281     test!("==")(test_collector.getCollection(),
282         "up_time_s {id=\"123\"} 0\ncount {id=\"123\"} 0\n" ~
283         "ratio {id=\"123\"} 0\nfraction {id=\"123\"} 0\n" ~
284         "very_real {id=\"123\"} 0\n");
285 
286     test_collector.collect!("id")(
287         Statistics(3600, 347, 3.14, 6.023, 0.43), 123.034);
288 
289     test!("==")(test_collector.getCollection(),
290         "up_time_s {id=\"123\"} 0\ncount {id=\"123\"} 0\n" ~
291         "ratio {id=\"123\"} 0\nfraction {id=\"123\"} 0\n" ~
292         "very_real {id=\"123\"} 0\n" ~
293 
294         "up_time_s {id=\"123.034\"} 3600\ncount {id=\"123.034\"} 347\n" ~
295         "ratio {id=\"123.034\"} 3.14\nfraction {id=\"123.034\"} 6.023\n" ~
296         "very_real {id=\"123.034\"} 0.43\n");
297 }