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