/src/mod_auth_openidc/src/metrics.c
Line | Count | Source |
1 | | /* |
2 | | * Licensed to the Apache Software Foundation (ASF) under one |
3 | | * or more contributor license agreements. See the NOTICE file |
4 | | * distributed with this work for additional information |
5 | | * regarding copyright ownership. The ASF licenses this file |
6 | | * to you under the Apache License, Version 2.0 (the |
7 | | * "License"); you may not use this file except in compliance |
8 | | * with the License. You may obtain a copy of the License at |
9 | | * |
10 | | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | | * |
12 | | * Unless required by applicable law or agreed to in writing, |
13 | | * software distributed under the License is distributed on an |
14 | | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
15 | | * KIND, either express or implied. See the License for the |
16 | | * specific language governing permissions and limitations |
17 | | * under the License. |
18 | | */ |
19 | | |
20 | | /*************************************************************************** |
21 | | * Copyright (C) 2023-2026 ZmartZone Holding BV |
22 | | * All rights reserved. |
23 | | * |
24 | | * DISCLAIMER OF WARRANTIES: |
25 | | * |
26 | | * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT |
27 | | * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, |
28 | | * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, |
29 | | * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY |
30 | | * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE |
31 | | * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET |
32 | | * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE |
33 | | * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR |
34 | | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
35 | | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF |
36 | | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
37 | | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
38 | | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
39 | | * |
40 | | * @Author: Hans Zandbelt - hans.zandbelt@openidc.com |
41 | | */ |
42 | | |
43 | | // clang-format off |
44 | | |
45 | | #ifndef WIN32 |
46 | | #include <unistd.h> |
47 | | #endif |
48 | | |
49 | | #include "util/util.h" |
50 | | #include "http.h" |
51 | | #include "cache/cache.h" |
52 | | #include "metrics.h" |
53 | | #include <limits.h> |
54 | | #include <apr_atomic.h> |
55 | | #include <apr_shm.h> |
56 | | #include <apr_lib.h> |
57 | | |
58 | | // NB: formatting matters for docs script from here until clang-format on |
59 | | |
60 | | // KEEP THIS: start-of-classes |
61 | | |
62 | | #define OM_CLASS_AUTH_TYPE "authtype" // Request counter, overall and per AuthType: openid-connect, oauth20 and auth-openidc. |
63 | | #define OM_CLASS_AUTHN "authn" // Authentication request creation and response processing. |
64 | | #define OM_CLASS_AUTHZ "authz" // Authorization errors per OIDCUnAutzAction (per Require statement, not overall). |
65 | | #define OM_CLASS_REQUIRE_CLAIM "require.claim" // Match/failure count of Require claim directives (per Require statement, not overall). |
66 | | #define OM_CLASS_CLAIM "claim" // Claims per value |
67 | | #define OM_CLASS_PROVIDER "provider" // Requests to the provider [token, userinfo, metadata] endpoints. |
68 | | #define OM_CLASS_SESSION "session" // Existing session processing. |
69 | | #define OM_CLASS_CACHE "cache" // Cache read/write timings and errors. |
70 | | #define OM_CLASS_LOGOUT "logout" // Front- and back-channel logout processing. |
71 | | #define OM_CLASS_REDIRECT_URI "redirect_uri" // Requests to the Redirect URI, per type. |
72 | | #define OM_CLASS_CONTENT "content" // Requests to the content handler, per type of request: info, metrics, jwks, etc. |
73 | | |
74 | | // KEEP THIS: end-of-classes |
75 | | |
76 | | // NB: order must match the oidc_metrics_timing_type_t enum type in metrics.h |
77 | | |
78 | | const oidc_metrics_timing_info_t _oidc_metrics_timings_info[] = { |
79 | | |
80 | | // KEEP THIS: start-of-timers |
81 | | |
82 | | { OM_CLASS_AUTH_TYPE, "handler", "the overall authn+authz processing time" }, |
83 | | |
84 | | { OM_CLASS_AUTHN, "request", "authentication requests" }, |
85 | | { OM_CLASS_AUTHN, "response", "authentication responses" }, |
86 | | |
87 | | { OM_CLASS_SESSION, "valid", "successfully validated existing sessions" }, |
88 | | |
89 | | { OM_CLASS_PROVIDER, "metadata", "provider discovery document requests" }, |
90 | | { OM_CLASS_PROVIDER, "token", "provider token requests" }, |
91 | | { OM_CLASS_PROVIDER, "refresh", "provider refresh token requests" }, |
92 | | { OM_CLASS_PROVIDER, "userinfo", "provider userinfo requests" }, |
93 | | { OM_CLASS_PROVIDER, "jwks", "provider JWKs document requests" }, |
94 | | { OM_CLASS_PROVIDER, "par", "provider pushed authorization requests" }, |
95 | | |
96 | | { OM_CLASS_CACHE, "read", "cache read requests" }, |
97 | | { OM_CLASS_CACHE, "write", "cache write requests" }, |
98 | | |
99 | | // KEEP THIS: end-of-timers |
100 | | |
101 | | }; |
102 | | |
103 | | // NB: order must match the oidc_metrics_counter_type_t enum type in metrics.h |
104 | | |
105 | | const oidc_metrics_counter_info_t _oidc_metrics_counters_info[] = { |
106 | | #define OIDC_METRICS_COUNTER_INFO(id, class, name, desc) {class, name, desc}, |
107 | | OIDC_METRICS_COUNTERS_LIST(OIDC_METRICS_COUNTER_INFO) |
108 | | #undef OIDC_METRICS_COUNTER_INFO |
109 | | }; |
110 | | |
111 | | // clang-format on |
112 | | |
113 | | /* one entry per oidc_metrics_counter_type_t value */ |
114 | | OIDC_STATIC_ASSERT(sizeof(_oidc_metrics_counters_info) / sizeof(oidc_metrics_counter_info_t) == OM_NUMBER_OF_COUNTERS, |
115 | | counters_info_matches_enum); |
116 | | |
117 | | typedef struct oidc_metrics_t { |
118 | | apr_hash_t *counters; |
119 | | apr_hash_t *timings; |
120 | | } oidc_metrics_t; |
121 | | |
122 | | // pointer to the shared memory segment that holds the JSON metrics data |
123 | | static apr_shm_t *_oidc_metrics_cache = NULL; |
124 | | // flag to record if we are a parent process or a child process |
125 | | static apr_byte_t _oidc_metrics_is_parent = FALSE; |
126 | | /* Atomic stop flag shared by the flush, cleanup, and child-init paths. */ |
127 | | static volatile apr_uint32_t _oidc_metrics_thread_exit = 0; |
128 | | // mutex to protect the shared memory storage |
129 | | static oidc_cache_mutex_t *_oidc_metrics_global_mutex = NULL; |
130 | | // pointer to the thread that periodically writes the locally gathered metrics to shared memory |
131 | | static apr_thread_t *_oidc_metrics_thread = NULL; |
132 | | // pid that owns _oidc_metrics_thread; used to tell a post-fork stale handle from a same-process double-init |
133 | | static pid_t _oidc_metrics_thread_pid = 0; |
134 | | // local in-memory cached metrics |
135 | | static oidc_metrics_t _oidc_metrics = {NULL, NULL}; |
136 | | // mutex to protect the local metrics hash table |
137 | | static oidc_cache_mutex_t *_oidc_metrics_process_mutex = NULL; |
138 | | |
139 | | // default shared memory write interval in seconds |
140 | | #define OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT 5000 |
141 | | |
142 | | // maximum length of the string representation of the global JSON metrics data in shared memory |
143 | | // 1024 sample size (compact, long keys, large json_int values, no description), timing + counter |
144 | | // 256 number of individual metrics collected |
145 | | // 4 number of vhosts supported |
146 | 0 | #define OIDC_METRICS_CACHE_JSON_MAX_DEFAULT (1024 * 256 * 4) |
147 | | |
148 | | typedef struct oidc_metrics_bucket_t { |
149 | | const char *name; |
150 | | const char *label; |
151 | | apr_time_t threshold; |
152 | | } oidc_metrics_bucket_t; |
153 | | |
154 | | // clang-format off |
155 | | |
156 | | static oidc_metrics_bucket_t _oidc_metric_buckets[] = { |
157 | | { "le01", "le=\"0.1\"", 100 }, |
158 | | { "le05", "le=\"0.5\"", 500 }, |
159 | | { "le1", "le=\"1\"", apr_time_from_msec(1) }, |
160 | | { "le5", "le=\"5\"", apr_time_from_msec(5) }, |
161 | | { "le10", "le=\"10\"", apr_time_from_msec(10) }, |
162 | | { "le50", "le=\"50\"", apr_time_from_msec(50) }, |
163 | | { "le100", "le=\"100\"", apr_time_from_msec(100) }, |
164 | | { "le500", "le=\"500\"", apr_time_from_msec(500) }, |
165 | | { "le1000", "le=\"1000\"", apr_time_from_msec(1000) }, |
166 | | { "le5000", "le=\"5000\"", apr_time_from_msec(5000) }, |
167 | | { "inf", "le=\"+Inf\"", 0 } |
168 | | }; |
169 | | |
170 | | // clang-format on |
171 | | |
172 | 0 | #define OIDC_METRICS_BUCKET_NUM ((int)(sizeof(_oidc_metric_buckets) / sizeof(oidc_metrics_bucket_t))) |
173 | | |
174 | | // NB: matters for Prometheus formatting |
175 | 0 | #define OIDC_METRICS_SUM "sum" |
176 | 0 | #define OIDC_METRICS_COUNT "count" |
177 | | |
178 | | #define OIDC_METRICS_SPECS "specs" |
179 | | |
180 | 0 | #define OIDC_METRICS_JSON_CLASS_NAME "class" |
181 | 0 | #define OIDC_METRICS_JSON_METRIC_NAME "name" |
182 | 0 | #define OIDC_METRICS_JSON_DESC "desc" |
183 | | |
184 | 0 | #define OIDC_METRICS_TIMINGS "timings" |
185 | 0 | #define OIDC_METRICS_COUNTERS "counters" |
186 | | |
187 | | /* |
188 | | * convert a Jansson number to a string: OIDC_JSON_INT_FORMAT does not work with apr_psprintf !? |
189 | | */ |
190 | 0 | static inline char *_json_int2str(apr_pool_t *pool, oidc_json_int_t n) { |
191 | 0 | char s[255]; |
192 | 0 | snprintf(s, 255, "%" OIDC_JSON_INT_FORMAT, n); |
193 | 0 | return apr_pstrdup(pool, s); |
194 | 0 | } |
195 | | |
196 | 0 | #define OIDC_METRICS_INT_MAX OIDC_JSON_INT_MAX |
197 | | |
198 | | /* |
199 | | * check Jansson specific integer/long number overrun |
200 | | */ |
201 | 0 | static inline int _is_overflow(server_rec *s, oidc_json_int_t cur, oidc_json_int_t add) { |
202 | 0 | if (add > (OIDC_METRICS_INT_MAX - cur)) { |
203 | 0 | oidc_swarn(s, |
204 | 0 | "reset metrics since the size (%s) of the integer value would be larger than the " |
205 | 0 | "JSON/libjansson maximum " |
206 | 0 | "(%s)", |
207 | 0 | _json_int2str(s->process->pool, add), _json_int2str(s->process->pool, OIDC_METRICS_INT_MAX)); |
208 | 0 | return 1; |
209 | 0 | } |
210 | 0 | return 0; |
211 | 0 | } |
212 | | |
213 | | // single counter container |
214 | | typedef struct oidc_metrics_counter_t { |
215 | | oidc_json_int_t count; |
216 | | } oidc_metrics_counter_t; |
217 | | |
218 | | // single timing stats container |
219 | | typedef struct oidc_metrics_timing_t { |
220 | | oidc_json_int_t buckets[OIDC_METRICS_BUCKET_NUM]; |
221 | | apr_time_t sum; |
222 | | oidc_json_int_t count; |
223 | | } oidc_metrics_timing_t; |
224 | | |
225 | | // context holder for parsing valid classnames |
226 | | typedef struct oidc_metrics_add_classname_ctx_t { |
227 | | apr_pool_t *pool; |
228 | | char **valid_names; |
229 | | } oidc_metrics_add_classname_ctx_t; |
230 | | |
231 | | /* |
232 | | * loop function for parsing valid classnames |
233 | | */ |
234 | 0 | static int oidc_metrics_add_classnames(void *rec, const char *key, const char *value) { |
235 | 0 | oidc_metrics_add_classname_ctx_t *ctx = (oidc_metrics_add_classname_ctx_t *)rec; |
236 | 0 | *ctx->valid_names = apr_psprintf(ctx->pool, "%s%s%s", *ctx->valid_names ? *ctx->valid_names : "", |
237 | 0 | *ctx->valid_names ? " | " : "", value); |
238 | 0 | return 1; |
239 | 0 | } |
240 | | |
241 | | /* |
242 | | * check if the provided value is a valid classname |
243 | | */ |
244 | 0 | apr_byte_t oidc_metrics_is_valid_classname(apr_pool_t *pool, const char *name, char **valid_names) { |
245 | 0 | int i = 0; |
246 | 0 | int n = 0; |
247 | 0 | apr_table_t *names = apr_table_make(pool, 1); |
248 | 0 | oidc_metrics_add_classname_ctx_t ctx = {pool, valid_names}; |
249 | |
|
250 | 0 | n = sizeof(_oidc_metrics_timings_info) / sizeof(oidc_metrics_timing_info_t); |
251 | 0 | for (i = 0; i < n; i++) { |
252 | 0 | apr_table_set(names, _oidc_metrics_timings_info[i].class_name, |
253 | 0 | _oidc_metrics_timings_info[i].class_name); |
254 | 0 | } |
255 | 0 | n = sizeof(_oidc_metrics_counters_info) / sizeof(oidc_metrics_counter_info_t); |
256 | 0 | for (i = 0; i < n; i++) { |
257 | | // NB: "claim" is the only counter class with a name/value option and is special-cased here (rather |
258 | | // than driven from a static list) |
259 | 0 | if (_oidc_strcmp(_oidc_metrics_counters_info[i].class_name, "claim") != 0) |
260 | 0 | apr_table_set(names, _oidc_metrics_counters_info[i].class_name, |
261 | 0 | _oidc_metrics_counters_info[i].class_name); |
262 | 0 | } |
263 | |
|
264 | 0 | *valid_names = NULL; |
265 | 0 | apr_table_do(oidc_metrics_add_classnames, &ctx, names, NULL); |
266 | 0 | *valid_names = apr_psprintf(pool, "%s%s%s", *valid_names ? *valid_names : "", *valid_names ? " | " : "", |
267 | 0 | "claim.id_token.* | claim.userinfo.*"); |
268 | |
|
269 | 0 | return apr_table_get(names, name) ? TRUE |
270 | 0 | : ((_oidc_strstr(name, "claim.id_token.") != NULL) || |
271 | 0 | (_oidc_strstr(name, "claim.userinfo.") != NULL)); |
272 | 0 | } |
273 | | |
274 | | /* |
275 | | * retrieve environment variable integer with default setting |
276 | | */ |
277 | 0 | static inline int _oidc_metrics_get_env_int(const char *name, int dval) { |
278 | 0 | return _oidc_str_to_int(getenv(name), dval); |
279 | 0 | } |
280 | | |
281 | 0 | #define OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR "OIDC_METRICS_CACHE_JSON_MAX" |
282 | | |
283 | | static apr_size_t _g_oidc_metrics_shm_size = 0; |
284 | | |
285 | | /* |
286 | | * get the size of the to-be-allocated shared memory segment |
287 | | */ |
288 | 0 | static inline apr_size_t _oidc_metrics_shm_size(server_rec *s) { |
289 | 0 | if (_g_oidc_metrics_shm_size == 0) { |
290 | 0 | int n = |
291 | 0 | _oidc_metrics_get_env_int(OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, OIDC_METRICS_CACHE_JSON_MAX_DEFAULT); |
292 | 0 | if ((n < 1) || (n > 1024 * 256 * 4 * 100)) { |
293 | 0 | oidc_serror(s, "environment value %s out of bounds, fallback to default", |
294 | 0 | OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR); |
295 | 0 | _g_oidc_metrics_shm_size = OIDC_METRICS_CACHE_JSON_MAX_DEFAULT; |
296 | 0 | } else { |
297 | 0 | _g_oidc_metrics_shm_size = n; |
298 | 0 | } |
299 | 0 | } |
300 | 0 | return _g_oidc_metrics_shm_size; |
301 | 0 | } |
302 | | |
303 | | /* |
304 | | * retrieve the (JSON) serialized (global) metrics data from shared memory |
305 | | */ |
306 | 0 | static inline char *_oidc_metrics_storage_get(server_rec *s, apr_pool_t *pool) { |
307 | 0 | const char *p = (char *)apr_shm_baseaddr_get(_oidc_metrics_cache); |
308 | 0 | return p && (*p != 0) ? apr_pstrndup(pool, p, _oidc_metrics_shm_size(s)) : NULL; |
309 | 0 | } |
310 | | |
311 | | /* |
312 | | * store the serialized (global) metrics data in shared memory |
313 | | */ |
314 | 0 | static inline void _oidc_metrics_storage_set(server_rec *s, const char *value) { |
315 | 0 | char *p = apr_shm_baseaddr_get(_oidc_metrics_cache); |
316 | 0 | if (value) { |
317 | 0 | apr_size_t n = _oidc_strlen(value) + 1; |
318 | 0 | if (n > _oidc_metrics_shm_size(s)) |
319 | 0 | oidc_serror(s, |
320 | 0 | "json value too large: set or increase system environment variable %s to a value " |
321 | 0 | "larger than %" APR_SIZE_T_FMT, |
322 | 0 | OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, _oidc_metrics_shm_size(s)); |
323 | 0 | else |
324 | 0 | _oidc_memcpy(p, value, n); |
325 | 0 | } else { |
326 | 0 | *p = 0; |
327 | 0 | } |
328 | 0 | } |
329 | | |
330 | | /* |
331 | | * parse a string into a JSON object |
332 | | */ |
333 | 0 | static oidc_json_t *oidc_metrics_json_load(apr_pool_t *pool, const char *s_json, char **s_err) { |
334 | 0 | oidc_json_t *json = NULL; |
335 | 0 | if (s_json == NULL) |
336 | 0 | s_json = "{}"; |
337 | 0 | oidc_json_parse(pool, s_json, 0, &json, s_err); |
338 | 0 | return json; |
339 | 0 | } |
340 | | |
341 | | /* |
342 | | * parse a string into a JSON object in a server_rec context |
343 | | */ |
344 | 0 | static oidc_json_t *oidc_metrics_json_parse_s(server_rec *s, apr_pool_t *pool, const char *s_json) { |
345 | 0 | char *s_err = NULL; |
346 | 0 | oidc_json_t *json = oidc_metrics_json_load(pool, s_json, &s_err); |
347 | 0 | if (json == NULL) |
348 | 0 | oidc_serror(s, "JSON parsing failed: %s", s_err); |
349 | 0 | return json; |
350 | 0 | } |
351 | | |
352 | | /* |
353 | | * recursively reset all integer leaf values within a JSON object tree to 0 |
354 | | */ |
355 | 0 | static void oidc_metrics_reset_integer_tree(oidc_json_t *json) { |
356 | 0 | void *iter = oidc_json_object_iter(json); |
357 | 0 | while (iter) { |
358 | 0 | oidc_json_t *value = oidc_json_object_iter_value(iter); |
359 | 0 | if (oidc_json_is_integer(value)) |
360 | 0 | oidc_json_integer_set(value, 0); |
361 | 0 | else if (oidc_json_is_object(value)) |
362 | 0 | oidc_metrics_reset_integer_tree(value); |
363 | 0 | iter = oidc_json_object_iter_next(json, iter); |
364 | 0 | } |
365 | 0 | } |
366 | | |
367 | | /* |
368 | | * reset a single timings entry (buckets, sum, count) to 0 |
369 | | */ |
370 | 0 | static void oidc_metrics_reset_timer(oidc_json_t *j_entry) { |
371 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
372 | 0 | oidc_json_object_set_new(j_entry, _oidc_metric_buckets[i].name, oidc_json_integer(0)); |
373 | 0 | oidc_json_object_set_new(j_entry, OIDC_METRICS_SUM, oidc_json_integer(0)); |
374 | 0 | oidc_json_object_set_new(j_entry, OIDC_METRICS_COUNT, oidc_json_integer(0)); |
375 | 0 | } |
376 | | |
377 | | /* |
378 | | * reset all counter and timer entries for a single server in the JSON data |
379 | | */ |
380 | 0 | static void oidc_metrics_reset_server(const oidc_json_t *j_server) { |
381 | 0 | oidc_json_t *j_entries = NULL; |
382 | 0 | void *iter = NULL; |
383 | |
|
384 | 0 | j_entries = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
385 | 0 | if (j_entries) |
386 | 0 | oidc_metrics_reset_integer_tree(j_entries); |
387 | |
|
388 | 0 | j_entries = oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS); |
389 | 0 | iter = oidc_json_object_iter(j_entries); |
390 | 0 | while (iter) { |
391 | 0 | oidc_metrics_reset_timer(oidc_json_object_iter_value(iter)); |
392 | 0 | iter = oidc_json_object_iter_next(j_entries, iter); |
393 | 0 | } |
394 | 0 | } |
395 | | |
396 | | /* |
397 | | * reset the serialized (global) metrics data in shared memory |
398 | | */ |
399 | 0 | static inline void oidc_metrics_storage_reset(server_rec *s, apr_pool_t *pool) { |
400 | 0 | const char *s_json = NULL; |
401 | 0 | oidc_json_t *json = NULL; |
402 | 0 | void *iter = NULL; |
403 | | |
404 | | /* get the global stringified JSON metrics */ |
405 | 0 | s_json = _oidc_metrics_storage_get(s, pool); |
406 | | |
407 | | /* parse the metrics string to JSON */ |
408 | 0 | json = oidc_metrics_json_parse_s(s, pool, s_json); |
409 | 0 | if (json == NULL) |
410 | 0 | json = oidc_json_object(); |
411 | |
|
412 | 0 | iter = oidc_json_object_iter(json); |
413 | 0 | while (iter) { |
414 | 0 | oidc_metrics_reset_server(oidc_json_object_iter_value(iter)); |
415 | 0 | iter = oidc_json_object_iter_next(json, iter); |
416 | 0 | } |
417 | | |
418 | | /* serialize the metrics data, preserve order is required for Prometheus */ |
419 | 0 | s_json = oidc_json_encode(pool, json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
420 | | |
421 | | /* free the JSON data */ |
422 | 0 | oidc_json_decref(json); |
423 | | |
424 | | /* store the serialized metrics data in shared memory */ |
425 | 0 | _oidc_metrics_storage_set(s, s_json); |
426 | 0 | } |
427 | | |
428 | | /* |
429 | | * create a new timings entry in the collected JSON data |
430 | | */ |
431 | 0 | static oidc_json_t *oidc_metrics_timings_new(const oidc_metrics_timing_t *timing) { |
432 | 0 | oidc_json_t *entry = oidc_json_object(); |
433 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
434 | 0 | oidc_json_object_set_new(entry, _oidc_metric_buckets[i].name, oidc_json_integer(timing->buckets[i])); |
435 | 0 | oidc_json_object_set_new(entry, OIDC_METRICS_SUM, oidc_json_integer(apr_time_as_msec(timing->sum))); |
436 | 0 | oidc_json_object_set_new(entry, OIDC_METRICS_COUNT, oidc_json_integer(timing->count)); |
437 | 0 | return entry; |
438 | 0 | } |
439 | | |
440 | | /* |
441 | | * update an entry in the collected JSON data |
442 | | */ |
443 | 0 | static void oidc_metrics_timings_update(server_rec *s, const oidc_json_t *entry, const oidc_metrics_timing_t *timing) { |
444 | 0 | oidc_json_t *j_member = NULL; |
445 | 0 | oidc_json_int_t n = 0; |
446 | 0 | oidc_json_int_t v = 0; |
447 | |
|
448 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { |
449 | 0 | j_member = oidc_json_object_get(entry, _oidc_metric_buckets[i].name); |
450 | 0 | oidc_json_integer_set(j_member, oidc_json_integer_value(j_member) + timing->buckets[i]); |
451 | 0 | } |
452 | |
|
453 | 0 | j_member = oidc_json_object_get(entry, OIDC_METRICS_SUM); |
454 | 0 | n = oidc_json_integer_value(j_member); |
455 | |
|
456 | 0 | v = apr_time_as_msec(timing->sum); |
457 | 0 | if (_is_overflow(s, n, v)) |
458 | 0 | n = 0; |
459 | |
|
460 | 0 | oidc_json_integer_set(j_member, n + v); |
461 | |
|
462 | 0 | j_member = oidc_json_object_get(entry, OIDC_METRICS_COUNT); |
463 | 0 | n = oidc_json_integer_value(j_member); |
464 | 0 | oidc_json_integer_set(j_member, n + timing->count); |
465 | 0 | } |
466 | | |
467 | 0 | #define OIDC_METRICS_VALUE_DEFAULT "_" |
468 | | |
469 | | /* |
470 | | * value helper to make sure it is not empty |
471 | | */ |
472 | 0 | static inline const char *_metrics_value2key(const char *value) { |
473 | 0 | return (value && _oidc_strcmp(value, "") != 0) ? value : OIDC_METRICS_VALUE_DEFAULT; |
474 | 0 | } |
475 | | |
476 | | /* |
477 | | * create a new counter entry in the collected JSON data |
478 | | */ |
479 | 0 | static oidc_json_t *oidc_metrics_counter_new(apr_hash_t *htable) { |
480 | 0 | oidc_metrics_counter_t *counter = NULL; |
481 | 0 | char *value = NULL; |
482 | 0 | oidc_json_t *j_values = NULL; |
483 | 0 | for (apr_hash_index_t *hi = apr_hash_first(NULL, htable); hi; hi = apr_hash_next(hi)) { |
484 | 0 | apr_hash_this(hi, (const void **)&value, NULL, (void **)&counter); |
485 | 0 | if (_oidc_strcmp(value, OIDC_METRICS_VALUE_DEFAULT) == 0) { |
486 | 0 | j_values = oidc_json_integer(counter->count); |
487 | 0 | } else { |
488 | 0 | if (j_values == NULL) |
489 | 0 | j_values = oidc_json_object(); |
490 | 0 | oidc_json_object_set_new(j_values, value, oidc_json_integer(counter->count)); |
491 | 0 | } |
492 | 0 | } |
493 | 0 | return j_values; |
494 | 0 | } |
495 | | |
496 | | /* |
497 | | * update a counter entry in the collected JSON data |
498 | | */ |
499 | 0 | static void oidc_metrics_counter_update(server_rec *s, oidc_json_t *j_counter, apr_hash_t *htable) { |
500 | 0 | oidc_json_int_t v = 0; |
501 | 0 | oidc_metrics_counter_t *counter = NULL; |
502 | 0 | char *value = NULL; |
503 | 0 | oidc_json_t *j_value = NULL; |
504 | 0 | for (apr_hash_index_t *hi = apr_hash_first(NULL, htable); hi; hi = apr_hash_next(hi)) { |
505 | 0 | apr_hash_this(hi, (const void **)&value, NULL, (void **)&counter); |
506 | 0 | if (_oidc_strcmp(value, OIDC_METRICS_VALUE_DEFAULT) == 0) { |
507 | 0 | j_value = j_counter; |
508 | 0 | } else { |
509 | 0 | j_value = oidc_json_object_get(j_counter, value); |
510 | 0 | if (j_value == NULL) { |
511 | 0 | oidc_json_object_set_new(j_counter, value, oidc_json_integer(counter->count)); |
512 | 0 | continue; |
513 | 0 | } |
514 | 0 | } |
515 | 0 | v = oidc_json_integer_value(j_value); |
516 | 0 | if (_is_overflow(s, v, counter->count)) |
517 | 0 | v = 0; |
518 | 0 | oidc_json_integer_set(j_value, v + counter->count); |
519 | 0 | } |
520 | 0 | } |
521 | | |
522 | | /* |
523 | | * get or create the vhost entry in the global metrics |
524 | | */ |
525 | 0 | static oidc_json_t *oidc_metrics_server_get(oidc_json_t *json, const char *name) { |
526 | 0 | oidc_json_t *j_server = oidc_json_object_get(json, name); |
527 | 0 | if (j_server == NULL) { |
528 | 0 | j_server = oidc_json_object(); |
529 | 0 | oidc_json_object_set_new(j_server, OIDC_METRICS_COUNTERS, oidc_json_object()); |
530 | 0 | oidc_json_object_set_new(j_server, OIDC_METRICS_TIMINGS, oidc_json_object()); |
531 | 0 | oidc_json_object_set_new(json, name, j_server); |
532 | 0 | } |
533 | 0 | return j_server; |
534 | 0 | } |
535 | | |
536 | | /* |
537 | | * convert an enum type value to its corresponding string |
538 | | */ |
539 | 0 | static inline char *_oidc_metrics_type_name2key(apr_pool_t *pool, unsigned int type, const char *name) { |
540 | 0 | return (name == NULL) ? apr_psprintf(pool, "%u", type) : apr_psprintf(pool, "%u.%s", type, name); |
541 | 0 | } |
542 | | |
543 | | /* |
544 | | * convert a string key type to an enum type |
545 | | */ |
546 | 0 | static inline unsigned int _oidc_metrics_key2type(const char *key) { |
547 | 0 | unsigned int type = 0; |
548 | 0 | sscanf(key, "%u", &type); |
549 | 0 | return type; |
550 | 0 | } |
551 | | |
552 | | /* |
553 | | * update an existing counter under parent[key] or create a new one from counter_hash |
554 | | */ |
555 | | static void oidc_metrics_counter_set_or_update(server_rec *s, oidc_json_t *parent, const char *key, |
556 | 0 | apr_hash_t *counter_hash) { |
557 | 0 | oidc_json_t *j_counter = oidc_json_object_get(parent, key); |
558 | 0 | if (j_counter != NULL) |
559 | 0 | oidc_metrics_counter_update(s, j_counter, counter_hash); |
560 | 0 | else |
561 | 0 | oidc_json_object_set_new(parent, key, oidc_metrics_counter_new(counter_hash)); |
562 | 0 | } |
563 | | |
564 | | /* |
565 | | * merge a single local counter entry into the global counters object; |
566 | | * keys of the form "class.name" are nested as j_counters[class][name] |
567 | | */ |
568 | | static void oidc_metrics_store_counter_entry(server_rec *s, apr_pool_t *pool, oidc_json_t *j_counters, const char *key, |
569 | 0 | apr_hash_t *counter_hash) { |
570 | 0 | const char *class_name = apr_pstrdup(pool, key); |
571 | 0 | char *name = _oidc_strstr(class_name, "."); |
572 | 0 | oidc_json_t *j_names = NULL; |
573 | |
|
574 | 0 | if (name == NULL) { |
575 | 0 | oidc_metrics_counter_set_or_update(s, j_counters, class_name, counter_hash); |
576 | 0 | return; |
577 | 0 | } |
578 | | |
579 | 0 | *name++ = '\0'; |
580 | 0 | j_names = oidc_json_object_get(j_counters, class_name); |
581 | 0 | if (j_names == NULL) { |
582 | 0 | j_names = oidc_json_object(); |
583 | 0 | oidc_json_object_set_new(j_names, name, oidc_metrics_counter_new(counter_hash)); |
584 | 0 | oidc_json_object_set_new(j_counters, class_name, j_names); |
585 | 0 | return; |
586 | 0 | } |
587 | | |
588 | 0 | oidc_metrics_counter_set_or_update(s, j_names, name, counter_hash); |
589 | 0 | } |
590 | | |
591 | | /* |
592 | | * merge all locally collected counters into the global JSON |
593 | | */ |
594 | 0 | static void oidc_metrics_store_counters(server_rec *s, apr_pool_t *pool, oidc_json_t *json) { |
595 | 0 | const char *name = NULL; |
596 | 0 | const char *key = NULL; |
597 | 0 | apr_hash_t *server_hash = NULL; |
598 | 0 | apr_hash_t *counter_hash = NULL; |
599 | 0 | oidc_json_t *j_counters = NULL; |
600 | |
|
601 | 0 | for (apr_hash_index_t *hi1 = apr_hash_first(NULL, _oidc_metrics.counters); hi1; hi1 = apr_hash_next(hi1)) { |
602 | 0 | apr_hash_this(hi1, (const void **)&name, NULL, (void **)&server_hash); |
603 | 0 | j_counters = oidc_json_object_get(oidc_metrics_server_get(json, name), OIDC_METRICS_COUNTERS); |
604 | 0 | for (apr_hash_index_t *hi2 = apr_hash_first(NULL, server_hash); hi2; hi2 = apr_hash_next(hi2)) { |
605 | 0 | apr_hash_this(hi2, (const void **)&key, NULL, (void **)&counter_hash); |
606 | 0 | oidc_metrics_store_counter_entry(s, pool, j_counters, key, counter_hash); |
607 | 0 | } |
608 | 0 | } |
609 | 0 | } |
610 | | |
611 | | /* |
612 | | * merge all locally collected timings into the global JSON |
613 | | */ |
614 | 0 | static void oidc_metrics_store_timings(server_rec *s, oidc_json_t *json) { |
615 | 0 | const char *name = NULL; |
616 | 0 | const char *key = NULL; |
617 | 0 | apr_hash_t *server_hash = NULL; |
618 | 0 | oidc_metrics_timing_t *timing = NULL; |
619 | 0 | oidc_json_t *j_timings = NULL; |
620 | 0 | const oidc_json_t *j_timer = NULL; |
621 | |
|
622 | 0 | for (apr_hash_index_t *hi1 = apr_hash_first(NULL, _oidc_metrics.timings); hi1; hi1 = apr_hash_next(hi1)) { |
623 | 0 | apr_hash_this(hi1, (const void **)&name, NULL, (void **)&server_hash); |
624 | 0 | j_timings = oidc_json_object_get(oidc_metrics_server_get(json, name), OIDC_METRICS_TIMINGS); |
625 | 0 | for (apr_hash_index_t *hi2 = apr_hash_first(NULL, server_hash); hi2; hi2 = apr_hash_next(hi2)) { |
626 | 0 | apr_hash_this(hi2, (const void **)&key, NULL, (void **)&timing); |
627 | 0 | j_timer = oidc_json_object_get(j_timings, key); |
628 | 0 | if (j_timer != NULL) |
629 | 0 | oidc_metrics_timings_update(s, j_timer, timing); |
630 | 0 | else |
631 | 0 | oidc_json_object_set_new(j_timings, key, oidc_metrics_timings_new(timing)); |
632 | 0 | } |
633 | 0 | } |
634 | 0 | } |
635 | | |
636 | | /* |
637 | | * flush the locally gathered metrics data into the global data kept in shared memory |
638 | | */ |
639 | 0 | static void oidc_metrics_store(server_rec *s) { |
640 | 0 | const char *s_json = NULL; |
641 | 0 | oidc_json_t *json = NULL; |
642 | 0 | apr_pool_t *pool = NULL; |
643 | |
|
644 | 0 | if ((apr_hash_count(_oidc_metrics.counters) == 0) && (apr_hash_count(_oidc_metrics.timings) == 0)) |
645 | 0 | return; |
646 | | |
647 | | /* everything below is scratch: the whole document is read, re-serialized and copied |
648 | | * into shared memory on every flush, so it must not come from the process pool, |
649 | | * which is never cleared for the lifetime of the server */ |
650 | 0 | if (apr_pool_create(&pool, s->process->pool) != APR_SUCCESS) { |
651 | 0 | oidc_serror(s, "apr_pool_create failed: cannot flush metrics"); |
652 | 0 | return; |
653 | 0 | } |
654 | | |
655 | | /* lock the shared memory for other processes */ |
656 | 0 | oidc_cache_mutex_lock(pool, s, _oidc_metrics_global_mutex); |
657 | | |
658 | | /* get the global stringified JSON metrics */ |
659 | 0 | s_json = _oidc_metrics_storage_get(s, pool); |
660 | | |
661 | | /* parse the metrics string to JSON */ |
662 | 0 | json = oidc_metrics_json_parse_s(s, pool, s_json); |
663 | 0 | if (json == NULL) |
664 | 0 | json = oidc_json_object(); |
665 | |
|
666 | 0 | oidc_metrics_store_counters(s, pool, json); |
667 | 0 | oidc_metrics_store_timings(s, json); |
668 | | |
669 | | /* serialize the metrics data, preserve order is required for Prometheus */ |
670 | 0 | s_json = oidc_json_encode(pool, json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
671 | | |
672 | | /* free the JSON data */ |
673 | 0 | oidc_json_decref(json); |
674 | | |
675 | | /* store the serialized metrics data in shared memory */ |
676 | 0 | _oidc_metrics_storage_set(s, s_json); |
677 | | |
678 | | /* unlock the shared memory for other processes */ |
679 | 0 | oidc_cache_mutex_unlock(pool, s, _oidc_metrics_global_mutex); |
680 | |
|
681 | 0 | apr_pool_destroy(pool); |
682 | 0 | } |
683 | | |
684 | | #define OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR "OIDC_METRICS_CACHE_STORAGE_INTERVAL" |
685 | | |
686 | | /* |
687 | | * obtain the metrics flush interval from the environment variables |
688 | | */ |
689 | 0 | static inline apr_interval_time_t _oidc_metrics_interval(void) { |
690 | 0 | return apr_time_from_msec(_oidc_metrics_get_env_int(OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR, |
691 | 0 | OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT)); |
692 | 0 | } |
693 | | |
694 | | #define OIDC_METRICS_POLL_INTERVAL 250 |
695 | | |
696 | | /* |
697 | | * thread that periodically writes the local data into the shared memory |
698 | | */ |
699 | 0 | static void *APR_THREAD_FUNC oidc_metrics_thread_run(apr_thread_t *thread, void *data) { |
700 | 0 | server_rec *s = (server_rec *)data; |
701 | | |
702 | | /* sleep for a short random time <0.1s so child processes write-lock on a different frequency */ |
703 | 0 | apr_sleep(apr_time_from_msec(oidc_util_rand_int(100))); |
704 | | |
705 | | /* split the flush interval into POLL_INTERVAL-sized ticks so shutdown is observed quickly; if the |
706 | | * configured interval is shorter than POLL_INTERVAL, use it as the tick directly so we still flush |
707 | | * on schedule rather than busy-looping with n=0 */ |
708 | 0 | apr_interval_time_t interval = _oidc_metrics_interval(); |
709 | | /* a misconfigured env var parses to 0 (or negative) — fall back to the default to avoid 0/0 below */ |
710 | 0 | if (interval <= 0) |
711 | 0 | interval = apr_time_from_msec(OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT); |
712 | 0 | apr_interval_time_t tick = apr_time_from_msec(OIDC_METRICS_POLL_INTERVAL); |
713 | 0 | if (tick > interval) |
714 | 0 | tick = interval; |
715 | 0 | int n = (int)(interval / tick); |
716 | | |
717 | | /* see if we are asked to exit */ |
718 | 0 | while (apr_atomic_read32(&_oidc_metrics_thread_exit) == 0) { |
719 | | |
720 | | /* break up the sleep interval in short intervals so we can exit timely fashion without confusing Apache |
721 | | * at shutdown */ |
722 | 0 | for (int i = 0; i < n; i++) { |
723 | 0 | apr_sleep(tick); |
724 | 0 | if (apr_atomic_read32(&_oidc_metrics_thread_exit) != 0) |
725 | 0 | break; |
726 | 0 | } |
727 | | |
728 | | // NB: no exit here because we need to write our local metrics into the cache before exiting |
729 | | |
730 | | /* lock the mutex that protects the locally cached metrics */ |
731 | 0 | oidc_cache_mutex_lock(s->process->pool, s, _oidc_metrics_process_mutex); |
732 | | |
733 | | /* flush the locally cached metrics into the global shared memory */ |
734 | 0 | oidc_metrics_store(s); |
735 | | |
736 | | /* reset the local hashtables */ |
737 | 0 | oidc_util_apr_hash_clear(_oidc_metrics.counters); |
738 | 0 | oidc_util_apr_hash_clear(_oidc_metrics.timings); |
739 | | |
740 | | /* unlock the mutex that protects the locally cached metrics */ |
741 | 0 | oidc_cache_mutex_unlock(s->process->pool, s, _oidc_metrics_process_mutex); |
742 | 0 | } |
743 | | |
744 | | /* NB: don't call apr_thread_exit here because it seems that Apache is cleaning up its own threads */ |
745 | |
|
746 | 0 | return NULL; |
747 | 0 | } |
748 | | |
749 | | /* |
750 | | * server config handlers |
751 | | */ |
752 | | |
753 | | /* |
754 | | * NB: global, yet called for each vhost that has metrics enabled! |
755 | | */ |
756 | 0 | apr_byte_t oidc_metrics_post_config(apr_pool_t *pool, server_rec *s) { |
757 | | |
758 | | /* make sure it gets executed exactly once! */ |
759 | 0 | if (_oidc_metrics_cache != NULL) |
760 | 0 | return TRUE; |
761 | | |
762 | | /* create the shared memory segment that holds the stringified JSON formatted metrics data */ |
763 | 0 | if (apr_shm_create(&_oidc_metrics_cache, _oidc_metrics_shm_size(s), NULL, s->process->pool) != APR_SUCCESS) |
764 | 0 | return FALSE; |
765 | 0 | if (_oidc_metrics_cache == NULL) |
766 | 0 | return FALSE; |
767 | | |
768 | | /* initialize the shared memory segment to 0 */ |
769 | 0 | char *p = apr_shm_baseaddr_get(_oidc_metrics_cache); |
770 | 0 | _oidc_memset(p, 0, _oidc_metrics_shm_size(s)); |
771 | | |
772 | | /* flag this as the parent, for shared memory cleanup purposes and "multiple child-init calls" detection */ |
773 | 0 | _oidc_metrics_is_parent = TRUE; |
774 | | |
775 | | /* create the thread that will periodically flush the local metrics data to shared memory */ |
776 | 0 | if (apr_thread_create(&_oidc_metrics_thread, NULL, oidc_metrics_thread_run, s, s->process->pool) != APR_SUCCESS) |
777 | 0 | return FALSE; |
778 | 0 | _oidc_metrics_thread_pid = getpid(); |
779 | | |
780 | | /* create the hashtable that holds local metrics data */ |
781 | 0 | _oidc_metrics.counters = apr_hash_make(s->process->pool); |
782 | 0 | _oidc_metrics.timings = apr_hash_make(s->process->pool); |
783 | | |
784 | | /* create and initialize the mutex that guards _oidc_metrics_hash */ |
785 | 0 | _oidc_metrics_global_mutex = oidc_cache_mutex_create(s->process->pool, TRUE); |
786 | 0 | if (_oidc_metrics_global_mutex == NULL) |
787 | 0 | return FALSE; |
788 | 0 | if (oidc_cache_mutex_post_config(s->process->pool, s, _oidc_metrics_global_mutex, "metrics-global") == FALSE) |
789 | 0 | return FALSE; |
790 | | |
791 | | /* create and initialize the mutex that guards the shared memory */ |
792 | 0 | _oidc_metrics_process_mutex = oidc_cache_mutex_create(s->process->pool, FALSE); |
793 | 0 | if (_oidc_metrics_process_mutex == NULL) |
794 | 0 | return FALSE; |
795 | 0 | if (oidc_cache_mutex_post_config(s->process->pool, s, _oidc_metrics_process_mutex, "metrics-process") == FALSE) |
796 | 0 | return FALSE; |
797 | | |
798 | 0 | return TRUE; |
799 | 0 | } |
800 | | |
801 | | /* |
802 | | * NB: global, yet called for each vhost that has metrics enabled! |
803 | | */ |
804 | 0 | apr_status_t oidc_metrics_child_init(apr_pool_t *p, server_rec *s) { |
805 | 0 | apr_status_t rv = APR_SUCCESS; |
806 | | |
807 | | /* make sure this executes only once per child */ |
808 | 0 | if (_oidc_metrics_is_parent == FALSE) |
809 | 0 | return APR_SUCCESS; |
810 | | |
811 | 0 | if (oidc_cache_mutex_child_init(p, s, _oidc_metrics_global_mutex) != APR_SUCCESS) |
812 | 0 | return APR_EGENERAL; |
813 | | |
814 | 0 | if (oidc_cache_mutex_child_init(p, s, _oidc_metrics_process_mutex) != APR_SUCCESS) |
815 | 0 | return APR_EGENERAL; |
816 | | |
817 | | /* Stop an existing same-process flush thread; discard inherited handles after a fork. */ |
818 | 0 | if (_oidc_metrics_thread != NULL && _oidc_metrics_thread_pid == getpid()) { |
819 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 1); |
820 | 0 | apr_thread_join(&rv, _oidc_metrics_thread); |
821 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 0); |
822 | 0 | } |
823 | 0 | _oidc_metrics_thread = NULL; |
824 | | |
825 | | /* the metrics flush thread is not inherited from the parent, so re-create it in the child */ |
826 | 0 | if (apr_thread_create(&_oidc_metrics_thread, NULL, oidc_metrics_thread_run, s, s->process->pool) != APR_SUCCESS) |
827 | 0 | return APR_EGENERAL; |
828 | 0 | _oidc_metrics_thread_pid = getpid(); |
829 | | |
830 | | /* flag this is a child */ |
831 | 0 | _oidc_metrics_is_parent = FALSE; |
832 | |
|
833 | 0 | return APR_SUCCESS; |
834 | 0 | } |
835 | | |
836 | | /* |
837 | | * NB: global, yet called for each vhost that has metrics enabled! |
838 | | */ |
839 | 0 | apr_status_t oidc_metrics_cleanup(server_rec *s) { |
840 | 0 | apr_status_t rv = APR_SUCCESS; |
841 | | |
842 | | /* make sure it gets executed exactly once! */ |
843 | 0 | if ((_oidc_metrics_cache == NULL) || (apr_atomic_read32(&_oidc_metrics_thread_exit) != 0) || |
844 | 0 | (_oidc_metrics_thread == NULL)) |
845 | 0 | return APR_SUCCESS; |
846 | | |
847 | | /* signal the collector thread to exit */ |
848 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 1); |
849 | 0 | apr_thread_join(&rv, _oidc_metrics_thread); |
850 | 0 | if (rv != APR_SUCCESS) |
851 | 0 | oidc_serror(s, "apr_thread_join failed"); |
852 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 0); |
853 | 0 | _oidc_metrics_thread = NULL; |
854 | 0 | _oidc_metrics_thread_pid = 0; |
855 | | |
856 | | /* delete the shared memory segment if we are in the parent process */ |
857 | 0 | if (_oidc_metrics_is_parent == TRUE) |
858 | 0 | apr_shm_destroy(_oidc_metrics_cache); |
859 | 0 | _oidc_metrics_cache = NULL; |
860 | | |
861 | | /* delete the process mutex that guards the local metrics data */ |
862 | 0 | if (oidc_cache_mutex_destroy(s, _oidc_metrics_process_mutex) == FALSE) |
863 | 0 | return APR_EGENERAL; |
864 | 0 | _oidc_metrics_process_mutex = NULL; |
865 | | |
866 | | /* delete the process mutex that guards the global shared memory segment */ |
867 | 0 | if (oidc_cache_mutex_destroy(s, _oidc_metrics_global_mutex) == FALSE) |
868 | 0 | return APR_EGENERAL; |
869 | 0 | _oidc_metrics_global_mutex = NULL; |
870 | |
|
871 | 0 | return APR_SUCCESS; |
872 | 0 | } |
873 | | |
874 | | /* |
875 | | * sampling |
876 | | */ |
877 | | |
878 | | /* |
879 | | * obtain the local metrics hashtable for the current vhost |
880 | | */ |
881 | 0 | static inline apr_hash_t *_oidc_metrics_server_hash(request_rec *r, apr_hash_t *table) { |
882 | 0 | apr_hash_t *server_hash = NULL; |
883 | 0 | const char *name = "_default_"; |
884 | | |
885 | | /* obtain the server name */ |
886 | 0 | if (r->server->server_hostname) |
887 | 0 | name = r->server->server_hostname; |
888 | | |
889 | | /* get the entry to the vhost record, or newly create it */ |
890 | 0 | server_hash = apr_hash_get(table, name, APR_HASH_KEY_STRING); |
891 | 0 | if (server_hash == NULL) { |
892 | | // NB: process pool! |
893 | 0 | server_hash = apr_hash_make(r->server->process->pool); |
894 | 0 | apr_hash_set(table, name, APR_HASH_KEY_STRING, server_hash); |
895 | 0 | } |
896 | |
|
897 | 0 | return server_hash; |
898 | 0 | } |
899 | | |
900 | | /* |
901 | | * retrieve or create a local timing for the specified type |
902 | | */ |
903 | 0 | static inline oidc_metrics_timing_t *_oidc_metrics_timing_get(request_rec *r, unsigned int type) { |
904 | 0 | oidc_metrics_timing_t *result = NULL; |
905 | | /* NB: the lookup key is request-scoped; only the copy the hash retains on insert |
906 | | * below is allocated process-wide, since that pool is never cleared */ |
907 | 0 | const char *key = _oidc_metrics_type_name2key(r->pool, type, NULL); |
908 | 0 | apr_hash_t *server_hash = _oidc_metrics_server_hash(r, _oidc_metrics.timings); |
909 | | /* get the entry to the specified metric */ |
910 | 0 | result = apr_hash_get(server_hash, key, APR_HASH_KEY_STRING); |
911 | 0 | if (result == NULL) { |
912 | | /* allocate the timing structure in the process pool */ |
913 | 0 | result = apr_pcalloc(r->server->process->pool, sizeof(oidc_metrics_timing_t)); |
914 | 0 | apr_hash_set(server_hash, apr_pstrdup(r->server->process->pool, key), APR_HASH_KEY_STRING, result); |
915 | 0 | } |
916 | 0 | return result; |
917 | 0 | } |
918 | | |
919 | | /* |
920 | | * retrieve or create a counter from a hashtable of values |
921 | | */ |
922 | | static inline oidc_metrics_counter_t *_oidc_metrics_counter_value_get(request_rec *r, apr_hash_t *table, |
923 | 0 | const char *value) { |
924 | | /* get the entry to the specified metric */ |
925 | 0 | oidc_metrics_counter_t *result = apr_hash_get(table, value, APR_HASH_KEY_STRING); |
926 | 0 | if (result == NULL) { |
927 | 0 | result = apr_pcalloc(r->server->process->pool, sizeof(oidc_metrics_counter_t)); |
928 | 0 | apr_hash_set(table, apr_pstrdup(r->server->process->pool, value), APR_HASH_KEY_STRING, result); |
929 | 0 | } |
930 | 0 | return result; |
931 | 0 | } |
932 | | |
933 | | /* |
934 | | * retrieve or create a local counter for the specified type and name |
935 | | */ |
936 | 0 | static inline apr_hash_t *_oidc_metrics_counter_get(request_rec *r, unsigned int type, const char *name) { |
937 | 0 | apr_hash_t *result = NULL; |
938 | | /* NB: request-scoped lookup key, process-wide copy only on insert - see above */ |
939 | 0 | const char *key = _oidc_metrics_type_name2key(r->pool, type, name); |
940 | 0 | apr_hash_t *server_hash = _oidc_metrics_server_hash(r, _oidc_metrics.counters); |
941 | | |
942 | | /* get the entry to the specified metric */ |
943 | 0 | result = apr_hash_get(server_hash, key, APR_HASH_KEY_STRING); |
944 | 0 | if (result == NULL) { |
945 | | /* allocate the values hashtable in the process pool */ |
946 | 0 | result = apr_hash_make(r->server->process->pool); |
947 | 0 | apr_hash_set(server_hash, apr_pstrdup(r->server->process->pool, key), APR_HASH_KEY_STRING, result); |
948 | 0 | } |
949 | |
|
950 | 0 | return result; |
951 | 0 | } |
952 | | |
953 | | /* |
954 | | * add/increase a counter metric in the locally cached data |
955 | | */ |
956 | 0 | void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type, const char *name, const char *value) { |
957 | 0 | oidc_metrics_counter_t *counter = NULL; |
958 | | |
959 | | /* lock the local metrics cache hashtable */ |
960 | 0 | oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_process_mutex); |
961 | | |
962 | | /* obtain or create the entry for the specified key */ |
963 | 0 | counter = |
964 | 0 | _oidc_metrics_counter_value_get(r, _oidc_metrics_counter_get(r, type, name), _metrics_value2key(value)); |
965 | | |
966 | | /* performance */ |
967 | 0 | if (counter->count <= 0) { |
968 | | // new counter was created just now or reset earlier |
969 | 0 | counter->count = 1; |
970 | 0 | } else { |
971 | | // increase after checking possible overflow |
972 | 0 | if (_is_overflow(r->server, counter->count, 1)) |
973 | 0 | counter->count = 0; |
974 | 0 | counter->count++; |
975 | 0 | } |
976 | | |
977 | | /* unlock the local metrics cache hashtable */ |
978 | 0 | oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); |
979 | 0 | } |
980 | | |
981 | | /* |
982 | | * zero out a timings entry (count, sum and all buckets) |
983 | | */ |
984 | 0 | static inline void _oidc_metrics_timing_clear(oidc_metrics_timing_t *timing) { |
985 | 0 | timing->count = 0; |
986 | 0 | timing->sum = 0; |
987 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
988 | 0 | timing->buckets[i] = 0; |
989 | 0 | } |
990 | | |
991 | | /* |
992 | | * increment every bucket whose threshold covers elapsed (buckets are ordered; |
993 | | * threshold == 0 marks the open-ended last bucket) |
994 | | */ |
995 | 0 | static inline void _oidc_metrics_timing_buckets_inc(oidc_metrics_timing_t *timing, apr_time_t elapsed) { |
996 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { |
997 | 0 | if ((elapsed < _oidc_metric_buckets[i].threshold) || (_oidc_metric_buckets[i].threshold == 0)) { |
998 | 0 | for (int j = i; j < OIDC_METRICS_BUCKET_NUM; j++) |
999 | 0 | timing->buckets[j]++; |
1000 | 0 | break; |
1001 | 0 | } |
1002 | 0 | } |
1003 | 0 | } |
1004 | | |
1005 | | /* |
1006 | | * add a metrics timing sample to the locally cached data |
1007 | | */ |
1008 | 0 | void oidc_metrics_timing_add(request_rec *r, oidc_metrics_timing_type_t type, apr_time_t elapsed) { |
1009 | 0 | oidc_metrics_timing_t *timing = NULL; |
1010 | |
|
1011 | 0 | if (elapsed < 0) { |
1012 | 0 | oidc_warn(r, "discarding metrics timing [%s.%s]: elapsed (%" APR_TIME_T_FMT ") < 0", |
1013 | 0 | _oidc_metrics_timings_info[type].class_name, _oidc_metrics_timings_info[type].metric_name, |
1014 | 0 | elapsed); |
1015 | 0 | return; |
1016 | 0 | } |
1017 | | |
1018 | | /* lock the local metrics cache hashtable */ |
1019 | 0 | oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_process_mutex); |
1020 | | |
1021 | | /* obtain or create the entry for the specified key */ |
1022 | 0 | timing = _oidc_metrics_timing_get(r, type); |
1023 | | |
1024 | | /* reset on overflow; a freshly-created entry already has count/sum/buckets at zero */ |
1025 | 0 | if ((timing->count > 0) && _is_overflow(r->server, timing->sum, elapsed)) |
1026 | 0 | _oidc_metrics_timing_clear(timing); |
1027 | |
|
1028 | 0 | _oidc_metrics_timing_buckets_inc(timing, elapsed); |
1029 | 0 | timing->sum += elapsed; |
1030 | 0 | timing->count++; |
1031 | | |
1032 | | /* unlock the local metrics cache hashtable */ |
1033 | 0 | oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); |
1034 | 0 | } |
1035 | | |
1036 | | /* |
1037 | | * representation handlers |
1038 | | */ |
1039 | | |
1040 | | /* |
1041 | | * convert in integer counter enum type to its corresponding string name |
1042 | | */ |
1043 | 0 | static inline char *_oidc_metrics_counter_type2s(apr_pool_t *pool, unsigned int type) { |
1044 | 0 | return apr_psprintf(pool, "%s.%s", _oidc_metrics_counters_info[type].class_name, |
1045 | 0 | _oidc_metrics_counters_info[type].metric_name); |
1046 | 0 | } |
1047 | | |
1048 | | /* |
1049 | | * convert in integer timings enum type to its corresponding string name |
1050 | | */ |
1051 | 0 | static inline char *_oidc_metrics_timing_type2s(apr_pool_t *pool, unsigned int type) { |
1052 | 0 | return apr_psprintf(pool, "%s.%s", _oidc_metrics_timings_info[type].class_name, |
1053 | 0 | _oidc_metrics_timings_info[type].metric_name); |
1054 | 0 | } |
1055 | | |
1056 | | /* |
1057 | | * parse a string into a JSON object in the request_rec context |
1058 | | */ |
1059 | 0 | static oidc_json_t *oidc_metrics_json_parse_r(request_rec *r, const char *s_json) { |
1060 | 0 | char *s_err = NULL; |
1061 | 0 | oidc_json_t *json = oidc_metrics_json_load(r->pool, s_json, &s_err); |
1062 | 0 | if (json == NULL) |
1063 | 0 | oidc_error(r, "JSON parsing failed: %s", s_err); |
1064 | 0 | return json; |
1065 | 0 | } |
1066 | | |
1067 | | /* |
1068 | | * JSON with extended descriptions/names |
1069 | | */ |
1070 | 0 | static int oidc_metrics_handle_json(request_rec *r, const char *s_json) { |
1071 | |
|
1072 | 0 | oidc_json_t *json = NULL; |
1073 | 0 | const oidc_json_t *j_server = NULL; |
1074 | 0 | oidc_json_t *j_timings = NULL; |
1075 | 0 | oidc_json_t *j_counters = NULL; |
1076 | 0 | const oidc_json_t *j_timing = NULL; |
1077 | 0 | oidc_json_t *j_counter = NULL; |
1078 | 0 | oidc_json_t *o_json = NULL; |
1079 | 0 | oidc_json_t *o_server = NULL; |
1080 | 0 | oidc_json_t *o_counters = NULL; |
1081 | 0 | oidc_json_t *o_counter = NULL; |
1082 | 0 | oidc_json_t *o_timings = NULL; |
1083 | 0 | oidc_json_t *o_timing = NULL; |
1084 | 0 | const char *s_server = NULL; |
1085 | 0 | unsigned int type = 0; |
1086 | 0 | void *i1 = NULL; |
1087 | 0 | void *i2 = NULL; |
1088 | | |
1089 | | /* parse the metrics string to JSON */ |
1090 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1091 | 0 | if (json == NULL) |
1092 | 0 | goto end; |
1093 | | |
1094 | 0 | o_json = oidc_json_object(); |
1095 | |
|
1096 | 0 | i1 = oidc_json_object_iter(json); |
1097 | 0 | while (i1) { |
1098 | 0 | s_server = oidc_json_object_iter_key(i1); |
1099 | 0 | j_server = oidc_json_object_iter_value(i1); |
1100 | |
|
1101 | 0 | o_server = oidc_json_object(); |
1102 | 0 | oidc_json_object_set_new(o_json, s_server, o_server); |
1103 | |
|
1104 | 0 | j_counters = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
1105 | 0 | o_counters = oidc_json_object(); |
1106 | 0 | oidc_json_object_set_new(o_server, OIDC_METRICS_COUNTERS, o_counters); |
1107 | |
|
1108 | 0 | i2 = oidc_json_object_iter(j_counters); |
1109 | 0 | while (i2) { |
1110 | 0 | type = _oidc_metrics_key2type(oidc_json_object_iter_key(i2)); |
1111 | 0 | j_counter = oidc_json_object_iter_value(i2); |
1112 | 0 | o_counter = oidc_json_object(); |
1113 | 0 | if (oidc_json_is_integer(j_counter)) |
1114 | 0 | oidc_json_object_set(o_counter, "count", j_counter); |
1115 | 0 | else |
1116 | 0 | oidc_json_object_set_new(o_counter, "values", oidc_json_deep_copy(j_counter)); |
1117 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_CLASS_NAME, |
1118 | 0 | oidc_json_string(_oidc_metrics_counters_info[type].class_name)); |
1119 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_METRIC_NAME, |
1120 | 0 | oidc_json_string(_oidc_metrics_counters_info[type].metric_name)); |
1121 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_DESC, |
1122 | 0 | oidc_json_string(_oidc_metrics_counters_info[type].desc)); |
1123 | 0 | oidc_json_object_set_new(o_counters, _oidc_metrics_counter_type2s(r->pool, type), o_counter); |
1124 | 0 | i2 = oidc_json_object_iter_next(j_counters, i2); |
1125 | 0 | } |
1126 | |
|
1127 | 0 | j_timings = oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS); |
1128 | 0 | o_timings = oidc_json_object(); |
1129 | 0 | oidc_json_object_set_new(o_server, OIDC_METRICS_TIMINGS, o_timings); |
1130 | |
|
1131 | 0 | i2 = oidc_json_object_iter(j_timings); |
1132 | 0 | while (i2) { |
1133 | 0 | type = _oidc_metrics_key2type(oidc_json_object_iter_key(i2)); |
1134 | 0 | j_timing = oidc_json_object_iter_value(i2); |
1135 | |
|
1136 | 0 | o_timing = oidc_json_deep_copy(j_timing); |
1137 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_CLASS_NAME, |
1138 | 0 | oidc_json_string(_oidc_metrics_timings_info[type].class_name)); |
1139 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_METRIC_NAME, |
1140 | 0 | oidc_json_string(_oidc_metrics_timings_info[type].metric_name)); |
1141 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_DESC, |
1142 | 0 | oidc_json_string(_oidc_metrics_timings_info[type].desc)); |
1143 | |
|
1144 | 0 | oidc_json_object_set_new(o_timings, _oidc_metrics_timing_type2s(r->pool, type), o_timing); |
1145 | |
|
1146 | 0 | i2 = oidc_json_object_iter_next(j_timings, i2); |
1147 | 0 | } |
1148 | 0 | i1 = oidc_json_object_iter_next(json, i1); |
1149 | 0 | } |
1150 | |
|
1151 | 0 | s_json = oidc_json_encode(r->pool, o_json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
1152 | |
|
1153 | 0 | oidc_json_decref(o_json); |
1154 | 0 | oidc_json_decref(json); |
1155 | |
|
1156 | 0 | end: |
1157 | | |
1158 | | /* return the data to the caller */ |
1159 | 0 | return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_HTTP_CONTENT_TYPE_JSON, OK); |
1160 | 0 | } |
1161 | | |
1162 | | /* |
1163 | | * dump the internal shared memory segment |
1164 | | */ |
1165 | 0 | static int oidc_metrics_handle_internal(request_rec *r, const char *s_json) { |
1166 | 0 | if (s_json == NULL) |
1167 | 0 | return HTTP_NOT_FOUND; |
1168 | 0 | return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_HTTP_CONTENT_TYPE_JSON, OK); |
1169 | 0 | } |
1170 | | |
1171 | 0 | #define OIDC_METRICS_SERVER_PARAM "server_name" |
1172 | 0 | #define OIDC_METRICS_COUNTER_PARAM "counter" |
1173 | 0 | #define OIDC_METRICS_NAME_PARAM "name" |
1174 | 0 | #define OIDC_METRICS_VALUE_PARAM "value" |
1175 | | |
1176 | | /* |
1177 | | * find the counter entry in j_counters whose type matches "class.metric" (s_metric_param) |
1178 | | */ |
1179 | | static oidc_json_t *oidc_metrics_status_find_counter(request_rec *r, oidc_json_t *j_counters, |
1180 | 0 | const char *s_metric_param) { |
1181 | 0 | void *iter = oidc_json_object_iter(j_counters); |
1182 | 0 | while (iter) { |
1183 | 0 | unsigned int type = _oidc_metrics_key2type(oidc_json_object_iter_key(iter)); |
1184 | 0 | if (_oidc_strcmp(_oidc_metrics_counter_type2s(r->pool, type), s_metric_param) == 0) |
1185 | 0 | return oidc_json_object_iter_value(iter); |
1186 | 0 | iter = oidc_json_object_iter_next(j_counters, iter); |
1187 | 0 | } |
1188 | 0 | return NULL; |
1189 | 0 | } |
1190 | | |
1191 | | /* |
1192 | | * extract the requested integer value out of a counter entry: |
1193 | | * - integer entry: the counter itself |
1194 | | * - object entry with value-only selector: j_counter[value] |
1195 | | * - object entry with name+value selectors: j_counter[name][value] |
1196 | | */ |
1197 | | static oidc_json_t *oidc_metrics_status_select_value(oidc_json_t *j_counter, const char *s_name_param, |
1198 | 0 | const char *s_value_param) { |
1199 | 0 | const oidc_json_t *j_values = NULL; |
1200 | 0 | if (oidc_json_is_integer(j_counter)) |
1201 | 0 | return j_counter; |
1202 | 0 | if (s_value_param == NULL) |
1203 | 0 | return NULL; |
1204 | 0 | if (s_name_param == NULL) |
1205 | 0 | return oidc_json_object_get(j_counter, s_value_param); |
1206 | 0 | j_values = oidc_json_object_get(j_counter, s_name_param); |
1207 | 0 | if (j_values == NULL) |
1208 | 0 | return NULL; |
1209 | 0 | return oidc_json_object_get(j_values, s_value_param); |
1210 | 0 | } |
1211 | | |
1212 | | /* |
1213 | | * return status updates |
1214 | | */ |
1215 | 0 | static int oidc_metrics_handle_status(request_rec *r, const char *s_json) { |
1216 | 0 | const char *msg = "OK\n"; |
1217 | 0 | char *s_metric_param = NULL; |
1218 | 0 | char *s_server_param = NULL; |
1219 | 0 | char *s_name_param = NULL; |
1220 | 0 | char *s_value_param = NULL; |
1221 | 0 | oidc_json_t *json = NULL; |
1222 | 0 | const oidc_json_t *j_server = NULL; |
1223 | 0 | oidc_json_t *j_counters = NULL; |
1224 | 0 | oidc_json_t *j_counter = NULL; |
1225 | 0 | const oidc_json_t *j_value = NULL; |
1226 | |
|
1227 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_SERVER_PARAM, &s_server_param); |
1228 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_COUNTER_PARAM, &s_metric_param); |
1229 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_NAME_PARAM, &s_name_param); |
1230 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_VALUE_PARAM, &s_value_param); |
1231 | |
|
1232 | 0 | if (s_server_param == NULL) |
1233 | 0 | s_server_param = "localhost"; |
1234 | |
|
1235 | 0 | if (s_metric_param == NULL) |
1236 | 0 | goto end; |
1237 | | |
1238 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1239 | 0 | if (json == NULL) |
1240 | 0 | goto end; |
1241 | | |
1242 | 0 | j_server = oidc_json_object_get(json, s_server_param); |
1243 | 0 | if (j_server == NULL) |
1244 | 0 | goto end; |
1245 | | |
1246 | 0 | j_counters = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
1247 | 0 | if (j_counters == NULL) |
1248 | 0 | goto end; |
1249 | | |
1250 | 0 | j_counter = oidc_metrics_status_find_counter(r, j_counters, s_metric_param); |
1251 | 0 | if (j_counter == NULL) |
1252 | 0 | goto end; |
1253 | | |
1254 | 0 | j_value = oidc_metrics_status_select_value(j_counter, s_name_param, s_value_param); |
1255 | 0 | if (j_value) |
1256 | 0 | msg = apr_psprintf(r->pool, "OK: %s\n", _json_int2str(r->pool, oidc_json_integer_value(j_value))); |
1257 | |
|
1258 | 0 | end: |
1259 | |
|
1260 | 0 | if (json) |
1261 | 0 | oidc_json_decref(json); |
1262 | |
|
1263 | 0 | return oidc_util_http_send(r, msg, _oidc_strlen(msg), "text/plain", OK); |
1264 | 0 | } |
1265 | | |
1266 | | /* |
1267 | | * return the Prometheus label name for a bucket |
1268 | | */ |
1269 | 0 | static const char *oidc_metrics_prometheus_bucket_label(const char *json_name) { |
1270 | 0 | const char *name = NULL; |
1271 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { |
1272 | 0 | if (_oidc_strcmp(_oidc_metric_buckets[i].name, json_name) == 0) { |
1273 | 0 | name = _oidc_metric_buckets[i].label; |
1274 | 0 | break; |
1275 | 0 | } |
1276 | 0 | } |
1277 | 0 | return name; |
1278 | 0 | } |
1279 | | |
1280 | 0 | #define OIDC_METRICS_PROMETHEUS_PREFIX "oidc" |
1281 | | |
1282 | | /* |
1283 | | * normalize a metric name to something that Prometheus accepts |
1284 | | */ |
1285 | 0 | static const char *oidc_metric_prometheus_normalize_name(apr_pool_t *pool, const char *name) { |
1286 | 0 | char *label = apr_psprintf(pool, "%s", name); |
1287 | 0 | for (size_t i = 0; i < _oidc_strlen(label); i++) |
1288 | 0 | if (apr_isalnum(label[i]) == 0) |
1289 | 0 | label[i] = '_'; |
1290 | 0 | return apr_psprintf(pool, "%s_%s", OIDC_METRICS_PROMETHEUS_PREFIX, label); |
1291 | 0 | } |
1292 | | |
1293 | 0 | #define OIDC_METRICS_PROMETHEUS_CONTENT_TYPE "text/plain; version=0.0.4" |
1294 | | |
1295 | 0 | #define OIDC_METRICS_PROMETHEUS_SERVER "server_name" |
1296 | 0 | #define OIDC_METRICS_PROMETHEUS_BUCKET "bucket" |
1297 | 0 | #define OIDC_METRICS_PROMETHEUS_VALUE "value" |
1298 | 0 | #define OIDC_METRICS_PROMETHEUS_NAME "name" |
1299 | | |
1300 | | // loop context for Prometheus output |
1301 | | typedef struct oidc_metric_prometheus_callback_ctx_t { |
1302 | | char *s_result; |
1303 | | apr_pool_t *pool; |
1304 | | } oidc_metric_prometheus_callback_ctx_t; |
1305 | | |
1306 | | /* |
1307 | | * append per-(name, value) label-pair lines for a single key under a server's counter |
1308 | | */ |
1309 | | static char *oidc_metrics_prometheus_counter_named(apr_pool_t *pool, char *s_text, const char *s_start, |
1310 | 0 | const char *s_key, oidc_json_t *j_value) { |
1311 | 0 | void *iter = oidc_json_object_iter(j_value); |
1312 | 0 | while (iter) { |
1313 | 0 | const char *s_value = oidc_json_object_iter_key(iter); |
1314 | 0 | s_text = apr_psprintf(pool, "%s%s,%s=\"%s\",%s=\"%s\"} %s\n", s_text, s_start, |
1315 | 0 | OIDC_METRICS_PROMETHEUS_NAME, s_key, OIDC_METRICS_PROMETHEUS_VALUE, s_value, |
1316 | 0 | _json_int2str(pool, oidc_json_integer_value(oidc_json_object_iter_value(iter)))); |
1317 | 0 | iter = oidc_json_object_iter_next(j_value, iter); |
1318 | 0 | } |
1319 | 0 | return s_text; |
1320 | 0 | } |
1321 | | |
1322 | | /* |
1323 | | * append per-key lines for one server's counter dict; integer entries are value-labeled, sub-dict |
1324 | | * entries are delegated to oidc_metrics_prometheus_counter_named for name+value labeling |
1325 | | */ |
1326 | | static char *oidc_metrics_prometheus_counter_keyed(apr_pool_t *pool, char *s_text, const char *s_start, |
1327 | 0 | oidc_json_t *j_counter) { |
1328 | 0 | void *iter = oidc_json_object_iter(j_counter); |
1329 | 0 | while (iter) { |
1330 | 0 | const char *s_key = oidc_json_object_iter_key(iter); |
1331 | 0 | oidc_json_t *j_value = oidc_json_object_iter_value(iter); |
1332 | 0 | if (oidc_json_is_integer(j_value)) |
1333 | 0 | s_text = |
1334 | 0 | apr_psprintf(pool, "%s%s,%s=\"%s\"} %s\n", s_text, s_start, OIDC_METRICS_PROMETHEUS_VALUE, |
1335 | 0 | s_key, _json_int2str(pool, oidc_json_integer_value(j_value))); |
1336 | 0 | else |
1337 | 0 | s_text = oidc_metrics_prometheus_counter_named(pool, s_text, s_start, s_key, j_value); |
1338 | 0 | iter = oidc_json_object_iter_next(j_counter, iter); |
1339 | 0 | } |
1340 | 0 | return s_text; |
1341 | 0 | } |
1342 | | |
1343 | | /* |
1344 | | * loop function for converting counter metrics to Prometheus output |
1345 | | */ |
1346 | | static int oidc_metrics_prometheus_counters(oidc_metric_prometheus_callback_ctx_t *ctx, const char *key, |
1347 | 0 | oidc_json_t *value) { |
1348 | 0 | oidc_json_t *o_counter = value; |
1349 | 0 | unsigned int type = _oidc_metrics_key2type(key); |
1350 | 0 | const char *s_label = |
1351 | 0 | oidc_metric_prometheus_normalize_name(ctx->pool, _oidc_metrics_counter_type2s(ctx->pool, type)); |
1352 | 0 | char *s_text = |
1353 | 0 | apr_psprintf(ctx->pool, "# HELP %s The number of %s.\n", s_label, _oidc_metrics_counters_info[type].desc); |
1354 | 0 | s_text = apr_psprintf(ctx->pool, "%s# TYPE %s counter\n", s_text, s_label); |
1355 | |
|
1356 | 0 | void *iter = oidc_json_object_iter(o_counter); |
1357 | 0 | while (iter) { |
1358 | 0 | const char *s_server = oidc_json_object_iter_key(iter); |
1359 | 0 | oidc_json_t *j_counter = oidc_json_object_iter_value(iter); |
1360 | 0 | const char *s_start = |
1361 | 0 | apr_psprintf(ctx->pool, "%s{%s=\"%s\"", s_label, OIDC_METRICS_PROMETHEUS_SERVER, s_server); |
1362 | 0 | if (oidc_json_is_integer(j_counter)) |
1363 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s} %s\n", s_text, s_start, |
1364 | 0 | _json_int2str(ctx->pool, oidc_json_integer_value(j_counter))); |
1365 | 0 | else |
1366 | 0 | s_text = oidc_metrics_prometheus_counter_keyed(ctx->pool, s_text, s_start, j_counter); |
1367 | 0 | iter = oidc_json_object_iter_next(o_counter, iter); |
1368 | 0 | } |
1369 | 0 | ctx->s_result = apr_pstrcat(ctx->pool, ctx->s_result, s_text, "\n", NULL); |
1370 | 0 | oidc_json_decref(o_counter); |
1371 | 0 | return 1; |
1372 | 0 | } |
1373 | | |
1374 | | /* |
1375 | | * loop function for converting timing metrics to Prometheus output |
1376 | | */ |
1377 | | |
1378 | | static int oidc_metrics_prometheus_timings(oidc_metric_prometheus_callback_ctx_t *ctx, const char *key, |
1379 | 0 | oidc_json_t *value) { |
1380 | 0 | const char *s_server = NULL; |
1381 | 0 | const char *s_key = NULL; |
1382 | 0 | const char *s_bucket = NULL; |
1383 | 0 | oidc_json_t *j_timing = NULL; |
1384 | 0 | const oidc_json_t *j_member = NULL; |
1385 | 0 | oidc_json_t *o_timer = value; |
1386 | 0 | unsigned int type = _oidc_metrics_key2type(key); |
1387 | 0 | const char *s_label = |
1388 | 0 | oidc_metric_prometheus_normalize_name(ctx->pool, _oidc_metrics_timing_type2s(ctx->pool, type)); |
1389 | 0 | char *s_text = |
1390 | 0 | apr_psprintf(ctx->pool, "# HELP %s A histogram of %s.\n", s_label, _oidc_metrics_timings_info[type].desc); |
1391 | 0 | s_text = apr_psprintf(ctx->pool, "%s# TYPE %s histogram\n", s_text, s_label); |
1392 | |
|
1393 | 0 | void *iter1 = oidc_json_object_iter(o_timer); |
1394 | 0 | while (iter1) { |
1395 | 0 | s_server = oidc_json_object_iter_key(iter1); |
1396 | 0 | j_timing = oidc_json_object_iter_value(iter1); |
1397 | 0 | void *iter3 = oidc_json_object_iter(j_timing); |
1398 | 0 | while (iter3) { |
1399 | 0 | s_key = oidc_json_object_iter_key(iter3); |
1400 | 0 | j_member = oidc_json_object_iter_value(iter3); |
1401 | 0 | s_bucket = oidc_metrics_prometheus_bucket_label(s_key); |
1402 | 0 | if (s_bucket) |
1403 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s_%s{%s,", s_text, s_label, |
1404 | 0 | OIDC_METRICS_PROMETHEUS_BUCKET, s_bucket); |
1405 | 0 | else |
1406 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s_%s{", s_text, s_label, s_key); |
1407 | |
|
1408 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s=\"%s\"} %s\n", s_text, OIDC_METRICS_PROMETHEUS_SERVER, |
1409 | 0 | s_server, _json_int2str(ctx->pool, oidc_json_integer_value(j_member))); |
1410 | 0 | iter3 = oidc_json_object_iter_next(j_timing, iter3); |
1411 | 0 | } |
1412 | 0 | iter1 = oidc_json_object_iter_next(o_timer, iter1); |
1413 | 0 | } |
1414 | 0 | ctx->s_result = apr_pstrcat(ctx->pool, ctx->s_result, s_text, "\n", NULL); |
1415 | 0 | oidc_json_decref(o_timer); |
1416 | 0 | return 1; |
1417 | 0 | } |
1418 | | |
1419 | | /* |
1420 | | * take a list of metrics from a server indexed list and add it to a type indexed list |
1421 | | */ |
1422 | 0 | static void oidc_metrics_prometheus_convert(apr_hash_t *hash, const char *server, oidc_json_t *list) { |
1423 | 0 | const char *type = NULL; |
1424 | 0 | oidc_json_t *src = NULL; |
1425 | 0 | oidc_json_t *dst = NULL; |
1426 | 0 | void *iter = oidc_json_object_iter(list); |
1427 | 0 | while (iter) { |
1428 | 0 | type = oidc_json_object_iter_key(iter); |
1429 | 0 | src = oidc_json_object_iter_value(iter); |
1430 | 0 | dst = (oidc_json_t *)apr_hash_get(hash, type, APR_HASH_KEY_STRING); |
1431 | 0 | if (dst) { |
1432 | 0 | oidc_json_object_set(dst, server, src); |
1433 | 0 | } else { |
1434 | 0 | dst = oidc_json_object(); |
1435 | 0 | oidc_json_object_set(dst, server, src); |
1436 | 0 | apr_hash_set(hash, type, APR_HASH_KEY_STRING, dst); |
1437 | 0 | } |
1438 | 0 | iter = oidc_json_object_iter_next(list, iter); |
1439 | 0 | } |
1440 | 0 | } |
1441 | | |
1442 | | /* |
1443 | | * generate output in Prometheus formatting |
1444 | | */ |
1445 | 0 | static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { |
1446 | 0 | oidc_json_t *json = NULL; |
1447 | 0 | const oidc_json_t *j_server = NULL; |
1448 | 0 | const char *s_server = NULL; |
1449 | 0 | apr_hash_t *t_counters = apr_hash_make(r->pool); |
1450 | 0 | apr_hash_t *t_timings = apr_hash_make(r->pool); |
1451 | 0 | apr_hash_index_t *hi = NULL; |
1452 | 0 | const char *name = NULL; |
1453 | 0 | void *value = NULL; |
1454 | |
|
1455 | 0 | oidc_metric_prometheus_callback_ctx_t ctx = {"", r->pool}; |
1456 | 0 | void *iter = NULL; |
1457 | | |
1458 | | /* parse the metrics string to JSON */ |
1459 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1460 | 0 | if (json == NULL) |
1461 | 0 | return OK; |
1462 | | |
1463 | 0 | iter = oidc_json_object_iter(json); |
1464 | 0 | while (iter) { |
1465 | 0 | s_server = oidc_json_object_iter_key(iter); |
1466 | 0 | j_server = oidc_json_object_iter_value(iter); |
1467 | 0 | oidc_metrics_prometheus_convert(t_counters, s_server, |
1468 | 0 | oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS)); |
1469 | 0 | oidc_metrics_prometheus_convert(t_timings, s_server, |
1470 | 0 | oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS)); |
1471 | 0 | iter = oidc_json_object_iter_next(json, iter); |
1472 | 0 | } |
1473 | |
|
1474 | 0 | for (hi = apr_hash_first(r->pool, t_counters); hi; hi = apr_hash_next(hi)) { |
1475 | 0 | apr_hash_this(hi, (const void **)&name, NULL, &value); |
1476 | 0 | oidc_metrics_prometheus_counters(&ctx, name, value); |
1477 | 0 | } |
1478 | |
|
1479 | 0 | for (hi = apr_hash_first(r->pool, t_timings); hi; hi = apr_hash_next(hi)) { |
1480 | 0 | apr_hash_this(hi, (const void **)&name, NULL, &value); |
1481 | 0 | oidc_metrics_prometheus_timings(&ctx, name, value); |
1482 | 0 | } |
1483 | |
|
1484 | 0 | oidc_json_decref(json); |
1485 | |
|
1486 | 0 | return oidc_util_http_send(r, ctx.s_result, _oidc_strlen(ctx.s_result), OIDC_METRICS_PROMETHEUS_CONTENT_TYPE, |
1487 | 0 | OK); |
1488 | 0 | } |
1489 | | |
1490 | | /* |
1491 | | * definitions for handler callbacks |
1492 | | */ |
1493 | | |
1494 | | typedef int (*oidc_metrics_handler_function_t)(request_rec *, const char *); |
1495 | | |
1496 | | // holder for output function callback context |
1497 | | typedef struct oidc_metrics_handler_t { |
1498 | | const char *format; |
1499 | | oidc_metrics_handler_function_t callback; |
1500 | | int reset; |
1501 | | } oidc_metrics_content_handler_t; |
1502 | | |
1503 | | // output handlers |
1504 | | const oidc_metrics_content_handler_t _oidc_metrics_handlers[] = { |
1505 | | // first is default |
1506 | | {"prometheus", oidc_metrics_handle_prometheus, 0}, |
1507 | | {"json", oidc_metrics_handle_json, 1}, |
1508 | | {"internal", oidc_metrics_handle_internal, 0}, |
1509 | | {"status", oidc_metrics_handle_status, 0}, |
1510 | | }; |
1511 | | |
1512 | 0 | #define OIDC_CONTENT_HANDLER_MAX ((int)(sizeof(_oidc_metrics_handlers) / sizeof(oidc_metrics_content_handler_t))) |
1513 | | |
1514 | 0 | #define OIDC_METRICS_RESET_PARAM "reset" |
1515 | | |
1516 | | /* |
1517 | | * see if we are going to reset the cache after this |
1518 | | */ |
1519 | 0 | static int oidc_metric_reset(request_rec *r, int dvalue) { |
1520 | 0 | char *s_reset = NULL; |
1521 | 0 | int value = 0; |
1522 | |
|
1523 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_RESET_PARAM, &s_reset); |
1524 | |
|
1525 | 0 | if (s_reset == NULL) |
1526 | 0 | return dvalue; |
1527 | | |
1528 | 0 | if (_oidc_strnatcasecmp(s_reset, "true") == 0) |
1529 | 0 | value = 1; |
1530 | 0 | else if (_oidc_strnatcasecmp(s_reset, "false") == 0) |
1531 | 0 | value = 0; |
1532 | |
|
1533 | 0 | return value; |
1534 | 0 | } |
1535 | | |
1536 | 0 | #define OIDC_METRICS_FORMAT_PARAM "format" |
1537 | | |
1538 | | /* |
1539 | | * find the format handler |
1540 | | */ |
1541 | 0 | const oidc_metrics_content_handler_t *oidc_metrics_find_handler(request_rec *r) { |
1542 | 0 | const oidc_metrics_content_handler_t *handler = NULL; |
1543 | 0 | char *s_format = NULL; |
1544 | | |
1545 | | /* get the specified format */ |
1546 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_FORMAT_PARAM, &s_format); |
1547 | |
|
1548 | 0 | if (s_format == NULL) |
1549 | 0 | return &_oidc_metrics_handlers[0]; |
1550 | | |
1551 | 0 | for (int i = 0; i < OIDC_CONTENT_HANDLER_MAX; i++) { |
1552 | 0 | if (_oidc_strcmp(s_format, _oidc_metrics_handlers[i].format) == 0) { |
1553 | 0 | handler = &_oidc_metrics_handlers[i]; |
1554 | 0 | break; |
1555 | 0 | } |
1556 | 0 | } |
1557 | |
|
1558 | 0 | if (handler == NULL) |
1559 | 0 | oidc_warn(r, "could not find a metrics handler for format: %s", s_format); |
1560 | |
|
1561 | 0 | return handler; |
1562 | 0 | } |
1563 | | |
1564 | | /* |
1565 | | * return the metrics to the caller and flush the storage |
1566 | | */ |
1567 | 0 | int oidc_metrics_handle_request(request_rec *r) { |
1568 | 0 | const char *s_json = NULL; |
1569 | 0 | const oidc_metrics_content_handler_t *handler = NULL; |
1570 | | |
1571 | | /* get the content handler for the format */ |
1572 | 0 | handler = oidc_metrics_find_handler(r); |
1573 | 0 | if (handler == NULL) |
1574 | 0 | return HTTP_NOT_FOUND; |
1575 | | |
1576 | | /* lock the global shared memory */ |
1577 | 0 | oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_global_mutex); |
1578 | | |
1579 | | /* retrieve the JSON formatted metrics as a string; NB: on the request pool, since |
1580 | | * it is handed to the content handler below, after the mutex has been released */ |
1581 | 0 | s_json = _oidc_metrics_storage_get(r->server, r->pool); |
1582 | | |
1583 | | /* now that the metrics have been consumed, clear the shared memory segment */ |
1584 | 0 | if (oidc_metric_reset(r, handler->reset)) |
1585 | 0 | oidc_metrics_storage_reset(r->server, r->pool); |
1586 | | |
1587 | | /* unlock the global shared memory */ |
1588 | 0 | oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_global_mutex); |
1589 | | |
1590 | | /* handle the specified format */ |
1591 | 0 | return handler->callback(r, s_json); |
1592 | 0 | } |