]> git.sesse.net Git - nageru/blob - shared/metrics.cpp
91b03e83483e1335d85d747dca730d607ff10438
[nageru] / shared / metrics.cpp
1 #include "shared/metrics.h"
2
3 #include <assert.h>
4 #include <math.h>
5
6 #include <algorithm>
7 #include <chrono>
8 #include <locale>
9 #include <sstream>
10
11 using namespace std;
12 using namespace std::chrono;
13
14 Metrics global_metrics;
15 string Metrics::prefix = "nageru";
16
17 double get_timestamp_for_metrics()
18 {
19         return duration<double>(system_clock::now().time_since_epoch()).count();
20 }
21
22 string Metrics::serialize_name(const string &name, const vector<pair<string, string>> &labels)
23 {
24         return prefix + "_" + name + serialize_labels(labels);
25 }
26
27 string Metrics::serialize_labels(const vector<pair<string, string>> &labels)
28 {
29         if (labels.empty()) {
30                 return "";
31         }
32
33         string label_str;
34         for (const pair<string, string> &label : labels) {
35                 if (!label_str.empty()) {
36                         label_str += ",";
37                 }
38                 label_str += label.first + "=\"" + label.second + "\"";
39         }
40         return "{" + label_str + "}";
41 }
42
43 void Metrics::add(const string &name, const vector<pair<string, string>> &labels, atomic<int64_t> *location, Metrics::Type type)
44 {
45         Metric metric;
46         metric.data_type = DATA_TYPE_INT64;
47         metric.location_int64 = location;
48
49         lock_guard<mutex> lock(mu);
50         metrics.emplace(MetricKey(name, labels), metric);
51         assert(types.count(name) == 0 || types[name] == type);
52         types[name] = type;
53 }
54
55 void Metrics::add(const string &name, const vector<pair<string, string>> &labels, atomic<double> *location, Metrics::Type type)
56 {
57         Metric metric;
58         metric.data_type = DATA_TYPE_DOUBLE;
59         metric.location_double = location;
60
61         lock_guard<mutex> lock(mu);
62         metrics.emplace(MetricKey(name, labels), metric);
63         assert(types.count(name) == 0 || types[name] == type);
64         types[name] = type;
65 }
66
67 void Metrics::add(const string &name, const vector<pair<string, string>> &labels, Histogram *location, Laziness laziness)
68 {
69         Metric metric;
70         metric.data_type = DATA_TYPE_HISTOGRAM;
71         metric.laziness = laziness;
72         metric.location_histogram = location;
73
74         lock_guard<mutex> lock(mu);
75         metrics.emplace(MetricKey(name, labels), metric);
76         assert(types.count(name) == 0 || types[name] == TYPE_HISTOGRAM);
77         types[name] = TYPE_HISTOGRAM;
78 }
79
80 void Metrics::add(const string &name, const vector<pair<string, string>> &labels, Summary *location, Laziness laziness)
81 {
82         Metric metric;
83         metric.data_type = DATA_TYPE_SUMMARY;
84         metric.laziness = laziness;
85         metric.location_summary = location;
86
87         lock_guard<mutex> lock(mu);
88         metrics.emplace(MetricKey(name, labels), metric);
89         assert(types.count(name) == 0 || types[name] == TYPE_SUMMARY);
90         types[name] = TYPE_SUMMARY;
91 }
92
93 void Metrics::remove(const string &name, const vector<pair<string, string>> &labels)
94 {
95         lock_guard<mutex> lock(mu);
96
97         auto it = metrics.find(MetricKey(name, labels));
98         assert(it != metrics.end());
99
100         // If this is the last metric with this name, remove the type as well.
101         if (!((it != metrics.begin() && prev(it)->first.name == name) ||
102               (it != metrics.end() && next(it) != metrics.end() && next(it)->first.name == name))) {
103                 types.erase(name);
104         }
105
106         metrics.erase(it);
107 }
108
109 void Metrics::remove_if_exists(const string &name, const vector<pair<string, string>> &labels)
110 {
111         lock_guard<mutex> lock(mu);
112         auto it = metrics.find(MetricKey(name, labels));
113         if (it == metrics.end()) {
114                 return;
115         }
116
117         // If this is the last metric with this name, remove the type as well.
118         if (!((it != metrics.begin() && prev(it)->first.name == name) ||
119               (it != metrics.end() && next(it) != metrics.end() && next(it)->first.name == name))) {
120                 types.erase(name);
121         }
122
123         metrics.erase(it);
124 }
125
126 string Metrics::serialize() const
127 {
128         stringstream ss;
129         ss.imbue(locale("C"));
130         ss.precision(20);
131
132         lock_guard<mutex> lock(mu);
133         auto type_it = types.cbegin();
134         for (const auto &key_and_metric : metrics) {
135                 string name = prefix + "_" + key_and_metric.first.name + key_and_metric.first.serialized_labels;
136                 const Metric &metric = key_and_metric.second;
137
138                 if (type_it != types.cend() &&
139                     key_and_metric.first.name == type_it->first) {
140                         // It's the first time we print out any metric with this name,
141                         // so add the type header.
142                         if (type_it->second == TYPE_GAUGE) {
143                                 ss << "# TYPE " + prefix + "_" << type_it->first << " gauge\n";
144                         } else if (type_it->second == TYPE_HISTOGRAM) {
145                                 ss << "# TYPE " + prefix + "_" << type_it->first << " histogram\n";
146                         } else if (type_it->second == TYPE_SUMMARY) {
147                                 ss << "# TYPE " + prefix + "_" << type_it->first << " summary\n";
148                         }
149                         ++type_it;
150                 }
151
152                 if (metric.data_type == DATA_TYPE_INT64) {
153                         ss << name << " " << metric.location_int64->load() << "\n";
154                 } else if (metric.data_type == DATA_TYPE_DOUBLE) {
155                         double val = metric.location_double->load();
156                         if (isnan(val)) {
157                                 // Prometheus can't handle “-nan”.
158                                 ss << name << " NaN\n";
159                         } else {
160                                 ss << name << " " << val << "\n";
161                         }
162                 } else if (metric.data_type == DATA_TYPE_HISTOGRAM) {
163                         ss << metric.location_histogram->serialize(metric.laziness, key_and_metric.first.name, key_and_metric.first.labels);
164                 } else {
165                         ss << metric.location_summary->serialize(metric.laziness, key_and_metric.first.name, key_and_metric.first.labels);
166                 }
167         }
168
169         return ss.str();
170 }
171
172 void Histogram::init(const vector<double> &bucket_vals)
173 {
174         this->num_buckets = bucket_vals.size();
175         buckets.reset(new Bucket[num_buckets]);
176         for (size_t i = 0; i < num_buckets; ++i) {
177                 buckets[i].val = bucket_vals[i];
178         }
179 }
180
181 void Histogram::init_uniform(size_t num_buckets)
182 {
183         this->num_buckets = num_buckets;
184         buckets.reset(new Bucket[num_buckets]);
185         for (size_t i = 0; i < num_buckets; ++i) {
186                 buckets[i].val = i;
187         }
188 }
189
190 void Histogram::init_geometric(double min, double max, size_t num_buckets)
191 {
192         this->num_buckets = num_buckets;
193         buckets.reset(new Bucket[num_buckets]);
194         for (size_t i = 0; i < num_buckets; ++i) {
195                 buckets[i].val = min * pow(max / min, double(i) / (num_buckets - 1));
196         }
197 }
198
199 void Histogram::count_event(double val)
200 {
201         Bucket ref_bucket;
202         ref_bucket.val = val;
203         auto it = lower_bound(buckets.get(), buckets.get() + num_buckets, ref_bucket,
204                 [](const Bucket &a, const Bucket &b) { return a.val < b.val; });
205         if (it == buckets.get() + num_buckets) {
206                 ++count_after_last_bucket;
207         } else {
208                 ++it->count;
209         }
210         // Non-atomic add, but that's fine, since there are no concurrent writers.
211         sum = sum + val;
212 }
213
214 string Histogram::serialize(Metrics::Laziness laziness, const string &name, const vector<pair<string, string>> &labels) const
215 {
216         // Check if the histogram is empty and should not be serialized.
217         if (laziness == Metrics::PRINT_WHEN_NONEMPTY && count_after_last_bucket.load() == 0) {
218                 bool empty = true;
219                 for (size_t bucket_idx = 0; bucket_idx < num_buckets; ++bucket_idx) {
220                         if (buckets[bucket_idx].count.load() != 0) {
221                                 empty = false;
222                                 break;
223                         }
224                 }
225                 if (empty) {
226                         return "";
227                 }
228         }
229
230         stringstream ss;
231         ss.imbue(locale("C"));
232         ss.precision(20);
233
234         int64_t count = 0;
235         for (size_t bucket_idx = 0; bucket_idx < num_buckets; ++bucket_idx) {
236                 stringstream le_ss;
237                 le_ss.imbue(locale("C"));
238                 le_ss.precision(20);
239                 le_ss << buckets[bucket_idx].val;
240                 vector<pair<string, string>> bucket_labels = labels;
241                 bucket_labels.emplace_back("le", le_ss.str());
242
243                 count += buckets[bucket_idx].count.load();
244                 ss << Metrics::serialize_name(name + "_bucket", bucket_labels) << " " << count << "\n";
245         }
246
247         count += count_after_last_bucket.load();
248
249         ss << Metrics::serialize_name(name + "_sum", labels) << " " << sum.load() << "\n";
250         ss << Metrics::serialize_name(name + "_count", labels) << " " << count << "\n";
251
252         return ss.str();
253 }
254
255 void Summary::init(const vector<double> &quantiles, double window_seconds)
256 {
257         this->quantiles = quantiles;
258         window = duration<double>(window_seconds);
259 }
260
261 void Summary::count_event(double val)
262 {
263         steady_clock::time_point now = steady_clock::now();
264         steady_clock::time_point cutoff = now - duration_cast<steady_clock::duration>(window);
265
266         lock_guard<mutex> lock(mu);
267         values.emplace_back(now, val);
268         while (!values.empty() && values.front().first < cutoff) {
269                 values.pop_front();
270         }
271
272         // Non-atomic add, but that's fine, since there are no concurrent writers.
273         sum = sum + val;
274         ++count;
275 }
276
277 string Summary::serialize(Metrics::Laziness laziness, const string &name, const vector<pair<string, string>> &labels)
278 {
279         steady_clock::time_point now = steady_clock::now();
280         steady_clock::time_point cutoff = now - duration_cast<steady_clock::duration>(window);
281
282         vector<double> values_copy;
283         {
284                 lock_guard<mutex> lock(mu);
285                 while (!values.empty() && values.front().first < cutoff) {
286                         values.pop_front();
287                 }
288                 values_copy.reserve(values.size());
289                 for (const auto &time_and_value : values) {
290                         values_copy.push_back(time_and_value.second);
291                 }
292         }
293
294         vector<pair<double, double>> answers;
295         if (values_copy.size() == 0) {
296                 if (laziness == Metrics::PRINT_WHEN_NONEMPTY) {
297                         return "";
298                 }
299                 for (double quantile : quantiles) {
300                         answers.emplace_back(quantile, 0.0 / 0.0);
301                 }
302         } else if (values_copy.size() == 1) {
303                 for (double quantile : quantiles) {
304                         answers.emplace_back(quantile, values_copy[0]);
305                 }
306         } else {
307                 // We could probably do repeated nth_element, but the constant factor
308                 // gets a bit high, so just sorting probably is about as fast.
309                 sort(values_copy.begin(), values_copy.end());
310                 for (double quantile : quantiles) {
311                         double idx = quantile * (values_copy.size() - 1);
312                         size_t idx_floor = size_t(floor(idx));
313                         const double v0 = values_copy[idx_floor];
314
315                         if (idx_floor == values_copy.size() - 1) {
316                                 answers.emplace_back(quantile, values_copy[idx_floor]);
317                         } else {
318                                 // Linear interpolation.
319                                 double t = idx - idx_floor;
320                                 const double v1 = values_copy[idx_floor + 1];
321                                 answers.emplace_back(quantile, v0 + t * (v1 - v0));
322                         }
323                 }
324         }
325
326         stringstream ss;
327         ss.imbue(locale("C"));
328         ss.precision(20);
329
330         for (const auto &quantile_and_value : answers) {
331                 stringstream quantile_ss;
332                 quantile_ss.imbue(locale("C"));
333                 quantile_ss.precision(3);
334                 quantile_ss << quantile_and_value.first;
335                 vector<pair<string, string>> quantile_labels = labels;
336                 quantile_labels.emplace_back("quantile", quantile_ss.str());
337
338                 double val = quantile_and_value.second;;
339                 if (isnan(val)) {
340                         // Prometheus can't handle “-nan”.
341                         ss << Metrics::serialize_name(name, quantile_labels) << " NaN\n";
342                 } else {
343                         ss << Metrics::serialize_name(name, quantile_labels) << " " << val << "\n";
344                 }
345         }
346
347         ss << Metrics::serialize_name(name + "_sum", labels) << " " << sum.load() << "\n";
348         ss << Metrics::serialize_name(name + "_count", labels) << " " << count.load() << "\n";
349         return ss.str();
350 }