/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 "cfg/cfg_int.h" |
53 | | #include "metrics.h" |
54 | | #include <limits.h> |
55 | | #include <apr_atomic.h> |
56 | | #include <apr_shm.h> |
57 | | #include <apr_lib.h> |
58 | | |
59 | | // NB: formatting matters for docs script from here until clang-format on |
60 | | |
61 | | // KEEP THIS: start-of-classes |
62 | | |
63 | | #define OM_CLASS_AUTH_TYPE "authtype" // Request counter, overall and per AuthType: openid-connect, oauth20 and auth-openidc. |
64 | | #define OM_CLASS_AUTHN "authn" // Authentication request creation and response processing. |
65 | | #define OM_CLASS_AUTHZ "authz" // Authorization errors per OIDCUnAutzAction (per Require statement, not overall). |
66 | | #define OM_CLASS_REQUIRE_CLAIM "require.claim" // Match/failure count of Require claim directives (per Require statement, not overall). |
67 | | #define OM_CLASS_CLAIM "claim" // Claims per value |
68 | | #define OM_CLASS_PROVIDER "provider" // Requests to the provider [token, userinfo, metadata] endpoints. |
69 | | #define OM_CLASS_SESSION "session" // Existing session processing. |
70 | | #define OM_CLASS_CACHE "cache" // Cache read/write timings and errors. |
71 | | #define OM_CLASS_LOGOUT "logout" // Front- and back-channel logout processing. |
72 | | #define OM_CLASS_REDIRECT_URI "redirect_uri" // Requests to the Redirect URI, per type. |
73 | | #define OM_CLASS_CONTENT "content" // Requests to the content handler, per type of request: info, metrics, jwks, etc. |
74 | | |
75 | | // KEEP THIS: end-of-classes |
76 | | |
77 | | // NB: order must match the oidc_metrics_timing_type_t enum type in metrics.h |
78 | | |
79 | | const oidc_metrics_timing_info_t _oidc_metrics_timings_info[] = { |
80 | | |
81 | | // KEEP THIS: start-of-timers |
82 | | |
83 | | { OM_CLASS_AUTH_TYPE, "handler", "the overall authn+authz processing time" }, |
84 | | |
85 | | { OM_CLASS_AUTHN, "request", "authentication requests" }, |
86 | | { OM_CLASS_AUTHN, "response", "authentication responses" }, |
87 | | |
88 | | { OM_CLASS_SESSION, "valid", "successfully validated existing sessions" }, |
89 | | |
90 | | { OM_CLASS_PROVIDER, "metadata", "provider discovery document requests" }, |
91 | | { OM_CLASS_PROVIDER, "token", "provider token requests" }, |
92 | | { OM_CLASS_PROVIDER, "refresh", "provider refresh token requests" }, |
93 | | { OM_CLASS_PROVIDER, "userinfo", "provider userinfo requests" }, |
94 | | { OM_CLASS_PROVIDER, "jwks", "provider JWKs document requests" }, |
95 | | { OM_CLASS_PROVIDER, "par", "provider pushed authorization requests" }, |
96 | | |
97 | | { OM_CLASS_CACHE, "read", "cache read requests" }, |
98 | | { OM_CLASS_CACHE, "write", "cache write requests" }, |
99 | | |
100 | | // KEEP THIS: end-of-timers |
101 | | |
102 | | }; |
103 | | |
104 | | // NB: order must match the oidc_metrics_counter_type_t enum type in metrics.h |
105 | | |
106 | | const oidc_metrics_counter_info_t _oidc_metrics_counters_info[] = { |
107 | | #define OIDC_METRICS_COUNTER_INFO(id, class, name, desc) {class, name, desc}, |
108 | | OIDC_METRICS_COUNTERS_LIST(OIDC_METRICS_COUNTER_INFO) |
109 | | #undef OIDC_METRICS_COUNTER_INFO |
110 | | }; |
111 | | |
112 | | // clang-format on |
113 | | |
114 | | /* one entry per oidc_metrics_counter_type_t value */ |
115 | | OIDC_STATIC_ASSERT(sizeof(_oidc_metrics_counters_info) / sizeof(oidc_metrics_counter_info_t) == OM_NUMBER_OF_COUNTERS, |
116 | | counters_info_matches_enum); |
117 | | /* one entry per oidc_metrics_timing_type_t value */ |
118 | | OIDC_STATIC_ASSERT(sizeof(_oidc_metrics_timings_info) / sizeof(oidc_metrics_timing_info_t) == OM_NUMBER_OF_TIMINGS, |
119 | | timings_info_matches_enum); |
120 | | |
121 | | typedef struct oidc_metrics_t { |
122 | | apr_pool_t *pool; |
123 | | apr_hash_t *counters; |
124 | | apr_hash_t *timings; |
125 | | } oidc_metrics_t; |
126 | | |
127 | | // pointer to the shared memory segment that holds the JSON metrics data |
128 | | static apr_shm_t *_oidc_metrics_cache = NULL; |
129 | | // flag to record if we are a parent process or a child process |
130 | | static apr_byte_t _oidc_metrics_is_parent = FALSE; |
131 | | /* Atomic stop flag shared by the flush, cleanup, and child-init paths. */ |
132 | | static volatile apr_uint32_t _oidc_metrics_thread_exit = 0; |
133 | | // mutex to protect the shared memory storage |
134 | | static oidc_cache_mutex_t *_oidc_metrics_global_mutex = NULL; |
135 | | // pointer to the thread that periodically writes the locally gathered metrics to shared memory |
136 | | static apr_thread_t *_oidc_metrics_thread = NULL; |
137 | | // pid that owns _oidc_metrics_thread; used to tell a post-fork stale handle from a same-process double-init |
138 | | static pid_t _oidc_metrics_thread_pid = 0; |
139 | | // local in-memory cached metrics |
140 | | static oidc_metrics_t _oidc_metrics = {NULL, NULL, NULL}; |
141 | | // mutex to protect the local metrics hash table |
142 | | static oidc_cache_mutex_t *_oidc_metrics_process_mutex = NULL; |
143 | | |
144 | | // default shared memory write interval in seconds |
145 | 0 | #define OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT 5000 |
146 | | |
147 | | // maximum length of the string representation of the global JSON metrics data in shared memory |
148 | | // 1024 sample size (compact, long keys, large json_int values, no description), timing + counter |
149 | | // 256 number of individual metrics collected |
150 | | // 4 number of vhosts supported |
151 | 0 | #define OIDC_METRICS_CACHE_JSON_MAX_DEFAULT (1024 * 256 * 4) |
152 | | |
153 | | // NB: "label" is the millisecond le= label of the legacy (deprecated) Prometheus families, |
154 | | // "label_seconds" the seconds le= label of the idiomatic _seconds families |
155 | | typedef struct oidc_metrics_bucket_t { |
156 | | const char *name; |
157 | | const char *label; |
158 | | const char *label_seconds; |
159 | | apr_time_t threshold; |
160 | | } oidc_metrics_bucket_t; |
161 | | |
162 | | // clang-format off |
163 | | |
164 | | static oidc_metrics_bucket_t _oidc_metric_buckets[] = { |
165 | | { "le01", "le=\"0.1\"", "le=\"0.0001\"", 100 }, |
166 | | { "le05", "le=\"0.5\"", "le=\"0.0005\"", 500 }, |
167 | | { "le1", "le=\"1\"", "le=\"0.001\"", apr_time_from_msec(1) }, |
168 | | { "le5", "le=\"5\"", "le=\"0.005\"", apr_time_from_msec(5) }, |
169 | | { "le10", "le=\"10\"", "le=\"0.01\"", apr_time_from_msec(10) }, |
170 | | { "le50", "le=\"50\"", "le=\"0.05\"", apr_time_from_msec(50) }, |
171 | | { "le100", "le=\"100\"", "le=\"0.1\"", apr_time_from_msec(100) }, |
172 | | { "le500", "le=\"500\"", "le=\"0.5\"", apr_time_from_msec(500) }, |
173 | | { "le1000", "le=\"1000\"", "le=\"1\"", apr_time_from_msec(1000) }, |
174 | | { "le5000", "le=\"5000\"", "le=\"5\"", apr_time_from_msec(5000) }, |
175 | | { "le10000", "le=\"10000\"", "le=\"10\"", apr_time_from_msec(10000) }, |
176 | | { "le30000", "le=\"30000\"", "le=\"30\"", apr_time_from_msec(30000) }, |
177 | | { "inf", "le=\"+Inf\"", "le=\"+Inf\"", 0 } |
178 | | }; |
179 | | |
180 | | // clang-format on |
181 | | |
182 | 0 | #define OIDC_METRICS_BUCKET_NUM ((int)(sizeof(_oidc_metric_buckets) / sizeof(oidc_metrics_bucket_t))) |
183 | | |
184 | | // NB: matters for Prometheus formatting |
185 | 0 | #define OIDC_METRICS_SUM "sum" |
186 | 0 | #define OIDC_METRICS_COUNT "count" |
187 | | |
188 | | #define OIDC_METRICS_SPECS "specs" |
189 | | |
190 | 0 | #define OIDC_METRICS_JSON_CLASS_NAME "class" |
191 | 0 | #define OIDC_METRICS_JSON_METRIC_NAME "name" |
192 | 0 | #define OIDC_METRICS_JSON_DESC "desc" |
193 | | |
194 | 0 | #define OIDC_METRICS_TIMINGS "timings" |
195 | 0 | #define OIDC_METRICS_COUNTERS "counters" |
196 | | |
197 | | /* |
198 | | * convert a Jansson number to a string: apr_psprintf does not understand OIDC_JSON_INT_FORMAT |
199 | | * ("%lld"), APR spells a 64-bit integer as APR_INT64_T_FMT, and oidc_json_int_t is at most 64 |
200 | | * bits wide |
201 | | */ |
202 | 0 | static inline char *_json_int2str(apr_pool_t *pool, oidc_json_int_t n) { |
203 | 0 | return apr_psprintf(pool, "%" APR_INT64_T_FMT, (apr_int64_t)n); |
204 | 0 | } |
205 | | |
206 | 0 | #define OIDC_METRICS_INT_MAX OIDC_JSON_INT_MAX |
207 | | |
208 | | /* |
209 | | * check Jansson specific integer/long number overrun |
210 | | */ |
211 | 0 | static inline int _is_overflow(server_rec *s, oidc_json_int_t cur, oidc_json_int_t add) { |
212 | 0 | if (add > (OIDC_METRICS_INT_MAX - cur)) { |
213 | 0 | oidc_swarn(s, |
214 | 0 | "reset metrics since the size (%s) of the integer value would be larger than the " |
215 | 0 | "JSON/libjansson maximum " |
216 | 0 | "(%s)", |
217 | 0 | _json_int2str(s->process->pool, add), _json_int2str(s->process->pool, OIDC_METRICS_INT_MAX)); |
218 | 0 | return 1; |
219 | 0 | } |
220 | 0 | return 0; |
221 | 0 | } |
222 | | |
223 | | // single counter container |
224 | | typedef struct oidc_metrics_counter_t { |
225 | | oidc_json_int_t count; |
226 | | } oidc_metrics_counter_t; |
227 | | |
228 | | // single timing stats container |
229 | | typedef struct oidc_metrics_timing_t { |
230 | | oidc_json_int_t buckets[OIDC_METRICS_BUCKET_NUM]; |
231 | | apr_time_t sum; |
232 | | oidc_json_int_t count; |
233 | | } oidc_metrics_timing_t; |
234 | | |
235 | | /* |
236 | | * forget the local collection pool when it goes away: it is a child of the process pool, and at exit |
237 | | * apr_pool_destroy() of the process pool destroys it before pconf (an older sibling) and so before the pconf |
238 | | * cleanup that runs oidc_metrics_cleanup(), which must then not destroy it a second time |
239 | | */ |
240 | 0 | static apr_status_t oidc_metrics_local_pool_cleanup(void *data) { |
241 | 0 | _oidc_metrics.pool = NULL; |
242 | 0 | _oidc_metrics.counters = NULL; |
243 | 0 | _oidc_metrics.timings = NULL; |
244 | 0 | return APR_SUCCESS; |
245 | 0 | } |
246 | | |
247 | | /* |
248 | | * Reinitialize the per-process collection tables in a dedicated child pool. |
249 | | * Clearing this pool after a flush reclaims all nested hashes, keys and values |
250 | | * in one operation; the process mutex must be held once request threads exist. |
251 | | */ |
252 | 0 | static apr_byte_t oidc_metrics_local_reset(server_rec *s) { |
253 | 0 | apr_pool_t *pool = _oidc_metrics.pool; |
254 | |
|
255 | 0 | if (pool == NULL) { |
256 | 0 | if (apr_pool_create(&pool, s->process->pool) != APR_SUCCESS) { |
257 | 0 | oidc_serror(s, "apr_pool_create failed: cannot initialize local metrics"); |
258 | 0 | return FALSE; |
259 | 0 | } |
260 | 0 | } else { |
261 | | /* NB: this runs (and so consumes) oidc_metrics_local_pool_cleanup as well, hence the re-registration */ |
262 | 0 | apr_pool_clear(pool); |
263 | 0 | } |
264 | 0 | apr_pool_cleanup_register(pool, NULL, oidc_metrics_local_pool_cleanup, apr_pool_cleanup_null); |
265 | |
|
266 | 0 | _oidc_metrics.pool = pool; |
267 | 0 | _oidc_metrics.counters = apr_hash_make(pool); |
268 | 0 | _oidc_metrics.timings = apr_hash_make(pool); |
269 | 0 | return TRUE; |
270 | 0 | } |
271 | | |
272 | | // context holder for parsing valid classnames |
273 | | typedef struct oidc_metrics_add_classname_ctx_t { |
274 | | apr_pool_t *pool; |
275 | | char **valid_names; |
276 | | } oidc_metrics_add_classname_ctx_t; |
277 | | |
278 | | /* |
279 | | * loop function for parsing valid classnames |
280 | | */ |
281 | 0 | static int oidc_metrics_add_classnames(void *rec, const char *key, const char *value) { |
282 | 0 | oidc_metrics_add_classname_ctx_t *ctx = (oidc_metrics_add_classname_ctx_t *)rec; |
283 | 0 | *ctx->valid_names = apr_psprintf(ctx->pool, "%s%s%s", *ctx->valid_names ? *ctx->valid_names : "", |
284 | 0 | *ctx->valid_names ? " | " : "", value); |
285 | 0 | return 1; |
286 | 0 | } |
287 | | |
288 | | /* |
289 | | * check if the provided value is a valid classname |
290 | | */ |
291 | 0 | apr_byte_t oidc_metrics_is_valid_classname(apr_pool_t *pool, const char *name, char **valid_names) { |
292 | 0 | int i = 0; |
293 | 0 | int n = 0; |
294 | 0 | apr_table_t *names = apr_table_make(pool, 1); |
295 | 0 | oidc_metrics_add_classname_ctx_t ctx = {pool, valid_names}; |
296 | |
|
297 | 0 | n = sizeof(_oidc_metrics_timings_info) / sizeof(oidc_metrics_timing_info_t); |
298 | 0 | for (i = 0; i < n; i++) { |
299 | 0 | apr_table_set(names, _oidc_metrics_timings_info[i].class_name, |
300 | 0 | _oidc_metrics_timings_info[i].class_name); |
301 | 0 | } |
302 | 0 | n = sizeof(_oidc_metrics_counters_info) / sizeof(oidc_metrics_counter_info_t); |
303 | 0 | for (i = 0; i < n; i++) { |
304 | | // NB: "claim" is the only counter class with a name/value option and is special-cased here (rather |
305 | | // than driven from a static list) |
306 | 0 | if (_oidc_strcmp(_oidc_metrics_counters_info[i].class_name, "claim") != 0) |
307 | 0 | apr_table_set(names, _oidc_metrics_counters_info[i].class_name, |
308 | 0 | _oidc_metrics_counters_info[i].class_name); |
309 | 0 | } |
310 | |
|
311 | 0 | *valid_names = NULL; |
312 | 0 | apr_table_do(oidc_metrics_add_classnames, &ctx, names, NULL); |
313 | 0 | *valid_names = apr_psprintf(pool, "%s%s%s", *valid_names ? *valid_names : "", *valid_names ? " | " : "", |
314 | 0 | "claim.id_token.* | claim.userinfo.*"); |
315 | |
|
316 | 0 | return apr_table_get(names, name) ? TRUE |
317 | 0 | : ((_oidc_strstr(name, "claim.id_token.") != NULL) || |
318 | 0 | (_oidc_strstr(name, "claim.userinfo.") != NULL)); |
319 | 0 | } |
320 | | |
321 | | /* |
322 | | * retrieve environment variable integer with default setting |
323 | | */ |
324 | 0 | static inline int _oidc_metrics_get_env_int(const char *name, int dval) { |
325 | 0 | return _oidc_str_to_int(getenv(name), dval); |
326 | 0 | } |
327 | | |
328 | 0 | #define OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR "OIDC_METRICS_CACHE_JSON_MAX" |
329 | | |
330 | | /* |
331 | | * fixed self-health header kept in the shared memory segment in front of the JSON metrics data; |
332 | | * 32-bit members only so the apr_atomic_*32 functions cover every supported APR version |
333 | | */ |
334 | | typedef struct oidc_metrics_shm_hdr_t { |
335 | | // number of flushes/resets that could not be stored because the JSON data outgrew the segment |
336 | | apr_uint32_t flush_errors; |
337 | | // seconds-since-epoch timestamp of the last completed flush cycle of this process' flush thread |
338 | | apr_uint32_t last_flush; |
339 | | } oidc_metrics_shm_hdr_t; |
340 | | |
341 | 0 | #define OIDC_METRICS_SHM_HDR_SIZE (APR_ALIGN_DEFAULT(sizeof(oidc_metrics_shm_hdr_t))) |
342 | | |
343 | 0 | static inline oidc_metrics_shm_hdr_t *_oidc_metrics_shm_hdr(void) { |
344 | 0 | return (oidc_metrics_shm_hdr_t *)apr_shm_baseaddr_get(_oidc_metrics_cache); |
345 | 0 | } |
346 | | |
347 | 0 | static inline char *_oidc_metrics_shm_json(void) { |
348 | 0 | return ((char *)apr_shm_baseaddr_get(_oidc_metrics_cache)) + OIDC_METRICS_SHM_HDR_SIZE; |
349 | 0 | } |
350 | | |
351 | | static apr_size_t _g_oidc_metrics_shm_size = 0; |
352 | | |
353 | | /* |
354 | | * get the maximum size of the serialized JSON metrics data: directive, environment variable or default |
355 | | */ |
356 | 0 | static inline apr_size_t _oidc_metrics_shm_size(server_rec *s) { |
357 | 0 | if (_g_oidc_metrics_shm_size == 0) { |
358 | 0 | const oidc_cfg_t *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); |
359 | 0 | int n = oidc_cfg_metrics_cache_json_max_get(cfg); |
360 | 0 | if (n == OIDC_CONFIG_POS_INT_UNSET) |
361 | 0 | n = _oidc_metrics_get_env_int(OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, |
362 | 0 | OIDC_METRICS_CACHE_JSON_MAX_DEFAULT); |
363 | 0 | if ((n < 1) || (n > 1024 * 256 * 4 * 100)) { |
364 | 0 | oidc_serror(s, "environment value %s out of bounds, fallback to default", |
365 | 0 | OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR); |
366 | 0 | _g_oidc_metrics_shm_size = OIDC_METRICS_CACHE_JSON_MAX_DEFAULT; |
367 | 0 | } else { |
368 | 0 | _g_oidc_metrics_shm_size = n; |
369 | 0 | } |
370 | 0 | } |
371 | 0 | return _g_oidc_metrics_shm_size; |
372 | 0 | } |
373 | | |
374 | | /* |
375 | | * retrieve the (JSON) serialized (global) metrics data from shared memory |
376 | | */ |
377 | 0 | static inline char *_oidc_metrics_storage_get(server_rec *s, apr_pool_t *pool) { |
378 | 0 | const char *p = _oidc_metrics_shm_json(); |
379 | 0 | return p && (*p != 0) ? apr_pstrndup(pool, p, _oidc_metrics_shm_size(s)) : NULL; |
380 | 0 | } |
381 | | |
382 | | /* |
383 | | * store the serialized (global) metrics data in shared memory |
384 | | */ |
385 | 0 | static inline void _oidc_metrics_storage_set(server_rec *s, const char *value) { |
386 | 0 | char *p = _oidc_metrics_shm_json(); |
387 | 0 | if (value) { |
388 | 0 | apr_size_t n = _oidc_strlen(value) + 1; |
389 | 0 | if (n > _oidc_metrics_shm_size(s)) { |
390 | 0 | apr_atomic_inc32(&_oidc_metrics_shm_hdr()->flush_errors); |
391 | 0 | oidc_serror(s, |
392 | 0 | "json value too large: set or increase " OIDCMetricsCacheJsonMax |
393 | 0 | " (or system environment variable %s) to a value " |
394 | 0 | "larger than %" APR_SIZE_T_FMT, |
395 | 0 | OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, _oidc_metrics_shm_size(s)); |
396 | 0 | } else { |
397 | 0 | _oidc_memcpy(p, value, n); |
398 | 0 | } |
399 | 0 | } else { |
400 | 0 | *p = 0; |
401 | 0 | } |
402 | 0 | } |
403 | | |
404 | | /* |
405 | | * parse a string into a JSON object |
406 | | */ |
407 | 0 | static oidc_json_t *oidc_metrics_json_load(apr_pool_t *pool, const char *s_json, char **s_err) { |
408 | 0 | oidc_json_t *json = NULL; |
409 | 0 | if (s_json == NULL) |
410 | 0 | s_json = "{}"; |
411 | 0 | oidc_json_parse(pool, s_json, 0, &json, s_err); |
412 | 0 | return json; |
413 | 0 | } |
414 | | |
415 | | /* |
416 | | * parse a string into a JSON object in a server_rec context |
417 | | */ |
418 | 0 | static oidc_json_t *oidc_metrics_json_parse_s(server_rec *s, apr_pool_t *pool, const char *s_json) { |
419 | 0 | char *s_err = NULL; |
420 | 0 | oidc_json_t *json = oidc_metrics_json_load(pool, s_json, &s_err); |
421 | 0 | if (json == NULL) |
422 | 0 | oidc_serror(s, "JSON parsing failed: %s", s_err); |
423 | 0 | return json; |
424 | 0 | } |
425 | | |
426 | | /* |
427 | | * recursively reset all integer leaf values within a JSON object tree to 0 |
428 | | */ |
429 | 0 | static void oidc_metrics_reset_integer_tree(oidc_json_t *json) { |
430 | 0 | void *iter = oidc_json_object_iter(json); |
431 | 0 | while (iter) { |
432 | 0 | oidc_json_t *value = oidc_json_object_iter_value(iter); |
433 | 0 | if (oidc_json_is_integer(value)) |
434 | 0 | oidc_json_integer_set(value, 0); |
435 | 0 | else if (oidc_json_is_object(value)) |
436 | 0 | oidc_metrics_reset_integer_tree(value); |
437 | 0 | iter = oidc_json_object_iter_next(json, iter); |
438 | 0 | } |
439 | 0 | } |
440 | | |
441 | | /* |
442 | | * reset a single timings entry (buckets, sum, count) to 0 |
443 | | */ |
444 | 0 | static void oidc_metrics_reset_timer(oidc_json_t *j_entry) { |
445 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
446 | 0 | oidc_json_object_set_new(j_entry, _oidc_metric_buckets[i].name, oidc_json_integer(0)); |
447 | 0 | oidc_json_object_set_new(j_entry, OIDC_METRICS_SUM, oidc_json_integer(0)); |
448 | 0 | oidc_json_object_set_new(j_entry, OIDC_METRICS_COUNT, oidc_json_integer(0)); |
449 | 0 | } |
450 | | |
451 | | /* |
452 | | * reset all counter and timer entries for a single server in the JSON data |
453 | | */ |
454 | 0 | static void oidc_metrics_reset_server(const oidc_json_t *j_server) { |
455 | 0 | oidc_json_t *j_entries = NULL; |
456 | 0 | void *iter = NULL; |
457 | |
|
458 | 0 | j_entries = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
459 | 0 | if (j_entries) |
460 | 0 | oidc_metrics_reset_integer_tree(j_entries); |
461 | |
|
462 | 0 | j_entries = oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS); |
463 | 0 | iter = oidc_json_object_iter(j_entries); |
464 | 0 | while (iter) { |
465 | 0 | oidc_metrics_reset_timer(oidc_json_object_iter_value(iter)); |
466 | 0 | iter = oidc_json_object_iter_next(j_entries, iter); |
467 | 0 | } |
468 | 0 | } |
469 | | |
470 | | /* |
471 | | * reset the serialized (global) metrics data in shared memory |
472 | | */ |
473 | 0 | static inline void oidc_metrics_storage_reset(server_rec *s, apr_pool_t *pool) { |
474 | 0 | const char *s_json = NULL; |
475 | 0 | oidc_json_t *json = NULL; |
476 | 0 | void *iter = NULL; |
477 | | |
478 | | /* get the global stringified JSON metrics */ |
479 | 0 | s_json = _oidc_metrics_storage_get(s, pool); |
480 | | |
481 | | /* parse the metrics string to JSON */ |
482 | 0 | json = oidc_metrics_json_parse_s(s, pool, s_json); |
483 | 0 | if (json == NULL) |
484 | 0 | json = oidc_json_object(); |
485 | |
|
486 | 0 | iter = oidc_json_object_iter(json); |
487 | 0 | while (iter) { |
488 | 0 | oidc_metrics_reset_server(oidc_json_object_iter_value(iter)); |
489 | 0 | iter = oidc_json_object_iter_next(json, iter); |
490 | 0 | } |
491 | | |
492 | | /* serialize the metrics data, preserve order is required for Prometheus */ |
493 | 0 | s_json = oidc_json_encode(pool, json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
494 | | |
495 | | /* free the JSON data */ |
496 | 0 | oidc_json_decref(json); |
497 | | |
498 | | /* store the serialized metrics data in shared memory */ |
499 | 0 | _oidc_metrics_storage_set(s, s_json); |
500 | 0 | } |
501 | | |
502 | | /* |
503 | | * create a new timings entry in the collected JSON data |
504 | | */ |
505 | 0 | static oidc_json_t *oidc_metrics_timings_new(const oidc_metrics_timing_t *timing) { |
506 | 0 | oidc_json_t *entry = oidc_json_object(); |
507 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
508 | 0 | oidc_json_object_set_new(entry, _oidc_metric_buckets[i].name, oidc_json_integer(timing->buckets[i])); |
509 | | /* the sum is kept in microseconds so the _seconds Prometheus family is exact; the |
510 | | * (documented) json format converts to milliseconds at presentation time */ |
511 | 0 | oidc_json_object_set_new(entry, OIDC_METRICS_SUM, oidc_json_integer(timing->sum)); |
512 | 0 | oidc_json_object_set_new(entry, OIDC_METRICS_COUNT, oidc_json_integer(timing->count)); |
513 | 0 | return entry; |
514 | 0 | } |
515 | | |
516 | | /* |
517 | | * update an entry in the collected JSON data |
518 | | */ |
519 | 0 | static void oidc_metrics_timings_update(server_rec *s, const oidc_json_t *entry, const oidc_metrics_timing_t *timing) { |
520 | 0 | oidc_json_t *j_member = NULL; |
521 | 0 | oidc_json_int_t n = 0; |
522 | 0 | oidc_json_int_t v = 0; |
523 | |
|
524 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { |
525 | 0 | j_member = oidc_json_object_get(entry, _oidc_metric_buckets[i].name); |
526 | 0 | oidc_json_integer_set(j_member, oidc_json_integer_value(j_member) + timing->buckets[i]); |
527 | 0 | } |
528 | |
|
529 | 0 | j_member = oidc_json_object_get(entry, OIDC_METRICS_SUM); |
530 | 0 | n = oidc_json_integer_value(j_member); |
531 | | |
532 | | /* microseconds, matching oidc_metrics_timings_new */ |
533 | 0 | v = timing->sum; |
534 | 0 | if (_is_overflow(s, n, v)) |
535 | 0 | n = 0; |
536 | |
|
537 | 0 | oidc_json_integer_set(j_member, n + v); |
538 | |
|
539 | 0 | j_member = oidc_json_object_get(entry, OIDC_METRICS_COUNT); |
540 | 0 | n = oidc_json_integer_value(j_member); |
541 | 0 | oidc_json_integer_set(j_member, n + timing->count); |
542 | 0 | } |
543 | | |
544 | 0 | #define OIDC_METRICS_VALUE_DEFAULT "_" |
545 | | |
546 | | /* |
547 | | * value helper to make sure it is not empty |
548 | | */ |
549 | 0 | static inline const char *_metrics_value2key(const char *value) { |
550 | 0 | return (value && _oidc_strcmp(value, "") != 0) ? value : OIDC_METRICS_VALUE_DEFAULT; |
551 | 0 | } |
552 | | |
553 | | /* |
554 | | * create a new counter entry in the collected JSON data |
555 | | */ |
556 | 0 | static oidc_json_t *oidc_metrics_counter_new(apr_hash_t *htable) { |
557 | 0 | oidc_metrics_counter_t *counter = NULL; |
558 | 0 | char *value = NULL; |
559 | 0 | oidc_json_t *j_values = NULL; |
560 | 0 | for (apr_hash_index_t *hi = apr_hash_first(NULL, htable); hi; hi = apr_hash_next(hi)) { |
561 | 0 | apr_hash_this(hi, (const void **)&value, NULL, (void **)&counter); |
562 | 0 | if (_oidc_strcmp(value, OIDC_METRICS_VALUE_DEFAULT) == 0) { |
563 | 0 | j_values = oidc_json_integer(counter->count); |
564 | 0 | } else { |
565 | 0 | if (j_values == NULL) |
566 | 0 | j_values = oidc_json_object(); |
567 | 0 | oidc_json_object_set_new(j_values, value, oidc_json_integer(counter->count)); |
568 | 0 | } |
569 | 0 | } |
570 | 0 | return j_values; |
571 | 0 | } |
572 | | |
573 | | /* |
574 | | * update a counter entry in the collected JSON data |
575 | | */ |
576 | 0 | static void oidc_metrics_counter_update(server_rec *s, oidc_json_t *j_counter, apr_hash_t *htable) { |
577 | 0 | oidc_json_int_t v = 0; |
578 | 0 | oidc_metrics_counter_t *counter = NULL; |
579 | 0 | char *value = NULL; |
580 | 0 | oidc_json_t *j_value = NULL; |
581 | 0 | for (apr_hash_index_t *hi = apr_hash_first(NULL, htable); hi; hi = apr_hash_next(hi)) { |
582 | 0 | apr_hash_this(hi, (const void **)&value, NULL, (void **)&counter); |
583 | 0 | if (_oidc_strcmp(value, OIDC_METRICS_VALUE_DEFAULT) == 0) { |
584 | 0 | j_value = j_counter; |
585 | 0 | } else { |
586 | 0 | j_value = oidc_json_object_get(j_counter, value); |
587 | 0 | if (j_value == NULL) { |
588 | 0 | oidc_json_object_set_new(j_counter, value, oidc_json_integer(counter->count)); |
589 | 0 | continue; |
590 | 0 | } |
591 | 0 | } |
592 | 0 | v = oidc_json_integer_value(j_value); |
593 | 0 | if (_is_overflow(s, v, counter->count)) |
594 | 0 | v = 0; |
595 | 0 | oidc_json_integer_set(j_value, v + counter->count); |
596 | 0 | } |
597 | 0 | } |
598 | | |
599 | | /* |
600 | | * get or create the vhost entry in the global metrics |
601 | | */ |
602 | 0 | static oidc_json_t *oidc_metrics_server_get(oidc_json_t *json, const char *name) { |
603 | 0 | oidc_json_t *j_server = oidc_json_object_get(json, name); |
604 | 0 | if (j_server == NULL) { |
605 | 0 | j_server = oidc_json_object(); |
606 | 0 | oidc_json_object_set_new(j_server, OIDC_METRICS_COUNTERS, oidc_json_object()); |
607 | 0 | oidc_json_object_set_new(j_server, OIDC_METRICS_TIMINGS, oidc_json_object()); |
608 | 0 | oidc_json_object_set_new(json, name, j_server); |
609 | 0 | } |
610 | 0 | return j_server; |
611 | 0 | } |
612 | | |
613 | | /* |
614 | | * convert an enum type value to its corresponding string |
615 | | */ |
616 | 0 | static inline char *_oidc_metrics_type_name2key(apr_pool_t *pool, unsigned int type, const char *name) { |
617 | 0 | return (name == NULL) ? apr_psprintf(pool, "%u", type) : apr_psprintf(pool, "%u.%s", type, name); |
618 | 0 | } |
619 | | |
620 | | /* |
621 | | * look up the info table entry for the enum type in a "<type>[.<name>]" key: the keys come out of the |
622 | | * JSON in the shared memory segment that this module writes itself, but a corrupted or stale segment |
623 | | * (or a key that does not parse) must not turn into an out-of-bounds index into the info tables, so |
624 | | * the type is checked against the table size right where it indexes it; NULL for a key that does not |
625 | | * name a type this build knows, which the callers skip |
626 | | */ |
627 | 0 | static inline const oidc_metrics_counter_info_t *_oidc_metrics_key2counter_info(const char *key) { |
628 | 0 | unsigned int type = 0; |
629 | 0 | if ((key == NULL) || (sscanf(key, "%u", &type) != 1) || (type >= OM_NUMBER_OF_COUNTERS)) |
630 | 0 | return NULL; |
631 | 0 | return &_oidc_metrics_counters_info[type]; |
632 | 0 | } |
633 | | |
634 | 0 | static inline const oidc_metrics_timing_info_t *_oidc_metrics_key2timing_info(const char *key) { |
635 | 0 | unsigned int type = 0; |
636 | 0 | if ((key == NULL) || (sscanf(key, "%u", &type) != 1) || (type >= OM_NUMBER_OF_TIMINGS)) |
637 | 0 | return NULL; |
638 | 0 | return &_oidc_metrics_timings_info[type]; |
639 | 0 | } |
640 | | |
641 | | /* |
642 | | * update an existing counter under parent[key] or create a new one from counter_hash |
643 | | */ |
644 | | static void oidc_metrics_counter_set_or_update(server_rec *s, oidc_json_t *parent, const char *key, |
645 | 0 | apr_hash_t *counter_hash) { |
646 | 0 | oidc_json_t *j_counter = oidc_json_object_get(parent, key); |
647 | 0 | if (j_counter != NULL) |
648 | 0 | oidc_metrics_counter_update(s, j_counter, counter_hash); |
649 | 0 | else |
650 | 0 | oidc_json_object_set_new(parent, key, oidc_metrics_counter_new(counter_hash)); |
651 | 0 | } |
652 | | |
653 | | /* |
654 | | * merge a single local counter entry into the global counters object; |
655 | | * keys of the form "class.name" are nested as j_counters[class][name] |
656 | | */ |
657 | | static void oidc_metrics_store_counter_entry(server_rec *s, apr_pool_t *pool, oidc_json_t *j_counters, const char *key, |
658 | 0 | apr_hash_t *counter_hash) { |
659 | 0 | const char *class_name = apr_pstrdup(pool, key); |
660 | 0 | char *name = _oidc_strstr(class_name, "."); |
661 | 0 | oidc_json_t *j_names = NULL; |
662 | |
|
663 | 0 | if (name == NULL) { |
664 | 0 | oidc_metrics_counter_set_or_update(s, j_counters, class_name, counter_hash); |
665 | 0 | return; |
666 | 0 | } |
667 | | |
668 | 0 | *name++ = '\0'; |
669 | 0 | j_names = oidc_json_object_get(j_counters, class_name); |
670 | 0 | if (j_names == NULL) { |
671 | 0 | j_names = oidc_json_object(); |
672 | 0 | oidc_json_object_set_new(j_names, name, oidc_metrics_counter_new(counter_hash)); |
673 | 0 | oidc_json_object_set_new(j_counters, class_name, j_names); |
674 | 0 | return; |
675 | 0 | } |
676 | | |
677 | 0 | oidc_metrics_counter_set_or_update(s, j_names, name, counter_hash); |
678 | 0 | } |
679 | | |
680 | | /* |
681 | | * merge all locally collected counters into the global JSON |
682 | | */ |
683 | 0 | static void oidc_metrics_store_counters(server_rec *s, apr_pool_t *pool, oidc_json_t *json) { |
684 | 0 | const char *name = NULL; |
685 | 0 | const char *key = NULL; |
686 | 0 | apr_hash_t *server_hash = NULL; |
687 | 0 | apr_hash_t *counter_hash = NULL; |
688 | 0 | oidc_json_t *j_counters = NULL; |
689 | |
|
690 | 0 | for (apr_hash_index_t *hi1 = apr_hash_first(NULL, _oidc_metrics.counters); hi1; hi1 = apr_hash_next(hi1)) { |
691 | 0 | apr_hash_this(hi1, (const void **)&name, NULL, (void **)&server_hash); |
692 | 0 | j_counters = oidc_json_object_get(oidc_metrics_server_get(json, name), OIDC_METRICS_COUNTERS); |
693 | 0 | for (apr_hash_index_t *hi2 = apr_hash_first(NULL, server_hash); hi2; hi2 = apr_hash_next(hi2)) { |
694 | 0 | apr_hash_this(hi2, (const void **)&key, NULL, (void **)&counter_hash); |
695 | 0 | oidc_metrics_store_counter_entry(s, pool, j_counters, key, counter_hash); |
696 | 0 | } |
697 | 0 | } |
698 | 0 | } |
699 | | |
700 | | /* |
701 | | * merge all locally collected timings into the global JSON |
702 | | */ |
703 | 0 | static void oidc_metrics_store_timings(server_rec *s, oidc_json_t *json) { |
704 | 0 | const char *name = NULL; |
705 | 0 | const char *key = NULL; |
706 | 0 | apr_hash_t *server_hash = NULL; |
707 | 0 | oidc_metrics_timing_t *timing = NULL; |
708 | 0 | oidc_json_t *j_timings = NULL; |
709 | 0 | const oidc_json_t *j_timer = NULL; |
710 | |
|
711 | 0 | for (apr_hash_index_t *hi1 = apr_hash_first(NULL, _oidc_metrics.timings); hi1; hi1 = apr_hash_next(hi1)) { |
712 | 0 | apr_hash_this(hi1, (const void **)&name, NULL, (void **)&server_hash); |
713 | 0 | j_timings = oidc_json_object_get(oidc_metrics_server_get(json, name), OIDC_METRICS_TIMINGS); |
714 | 0 | for (apr_hash_index_t *hi2 = apr_hash_first(NULL, server_hash); hi2; hi2 = apr_hash_next(hi2)) { |
715 | 0 | apr_hash_this(hi2, (const void **)&key, NULL, (void **)&timing); |
716 | 0 | j_timer = oidc_json_object_get(j_timings, key); |
717 | 0 | if (j_timer != NULL) |
718 | 0 | oidc_metrics_timings_update(s, j_timer, timing); |
719 | 0 | else |
720 | 0 | oidc_json_object_set_new(j_timings, key, oidc_metrics_timings_new(timing)); |
721 | 0 | } |
722 | 0 | } |
723 | 0 | } |
724 | | |
725 | | /* |
726 | | * flush the locally gathered metrics data into the global data kept in shared memory |
727 | | */ |
728 | 0 | static apr_byte_t oidc_metrics_store(server_rec *s) { |
729 | 0 | const char *s_json = NULL; |
730 | 0 | oidc_json_t *json = NULL; |
731 | 0 | apr_pool_t *pool = NULL; |
732 | |
|
733 | 0 | if ((apr_hash_count(_oidc_metrics.counters) == 0) && (apr_hash_count(_oidc_metrics.timings) == 0)) |
734 | 0 | return TRUE; |
735 | | |
736 | | /* everything below is scratch: the whole document is read, re-serialized and copied |
737 | | * into shared memory on every flush, so it must not come from the process pool, |
738 | | * which is never cleared for the lifetime of the server */ |
739 | 0 | if (apr_pool_create(&pool, s->process->pool) != APR_SUCCESS) { |
740 | 0 | oidc_serror(s, "apr_pool_create failed: cannot flush metrics"); |
741 | 0 | return FALSE; |
742 | 0 | } |
743 | | |
744 | | /* lock the shared memory for other processes */ |
745 | 0 | if (oidc_cache_mutex_lock(pool, s, _oidc_metrics_global_mutex) == FALSE) { |
746 | 0 | apr_pool_destroy(pool); |
747 | 0 | return FALSE; |
748 | 0 | } |
749 | | |
750 | | /* get the global stringified JSON metrics */ |
751 | 0 | s_json = _oidc_metrics_storage_get(s, pool); |
752 | | |
753 | | /* parse the metrics string to JSON */ |
754 | 0 | json = oidc_metrics_json_parse_s(s, pool, s_json); |
755 | 0 | if (json == NULL) |
756 | 0 | json = oidc_json_object(); |
757 | |
|
758 | 0 | oidc_metrics_store_counters(s, pool, json); |
759 | 0 | oidc_metrics_store_timings(s, json); |
760 | | |
761 | | /* serialize the metrics data, preserve order is required for Prometheus */ |
762 | 0 | s_json = oidc_json_encode(pool, json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
763 | | |
764 | | /* free the JSON data */ |
765 | 0 | oidc_json_decref(json); |
766 | | |
767 | | /* store the serialized metrics data in shared memory */ |
768 | 0 | _oidc_metrics_storage_set(s, s_json); |
769 | | |
770 | | /* unlock the shared memory for other processes; the transfer already completed, so an unlock |
771 | | * failure (logged by the mutex layer) still reports the data as consumed and a later cycle |
772 | | * cannot double-count it */ |
773 | 0 | (void)oidc_cache_mutex_unlock(pool, s, _oidc_metrics_global_mutex); |
774 | |
|
775 | 0 | apr_pool_destroy(pool); |
776 | 0 | return TRUE; |
777 | 0 | } |
778 | | |
779 | 0 | #define OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR "OIDC_METRICS_CACHE_STORAGE_INTERVAL" |
780 | | |
781 | | /* |
782 | | * obtain the metrics flush interval: directive, environment variable or default |
783 | | */ |
784 | 0 | static inline apr_interval_time_t _oidc_metrics_interval(server_rec *s) { |
785 | 0 | const oidc_cfg_t *cfg = ap_get_module_config(s->module_config, &auth_openidc_module); |
786 | 0 | int n = oidc_cfg_metrics_cache_storage_interval_get(cfg); |
787 | 0 | if (n == OIDC_CONFIG_POS_INT_UNSET) |
788 | 0 | n = _oidc_metrics_get_env_int(OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR, |
789 | 0 | OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT); |
790 | 0 | return apr_time_from_msec(n); |
791 | 0 | } |
792 | | |
793 | | #define OIDC_METRICS_POLL_INTERVAL 250 |
794 | | |
795 | | /* |
796 | | * thread that periodically writes the local data into the shared memory |
797 | | */ |
798 | 0 | static void *APR_THREAD_FUNC oidc_metrics_thread_run(apr_thread_t *thread, void *data) { |
799 | 0 | server_rec *s = (server_rec *)data; |
800 | | |
801 | | /* sleep for a short random time <0.1s so child processes write-lock on a different frequency */ |
802 | 0 | apr_sleep(apr_time_from_msec(oidc_util_rand_int(100))); |
803 | | |
804 | | /* split the flush interval into POLL_INTERVAL-sized ticks so shutdown is observed quickly; if the |
805 | | * configured interval is shorter than POLL_INTERVAL, use it as the tick directly so we still flush |
806 | | * on schedule rather than busy-looping with n=0 */ |
807 | 0 | apr_interval_time_t interval = _oidc_metrics_interval(s); |
808 | | /* a misconfigured env var parses to 0 (or negative) — fall back to the default to avoid 0/0 below */ |
809 | 0 | if (interval <= 0) |
810 | 0 | interval = apr_time_from_msec(OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT); |
811 | 0 | apr_interval_time_t tick = apr_time_from_msec(OIDC_METRICS_POLL_INTERVAL); |
812 | 0 | if (tick > interval) |
813 | 0 | tick = interval; |
814 | 0 | int n = (int)(interval / tick); |
815 | | |
816 | | /* NB: test for exit only after flushing, because we need to write our local metrics into the cache before |
817 | | * exiting, also when asked to exit during the random sleep above */ |
818 | 0 | do { |
819 | | |
820 | | /* break up the sleep interval in short intervals so we can exit timely fashion without confusing Apache |
821 | | * at shutdown */ |
822 | 0 | for (int i = 0; (i < n) && (apr_atomic_read32(&_oidc_metrics_thread_exit) == 0); i++) |
823 | 0 | apr_sleep(tick); |
824 | | |
825 | | /* lock the mutex that protects the locally cached metrics */ |
826 | 0 | if (oidc_cache_mutex_lock(s->process->pool, s, _oidc_metrics_process_mutex) == FALSE) |
827 | 0 | continue; |
828 | | |
829 | | /* flush the locally cached metrics into the global shared memory */ |
830 | 0 | if (oidc_metrics_store(s) == TRUE) { |
831 | | /* record that a flush cycle completed, for the metrics self-health output */ |
832 | 0 | apr_atomic_set32(&_oidc_metrics_shm_hdr()->last_flush, |
833 | 0 | (apr_uint32_t)apr_time_sec(apr_time_now())); |
834 | | |
835 | | /* release all allocations made while gathering this flush interval */ |
836 | 0 | oidc_metrics_local_reset(s); |
837 | 0 | } |
838 | | |
839 | | /* unlock the mutex that protects the locally cached metrics */ |
840 | 0 | if (oidc_cache_mutex_unlock(s->process->pool, s, _oidc_metrics_process_mutex) == FALSE) |
841 | 0 | break; |
842 | |
|
843 | 0 | } while (apr_atomic_read32(&_oidc_metrics_thread_exit) == 0); |
844 | | |
845 | | /* NB: don't call apr_thread_exit here because it seems that Apache is cleaning up its own threads */ |
846 | |
|
847 | 0 | return NULL; |
848 | 0 | } |
849 | | |
850 | | /* |
851 | | * stop the flush thread, which writes what it gathered into shared memory one last time on its way out; a handle |
852 | | * inherited over fork() names a thread that does not exist in this process and is just dropped |
853 | | */ |
854 | 0 | static void oidc_metrics_thread_stop(server_rec *s) { |
855 | 0 | apr_status_t rv = APR_SUCCESS; |
856 | |
|
857 | 0 | if ((_oidc_metrics_thread != NULL) && (_oidc_metrics_thread_pid == getpid())) { |
858 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 1); |
859 | 0 | if (apr_thread_join(&rv, _oidc_metrics_thread) != APR_SUCCESS) |
860 | 0 | oidc_serror(s, "apr_thread_join failed"); |
861 | 0 | apr_atomic_set32(&_oidc_metrics_thread_exit, 0); |
862 | 0 | } |
863 | 0 | _oidc_metrics_thread = NULL; |
864 | 0 | _oidc_metrics_thread_pid = 0; |
865 | 0 | } |
866 | | |
867 | | /* |
868 | | * child pool cleanup: stop the flush thread before the child exits; leaving that to oidc_metrics_cleanup() is too |
869 | | * late, since that is a pconf cleanup which at exit only runs after apr_terminate() has already destroyed the |
870 | | * local collection pool that the thread clears after every flush (see oidc_metrics_local_pool_cleanup), and a |
871 | | * flush landing in between crashes in apr_pool_clear() |
872 | | */ |
873 | 0 | static apr_status_t oidc_metrics_child_cleanup(void *data) { |
874 | 0 | oidc_metrics_thread_stop((server_rec *)data); |
875 | 0 | return APR_SUCCESS; |
876 | 0 | } |
877 | | |
878 | | /* |
879 | | * server config handlers |
880 | | */ |
881 | | |
882 | | /* |
883 | | * NB: global, yet called for each vhost that has metrics enabled! |
884 | | */ |
885 | 0 | apr_byte_t oidc_metrics_post_config(apr_pool_t *pool, server_rec *s) { |
886 | | |
887 | | /* make sure it gets executed exactly once! */ |
888 | 0 | if (_oidc_metrics_cache != NULL) |
889 | 0 | return TRUE; |
890 | | |
891 | | /* create the shared memory segment that holds the self-health header plus the |
892 | | * stringified JSON formatted metrics data */ |
893 | 0 | if (apr_shm_create(&_oidc_metrics_cache, OIDC_METRICS_SHM_HDR_SIZE + _oidc_metrics_shm_size(s), NULL, |
894 | 0 | s->process->pool) != APR_SUCCESS) |
895 | 0 | return FALSE; |
896 | 0 | if (_oidc_metrics_cache == NULL) |
897 | 0 | return FALSE; |
898 | | |
899 | | /* initialize the shared memory segment to 0 */ |
900 | 0 | char *p = apr_shm_baseaddr_get(_oidc_metrics_cache); |
901 | 0 | _oidc_memset(p, 0, OIDC_METRICS_SHM_HDR_SIZE + _oidc_metrics_shm_size(s)); |
902 | | |
903 | | /* flag this as the parent, for shared memory cleanup purposes and "multiple child-init calls" detection */ |
904 | 0 | _oidc_metrics_is_parent = TRUE; |
905 | | |
906 | | /* create the pool and hashtables that hold local metrics data */ |
907 | 0 | if (oidc_metrics_local_reset(s) == FALSE) |
908 | 0 | return FALSE; |
909 | | |
910 | | /* create and initialize the mutex that guards the shared-memory data */ |
911 | 0 | _oidc_metrics_global_mutex = oidc_cache_mutex_create(s->process->pool, TRUE); |
912 | 0 | if (_oidc_metrics_global_mutex == NULL) |
913 | 0 | return FALSE; |
914 | 0 | if (oidc_cache_mutex_post_config(s->process->pool, s, _oidc_metrics_global_mutex, "metrics-global") == FALSE) |
915 | 0 | return FALSE; |
916 | | |
917 | | /* create and initialize the mutex that guards the local metrics tables */ |
918 | 0 | _oidc_metrics_process_mutex = oidc_cache_mutex_create(s->process->pool, FALSE); |
919 | 0 | if (_oidc_metrics_process_mutex == NULL) |
920 | 0 | return FALSE; |
921 | 0 | if (oidc_cache_mutex_post_config(s->process->pool, s, _oidc_metrics_process_mutex, "metrics-process") == FALSE) |
922 | 0 | return FALSE; |
923 | | |
924 | | /* NB: no flush thread in the parent: samples are only ever recorded while handling a request, which the parent |
925 | | * does not do; the processes that do (including a -X single process) start one in oidc_metrics_child_init, and |
926 | | * a parent thread would race apr_pool_destroy() of the process pool at shutdown, which happens before the pconf |
927 | | * cleanup that would stop it */ |
928 | | |
929 | 0 | return TRUE; |
930 | 0 | } |
931 | | |
932 | | /* |
933 | | * NB: global, yet called for each vhost that has metrics enabled! |
934 | | */ |
935 | 0 | apr_status_t oidc_metrics_child_init(apr_pool_t *p, server_rec *s) { |
936 | | |
937 | | /* make sure this executes only once per child */ |
938 | 0 | if (_oidc_metrics_is_parent == FALSE) |
939 | 0 | return APR_SUCCESS; |
940 | | |
941 | 0 | if (oidc_cache_mutex_child_init(p, s, _oidc_metrics_global_mutex) != APR_SUCCESS) |
942 | 0 | return APR_EGENERAL; |
943 | | |
944 | 0 | if (oidc_cache_mutex_child_init(p, s, _oidc_metrics_process_mutex) != APR_SUCCESS) |
945 | 0 | return APR_EGENERAL; |
946 | | |
947 | | /* stop an existing same-process flush thread; discard inherited handles after a fork */ |
948 | 0 | oidc_metrics_thread_stop(s); |
949 | 0 | if (oidc_metrics_local_reset(s) == FALSE) |
950 | 0 | return APR_EGENERAL; |
951 | | |
952 | | /* create the thread only after all state that it accesses has been initialized */ |
953 | 0 | if (apr_thread_create(&_oidc_metrics_thread, NULL, oidc_metrics_thread_run, s, s->process->pool) != APR_SUCCESS) |
954 | 0 | return APR_EGENERAL; |
955 | 0 | _oidc_metrics_thread_pid = getpid(); |
956 | | |
957 | | /* stop it again when the child pool goes; registered after the mutex child_init calls above so that, cleanups |
958 | | * running last-in-first-out, the thread's final flush still finds them initialized */ |
959 | 0 | apr_pool_cleanup_register(p, s, oidc_metrics_child_cleanup, apr_pool_cleanup_null); |
960 | | |
961 | | /* flag this is a child */ |
962 | 0 | _oidc_metrics_is_parent = FALSE; |
963 | |
|
964 | 0 | return APR_SUCCESS; |
965 | 0 | } |
966 | | |
967 | | /* |
968 | | * NB: global, yet called for each vhost that has metrics enabled! |
969 | | */ |
970 | 0 | apr_status_t oidc_metrics_cleanup(server_rec *s) { |
971 | | |
972 | | /* make sure it gets executed exactly once! NB: not keyed on the flush thread, which the parent does not run |
973 | | * and a child has already stopped in oidc_metrics_child_cleanup by the time it gets here */ |
974 | 0 | if (_oidc_metrics_cache == NULL) |
975 | 0 | return APR_SUCCESS; |
976 | | |
977 | | /* stop the collector thread, if it still runs */ |
978 | 0 | oidc_metrics_thread_stop(s); |
979 | | |
980 | | /* delete the shared memory segment if we are in the parent process */ |
981 | 0 | if (_oidc_metrics_is_parent == TRUE) |
982 | 0 | apr_shm_destroy(_oidc_metrics_cache); |
983 | 0 | _oidc_metrics_cache = NULL; |
984 | | /* the cached segment size lives exactly as long as the segment: the next post_config (a graceful |
985 | | * restart, or the next unit test in the same process) sizes a new one from its own configuration */ |
986 | 0 | _g_oidc_metrics_shm_size = 0; |
987 | | |
988 | | /* delete the process mutex that guards the local metrics data */ |
989 | 0 | if (oidc_cache_mutex_destroy(s, _oidc_metrics_process_mutex) == FALSE) |
990 | 0 | return APR_EGENERAL; |
991 | 0 | _oidc_metrics_process_mutex = NULL; |
992 | | |
993 | | /* delete the process mutex that guards the global shared memory segment */ |
994 | 0 | if (oidc_cache_mutex_destroy(s, _oidc_metrics_global_mutex) == FALSE) |
995 | 0 | return APR_EGENERAL; |
996 | 0 | _oidc_metrics_global_mutex = NULL; |
997 | |
|
998 | 0 | if (_oidc_metrics.pool != NULL) |
999 | 0 | apr_pool_destroy(_oidc_metrics.pool); |
1000 | 0 | _oidc_metrics.pool = NULL; |
1001 | 0 | _oidc_metrics.counters = NULL; |
1002 | 0 | _oidc_metrics.timings = NULL; |
1003 | |
|
1004 | 0 | return APR_SUCCESS; |
1005 | 0 | } |
1006 | | |
1007 | | /* |
1008 | | * sampling |
1009 | | */ |
1010 | | |
1011 | | /* |
1012 | | * obtain the local metrics hashtable for the current vhost |
1013 | | */ |
1014 | 0 | static inline apr_hash_t *_oidc_metrics_server_hash(const request_rec *r, apr_hash_t *table) { |
1015 | 0 | apr_hash_t *server_hash = NULL; |
1016 | 0 | const char *name = "_default_"; |
1017 | | |
1018 | | /* obtain the server name */ |
1019 | 0 | if (r->server->server_hostname) |
1020 | 0 | name = r->server->server_hostname; |
1021 | | |
1022 | | /* get the entry to the vhost record, or newly create it */ |
1023 | 0 | server_hash = apr_hash_get(table, name, APR_HASH_KEY_STRING); |
1024 | 0 | if (server_hash == NULL) { |
1025 | 0 | server_hash = apr_hash_make(_oidc_metrics.pool); |
1026 | 0 | apr_hash_set(table, name, APR_HASH_KEY_STRING, server_hash); |
1027 | 0 | } |
1028 | |
|
1029 | 0 | return server_hash; |
1030 | 0 | } |
1031 | | |
1032 | | /* |
1033 | | * retrieve or create a local timing for the specified type |
1034 | | */ |
1035 | 0 | static inline oidc_metrics_timing_t *_oidc_metrics_timing_get(request_rec *r, unsigned int type) { |
1036 | 0 | oidc_metrics_timing_t *result = NULL; |
1037 | | /* NB: the lookup key is request-scoped; only the copy the hash retains on insert |
1038 | | * below is allocated process-wide, since that pool is never cleared */ |
1039 | 0 | const char *key = _oidc_metrics_type_name2key(r->pool, type, NULL); |
1040 | 0 | apr_hash_t *server_hash = _oidc_metrics_server_hash(r, _oidc_metrics.timings); |
1041 | | /* get the entry to the specified metric */ |
1042 | 0 | result = apr_hash_get(server_hash, key, APR_HASH_KEY_STRING); |
1043 | 0 | if (result == NULL) { |
1044 | 0 | result = apr_pcalloc(_oidc_metrics.pool, sizeof(oidc_metrics_timing_t)); |
1045 | 0 | apr_hash_set(server_hash, apr_pstrdup(_oidc_metrics.pool, key), APR_HASH_KEY_STRING, result); |
1046 | 0 | } |
1047 | 0 | return result; |
1048 | 0 | } |
1049 | | |
1050 | | /* |
1051 | | * retrieve or create a counter from a hashtable of values |
1052 | | */ |
1053 | 0 | static inline oidc_metrics_counter_t *_oidc_metrics_counter_value_get(apr_hash_t *table, const char *value) { |
1054 | | /* get the entry to the specified metric */ |
1055 | 0 | oidc_metrics_counter_t *result = apr_hash_get(table, value, APR_HASH_KEY_STRING); |
1056 | 0 | if (result == NULL) { |
1057 | 0 | result = apr_pcalloc(_oidc_metrics.pool, sizeof(oidc_metrics_counter_t)); |
1058 | 0 | apr_hash_set(table, apr_pstrdup(_oidc_metrics.pool, value), APR_HASH_KEY_STRING, result); |
1059 | 0 | } |
1060 | 0 | return result; |
1061 | 0 | } |
1062 | | |
1063 | | /* |
1064 | | * retrieve or create a local counter for the specified type and name |
1065 | | */ |
1066 | 0 | static inline apr_hash_t *_oidc_metrics_counter_get(request_rec *r, unsigned int type, const char *name) { |
1067 | 0 | apr_hash_t *result = NULL; |
1068 | | /* NB: request-scoped lookup key, process-wide copy only on insert - see above */ |
1069 | 0 | const char *key = _oidc_metrics_type_name2key(r->pool, type, name); |
1070 | 0 | apr_hash_t *server_hash = _oidc_metrics_server_hash(r, _oidc_metrics.counters); |
1071 | | |
1072 | | /* get the entry to the specified metric */ |
1073 | 0 | result = apr_hash_get(server_hash, key, APR_HASH_KEY_STRING); |
1074 | 0 | if (result == NULL) { |
1075 | 0 | result = apr_hash_make(_oidc_metrics.pool); |
1076 | 0 | apr_hash_set(server_hash, apr_pstrdup(_oidc_metrics.pool, key), APR_HASH_KEY_STRING, result); |
1077 | 0 | } |
1078 | |
|
1079 | 0 | return result; |
1080 | 0 | } |
1081 | | |
1082 | | /* |
1083 | | * add/increase a counter metric in the locally cached data |
1084 | | */ |
1085 | 0 | void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type, const char *name, const char *value) { |
1086 | 0 | oidc_metrics_counter_t *counter = NULL; |
1087 | | |
1088 | | /* lock the local metrics cache hashtable */ |
1089 | 0 | if (oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_process_mutex) == FALSE) |
1090 | 0 | return; |
1091 | | |
1092 | | /* obtain or create the entry for the specified key */ |
1093 | 0 | counter = _oidc_metrics_counter_value_get(_oidc_metrics_counter_get(r, type, name), _metrics_value2key(value)); |
1094 | | |
1095 | | /* performance */ |
1096 | 0 | if (counter->count <= 0) { |
1097 | | // new counter was created just now or reset earlier |
1098 | 0 | counter->count = 1; |
1099 | 0 | } else { |
1100 | | // increase after checking possible overflow |
1101 | 0 | if (_is_overflow(r->server, counter->count, 1)) |
1102 | 0 | counter->count = 0; |
1103 | 0 | counter->count++; |
1104 | 0 | } |
1105 | | |
1106 | | /* unlock the local metrics cache hashtable */ |
1107 | 0 | (void)oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); |
1108 | 0 | } |
1109 | | |
1110 | | /* |
1111 | | * zero out a timings entry (count, sum and all buckets) |
1112 | | */ |
1113 | 0 | static inline void _oidc_metrics_timing_clear(oidc_metrics_timing_t *timing) { |
1114 | 0 | timing->count = 0; |
1115 | 0 | timing->sum = 0; |
1116 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
1117 | 0 | timing->buckets[i] = 0; |
1118 | 0 | } |
1119 | | |
1120 | | /* |
1121 | | * increment every bucket whose threshold covers elapsed (buckets are ordered; |
1122 | | * threshold == 0 marks the open-ended last bucket) |
1123 | | */ |
1124 | 0 | static inline void _oidc_metrics_timing_buckets_inc(oidc_metrics_timing_t *timing, apr_time_t elapsed) { |
1125 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { |
1126 | 0 | if ((elapsed <= _oidc_metric_buckets[i].threshold) || (_oidc_metric_buckets[i].threshold == 0)) { |
1127 | 0 | for (int j = i; j < OIDC_METRICS_BUCKET_NUM; j++) |
1128 | 0 | timing->buckets[j]++; |
1129 | 0 | break; |
1130 | 0 | } |
1131 | 0 | } |
1132 | 0 | } |
1133 | | |
1134 | | /* |
1135 | | * add a metrics timing sample to the locally cached data |
1136 | | */ |
1137 | 0 | void oidc_metrics_timing_add(request_rec *r, oidc_metrics_timing_type_t type, apr_time_t elapsed) { |
1138 | 0 | oidc_metrics_timing_t *timing = NULL; |
1139 | |
|
1140 | 0 | if (elapsed < 0) { |
1141 | 0 | oidc_warn(r, "discarding metrics timing [%s.%s]: elapsed (%" APR_TIME_T_FMT ") < 0", |
1142 | 0 | _oidc_metrics_timings_info[type].class_name, _oidc_metrics_timings_info[type].metric_name, |
1143 | 0 | elapsed); |
1144 | 0 | return; |
1145 | 0 | } |
1146 | | |
1147 | | /* lock the local metrics cache hashtable */ |
1148 | 0 | if (oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_process_mutex) == FALSE) |
1149 | 0 | return; |
1150 | | |
1151 | | /* obtain or create the entry for the specified key */ |
1152 | 0 | timing = _oidc_metrics_timing_get(r, type); |
1153 | | |
1154 | | /* reset on overflow; a freshly-created entry already has count/sum/buckets at zero */ |
1155 | 0 | if ((timing->count > 0) && _is_overflow(r->server, timing->sum, elapsed)) |
1156 | 0 | _oidc_metrics_timing_clear(timing); |
1157 | |
|
1158 | 0 | _oidc_metrics_timing_buckets_inc(timing, elapsed); |
1159 | 0 | timing->sum += elapsed; |
1160 | 0 | timing->count++; |
1161 | | |
1162 | | /* unlock the local metrics cache hashtable */ |
1163 | 0 | (void)oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); |
1164 | 0 | } |
1165 | | |
1166 | | /* |
1167 | | * representation handlers |
1168 | | */ |
1169 | | |
1170 | | /* |
1171 | | * the "class.metric" name of a counter, from its info table entry |
1172 | | */ |
1173 | 0 | static inline char *_oidc_metrics_counter_info2s(apr_pool_t *pool, const oidc_metrics_counter_info_t *info) { |
1174 | 0 | return apr_psprintf(pool, "%s.%s", info->class_name, info->metric_name); |
1175 | 0 | } |
1176 | | |
1177 | | /* |
1178 | | * the "class.metric" name of a timing, from its info table entry |
1179 | | */ |
1180 | 0 | static inline char *_oidc_metrics_timing_info2s(apr_pool_t *pool, const oidc_metrics_timing_info_t *info) { |
1181 | 0 | return apr_psprintf(pool, "%s.%s", info->class_name, info->metric_name); |
1182 | 0 | } |
1183 | | |
1184 | | /* |
1185 | | * parse a string into a JSON object in the request_rec context |
1186 | | */ |
1187 | 0 | static oidc_json_t *oidc_metrics_json_parse_r(request_rec *r, const char *s_json) { |
1188 | 0 | char *s_err = NULL; |
1189 | 0 | oidc_json_t *json = oidc_metrics_json_load(r->pool, s_json, &s_err); |
1190 | 0 | if (json == NULL) |
1191 | 0 | oidc_error(r, "JSON parsing failed: %s", s_err); |
1192 | 0 | return json; |
1193 | 0 | } |
1194 | | |
1195 | | /* |
1196 | | * JSON with extended descriptions/names |
1197 | | */ |
1198 | 0 | static int oidc_metrics_handle_json(request_rec *r, const char *s_json) { |
1199 | |
|
1200 | 0 | oidc_json_t *json = NULL; |
1201 | 0 | const oidc_json_t *j_server = NULL; |
1202 | 0 | oidc_json_t *j_timings = NULL; |
1203 | 0 | oidc_json_t *j_counters = NULL; |
1204 | 0 | const oidc_json_t *j_timing = NULL; |
1205 | 0 | oidc_json_t *j_counter = NULL; |
1206 | 0 | oidc_json_t *o_json = NULL; |
1207 | 0 | oidc_json_t *o_server = NULL; |
1208 | 0 | oidc_json_t *o_counters = NULL; |
1209 | 0 | oidc_json_t *o_counter = NULL; |
1210 | 0 | oidc_json_t *o_timings = NULL; |
1211 | 0 | oidc_json_t *o_timing = NULL; |
1212 | 0 | const char *s_server = NULL; |
1213 | 0 | const oidc_metrics_counter_info_t *c_info = NULL; |
1214 | 0 | const oidc_metrics_timing_info_t *t_info = NULL; |
1215 | 0 | void *i1 = NULL; |
1216 | 0 | void *i2 = NULL; |
1217 | | |
1218 | | /* parse the metrics string to JSON */ |
1219 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1220 | 0 | if (json == NULL) |
1221 | 0 | goto end; |
1222 | | |
1223 | 0 | o_json = oidc_json_object(); |
1224 | |
|
1225 | 0 | i1 = oidc_json_object_iter(json); |
1226 | 0 | while (i1) { |
1227 | 0 | s_server = oidc_json_object_iter_key(i1); |
1228 | 0 | j_server = oidc_json_object_iter_value(i1); |
1229 | |
|
1230 | 0 | o_server = oidc_json_object(); |
1231 | 0 | oidc_json_object_set_new(o_json, s_server, o_server); |
1232 | |
|
1233 | 0 | j_counters = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
1234 | 0 | o_counters = oidc_json_object(); |
1235 | 0 | oidc_json_object_set_new(o_server, OIDC_METRICS_COUNTERS, o_counters); |
1236 | |
|
1237 | 0 | i2 = oidc_json_object_iter(j_counters); |
1238 | 0 | while (i2) { |
1239 | 0 | c_info = _oidc_metrics_key2counter_info(oidc_json_object_iter_key(i2)); |
1240 | 0 | if (c_info == NULL) { |
1241 | | /* not a counter this build knows: skip it rather than index past the info table */ |
1242 | 0 | i2 = oidc_json_object_iter_next(j_counters, i2); |
1243 | 0 | continue; |
1244 | 0 | } |
1245 | 0 | j_counter = oidc_json_object_iter_value(i2); |
1246 | 0 | o_counter = oidc_json_object(); |
1247 | 0 | if (oidc_json_is_integer(j_counter)) |
1248 | 0 | oidc_json_object_set(o_counter, "count", j_counter); |
1249 | 0 | else |
1250 | 0 | oidc_json_object_set_new(o_counter, "values", oidc_json_deep_copy(j_counter)); |
1251 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_CLASS_NAME, |
1252 | 0 | oidc_json_string(c_info->class_name)); |
1253 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_METRIC_NAME, |
1254 | 0 | oidc_json_string(c_info->metric_name)); |
1255 | 0 | oidc_json_object_set_new(o_counter, OIDC_METRICS_JSON_DESC, oidc_json_string(c_info->desc)); |
1256 | 0 | oidc_json_object_set_new(o_counters, _oidc_metrics_counter_info2s(r->pool, c_info), o_counter); |
1257 | 0 | i2 = oidc_json_object_iter_next(j_counters, i2); |
1258 | 0 | } |
1259 | |
|
1260 | 0 | j_timings = oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS); |
1261 | 0 | o_timings = oidc_json_object(); |
1262 | 0 | oidc_json_object_set_new(o_server, OIDC_METRICS_TIMINGS, o_timings); |
1263 | |
|
1264 | 0 | i2 = oidc_json_object_iter(j_timings); |
1265 | 0 | while (i2) { |
1266 | 0 | t_info = _oidc_metrics_key2timing_info(oidc_json_object_iter_key(i2)); |
1267 | 0 | if (t_info == NULL) { |
1268 | 0 | i2 = oidc_json_object_iter_next(j_timings, i2); |
1269 | 0 | continue; |
1270 | 0 | } |
1271 | 0 | j_timing = oidc_json_object_iter_value(i2); |
1272 | |
|
1273 | 0 | o_timing = oidc_json_deep_copy(j_timing); |
1274 | | /* the shm sum is kept in microseconds; this format documents milliseconds */ |
1275 | 0 | oidc_json_object_set_new( |
1276 | 0 | o_timing, OIDC_METRICS_SUM, |
1277 | 0 | oidc_json_integer( |
1278 | 0 | oidc_json_integer_value(oidc_json_object_get(j_timing, OIDC_METRICS_SUM)) / 1000)); |
1279 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_CLASS_NAME, |
1280 | 0 | oidc_json_string(t_info->class_name)); |
1281 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_METRIC_NAME, |
1282 | 0 | oidc_json_string(t_info->metric_name)); |
1283 | 0 | oidc_json_object_set_new(o_timing, OIDC_METRICS_JSON_DESC, oidc_json_string(t_info->desc)); |
1284 | |
|
1285 | 0 | oidc_json_object_set_new(o_timings, _oidc_metrics_timing_info2s(r->pool, t_info), o_timing); |
1286 | |
|
1287 | 0 | i2 = oidc_json_object_iter_next(j_timings, i2); |
1288 | 0 | } |
1289 | 0 | i1 = oidc_json_object_iter_next(json, i1); |
1290 | 0 | } |
1291 | |
|
1292 | 0 | s_json = oidc_json_encode(r->pool, o_json, OIDC_JSON_COMPACT | OIDC_JSON_PRESERVE_ORDER); |
1293 | |
|
1294 | 0 | oidc_json_decref(o_json); |
1295 | 0 | oidc_json_decref(json); |
1296 | |
|
1297 | 0 | end: |
1298 | | |
1299 | | /* return the data to the caller */ |
1300 | 0 | return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_HTTP_CONTENT_TYPE_JSON, OK); |
1301 | 0 | } |
1302 | | |
1303 | | /* |
1304 | | * dump the internal shared memory segment |
1305 | | */ |
1306 | 0 | static int oidc_metrics_handle_internal(request_rec *r, const char *s_json) { |
1307 | 0 | if (s_json == NULL) |
1308 | 0 | return HTTP_NOT_FOUND; |
1309 | 0 | return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_HTTP_CONTENT_TYPE_JSON, OK); |
1310 | 0 | } |
1311 | | |
1312 | 0 | #define OIDC_METRICS_SERVER_PARAM "server_name" |
1313 | 0 | #define OIDC_METRICS_COUNTER_PARAM "counter" |
1314 | 0 | #define OIDC_METRICS_NAME_PARAM "name" |
1315 | 0 | #define OIDC_METRICS_VALUE_PARAM "value" |
1316 | | |
1317 | | /* |
1318 | | * find the counter entry in j_counters whose type matches "class.metric" (s_metric_param) |
1319 | | */ |
1320 | | static oidc_json_t *oidc_metrics_status_find_counter(request_rec *r, oidc_json_t *j_counters, |
1321 | 0 | const char *s_metric_param) { |
1322 | 0 | const oidc_metrics_counter_info_t *info = NULL; |
1323 | 0 | void *iter = oidc_json_object_iter(j_counters); |
1324 | 0 | while (iter) { |
1325 | 0 | info = _oidc_metrics_key2counter_info(oidc_json_object_iter_key(iter)); |
1326 | 0 | if ((info != NULL) && (_oidc_strcmp(_oidc_metrics_counter_info2s(r->pool, info), s_metric_param) == 0)) |
1327 | 0 | return oidc_json_object_iter_value(iter); |
1328 | 0 | iter = oidc_json_object_iter_next(j_counters, iter); |
1329 | 0 | } |
1330 | 0 | return NULL; |
1331 | 0 | } |
1332 | | |
1333 | | /* |
1334 | | * extract the requested integer value out of a counter entry: |
1335 | | * - integer entry: the counter itself |
1336 | | * - object entry with value-only selector: j_counter[value] |
1337 | | * - object entry with name+value selectors: j_counter[name][value] |
1338 | | */ |
1339 | | static oidc_json_t *oidc_metrics_status_select_value(oidc_json_t *j_counter, const char *s_name_param, |
1340 | 0 | const char *s_value_param) { |
1341 | 0 | const oidc_json_t *j_values = NULL; |
1342 | 0 | if (oidc_json_is_integer(j_counter)) |
1343 | 0 | return j_counter; |
1344 | 0 | if (s_value_param == NULL) |
1345 | 0 | return NULL; |
1346 | 0 | if (s_name_param == NULL) |
1347 | 0 | return oidc_json_object_get(j_counter, s_value_param); |
1348 | 0 | j_values = oidc_json_object_get(j_counter, s_name_param); |
1349 | 0 | if (j_values == NULL) |
1350 | 0 | return NULL; |
1351 | 0 | return oidc_json_object_get(j_values, s_value_param); |
1352 | 0 | } |
1353 | | |
1354 | 0 | #define OIDC_METRICS_STATUS_PING_KEY "status-ping" |
1355 | | |
1356 | | /* |
1357 | | * readiness variant of format=status (no counter selector): verify that the configured cache |
1358 | | * backend completes a write/read round-trip and report how provider metadata is configured; |
1359 | | * returns a 503 with an "ERROR" body when the cache backend is not usable |
1360 | | */ |
1361 | 0 | static int oidc_metrics_status_readiness(request_rec *r) { |
1362 | 0 | oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); |
1363 | 0 | char *value = NULL; |
1364 | 0 | const char *msg = NULL; |
1365 | 0 | const char *s_provider = NULL; |
1366 | 0 | apr_byte_t ok = FALSE; |
1367 | | |
1368 | | /* write/read round-trip through the configured cache backend */ |
1369 | 0 | if ((oidc_cache_set(r, OIDC_CACHE_SECTION_HEALTH, OIDC_METRICS_STATUS_PING_KEY, "OK", |
1370 | 0 | apr_time_now() + apr_time_from_sec(60)) == TRUE) && |
1371 | 0 | (oidc_cache_get(r, OIDC_CACHE_SECTION_HEALTH, OIDC_METRICS_STATUS_PING_KEY, &value) == TRUE) && |
1372 | 0 | (value != NULL) && (_oidc_strcmp(value, "OK") == 0)) |
1373 | 0 | ok = TRUE; |
1374 | |
|
1375 | 0 | if (oidc_cfg_metadata_dir_get(cfg) != NULL) |
1376 | 0 | s_provider = "metadata_dir"; |
1377 | 0 | else if (oidc_cfg_provider_issuer_get(oidc_cfg_provider_get(cfg)) != NULL) |
1378 | 0 | s_provider = "static"; |
1379 | 0 | else |
1380 | 0 | s_provider = "none"; |
1381 | |
|
1382 | 0 | msg = apr_psprintf(r->pool, "%s\ncache: %s: %s\nprovider: metadata: %s\n", ok ? "OK" : "ERROR", |
1383 | 0 | cfg->cache.impl->name, ok ? "ok" : "fail", s_provider); |
1384 | |
|
1385 | 0 | return oidc_util_http_send(r, msg, _oidc_strlen(msg), "text/plain", ok ? OK : HTTP_SERVICE_UNAVAILABLE); |
1386 | 0 | } |
1387 | | |
1388 | | /* |
1389 | | * return status updates |
1390 | | */ |
1391 | 0 | static int oidc_metrics_handle_status(request_rec *r, const char *s_json) { |
1392 | 0 | const char *msg = "OK\n"; |
1393 | 0 | char *s_metric_param = NULL; |
1394 | 0 | char *s_server_param = NULL; |
1395 | 0 | char *s_name_param = NULL; |
1396 | 0 | char *s_value_param = NULL; |
1397 | 0 | oidc_json_t *json = NULL; |
1398 | 0 | const oidc_json_t *j_server = NULL; |
1399 | 0 | oidc_json_t *j_counters = NULL; |
1400 | 0 | oidc_json_t *j_counter = NULL; |
1401 | 0 | const oidc_json_t *j_value = NULL; |
1402 | |
|
1403 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_SERVER_PARAM, &s_server_param); |
1404 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_COUNTER_PARAM, &s_metric_param); |
1405 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_NAME_PARAM, &s_name_param); |
1406 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_VALUE_PARAM, &s_value_param); |
1407 | |
|
1408 | 0 | if (s_server_param == NULL) |
1409 | 0 | s_server_param = "localhost"; |
1410 | | |
1411 | | /* without a counter selector this is a readiness probe rather than a counter query */ |
1412 | 0 | if (s_metric_param == NULL) |
1413 | 0 | return oidc_metrics_status_readiness(r); |
1414 | | |
1415 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1416 | 0 | if (json == NULL) |
1417 | 0 | goto end; |
1418 | | |
1419 | 0 | j_server = oidc_json_object_get(json, s_server_param); |
1420 | 0 | if (j_server == NULL) |
1421 | 0 | goto end; |
1422 | | |
1423 | 0 | j_counters = oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS); |
1424 | 0 | if (j_counters == NULL) |
1425 | 0 | goto end; |
1426 | | |
1427 | 0 | j_counter = oidc_metrics_status_find_counter(r, j_counters, s_metric_param); |
1428 | 0 | if (j_counter == NULL) |
1429 | 0 | goto end; |
1430 | | |
1431 | 0 | j_value = oidc_metrics_status_select_value(j_counter, s_name_param, s_value_param); |
1432 | 0 | if (j_value) |
1433 | 0 | msg = apr_psprintf(r->pool, "OK: %s\n", _json_int2str(r->pool, oidc_json_integer_value(j_value))); |
1434 | |
|
1435 | 0 | end: |
1436 | |
|
1437 | 0 | if (json) |
1438 | 0 | oidc_json_decref(json); |
1439 | |
|
1440 | 0 | return oidc_util_http_send(r, msg, _oidc_strlen(msg), "text/plain", OK); |
1441 | 0 | } |
1442 | | |
1443 | | /* |
1444 | | * return the bucket table entry for a JSON bucket key |
1445 | | */ |
1446 | 0 | static const oidc_metrics_bucket_t *oidc_metrics_prometheus_bucket_get(const char *json_name) { |
1447 | 0 | for (int i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) |
1448 | 0 | if (_oidc_strcmp(_oidc_metric_buckets[i].name, json_name) == 0) |
1449 | 0 | return &_oidc_metric_buckets[i]; |
1450 | 0 | return NULL; |
1451 | 0 | } |
1452 | | |
1453 | 0 | #define OIDC_METRICS_PROMETHEUS_PREFIX "oidc" |
1454 | | |
1455 | | /* |
1456 | | * normalize a metric name to something that Prometheus accepts |
1457 | | */ |
1458 | 0 | static const char *oidc_metric_prometheus_normalize_name(apr_pool_t *pool, const char *name) { |
1459 | 0 | char *label = apr_psprintf(pool, "%s", name); |
1460 | 0 | for (size_t i = 0; i < _oidc_strlen(label); i++) |
1461 | 0 | if (apr_isalnum(label[i]) == 0) |
1462 | 0 | label[i] = '_'; |
1463 | 0 | return apr_psprintf(pool, "%s_%s", OIDC_METRICS_PROMETHEUS_PREFIX, label); |
1464 | 0 | } |
1465 | | |
1466 | | /* Escape a Prometheus text-format label value: backslash, quote and LF are special. */ |
1467 | 0 | static const char *oidc_metrics_prometheus_escape_label(apr_pool_t *pool, const char *value) { |
1468 | 0 | apr_size_t len = _oidc_strlen(value); |
1469 | 0 | char *result = apr_palloc(pool, (len * 2) + 1); |
1470 | 0 | char *dst = result; |
1471 | |
|
1472 | 0 | for (const char *src = value; *src != '\0'; src++) { |
1473 | 0 | switch (*src) { |
1474 | 0 | case '\\': |
1475 | 0 | case '"': |
1476 | 0 | *dst++ = '\\'; |
1477 | 0 | *dst++ = *src; |
1478 | 0 | break; |
1479 | 0 | case '\n': |
1480 | 0 | *dst++ = '\\'; |
1481 | 0 | *dst++ = 'n'; |
1482 | 0 | break; |
1483 | 0 | default: |
1484 | 0 | *dst++ = *src; |
1485 | 0 | break; |
1486 | 0 | } |
1487 | 0 | } |
1488 | 0 | *dst = '\0'; |
1489 | 0 | return result; |
1490 | 0 | } |
1491 | | |
1492 | 0 | #define OIDC_METRICS_PROMETHEUS_CONTENT_TYPE "text/plain; version=0.0.4" |
1493 | | |
1494 | 0 | #define OIDC_METRICS_PROMETHEUS_SERVER "server_name" |
1495 | 0 | #define OIDC_METRICS_PROMETHEUS_BUCKET "bucket" |
1496 | 0 | #define OIDC_METRICS_PROMETHEUS_VALUE "value" |
1497 | 0 | #define OIDC_METRICS_PROMETHEUS_NAME "name" |
1498 | | |
1499 | | // loop context for Prometheus output |
1500 | | typedef struct oidc_metric_prometheus_callback_ctx_t { |
1501 | | char *s_result; |
1502 | | apr_pool_t *pool; |
1503 | | } oidc_metric_prometheus_callback_ctx_t; |
1504 | | |
1505 | | /* |
1506 | | * append per-(name, value) label-pair lines for a single key under a server's counter |
1507 | | */ |
1508 | | static char *oidc_metrics_prometheus_counter_named(apr_pool_t *pool, char *s_text, const char *s_start, |
1509 | 0 | const char *s_key, oidc_json_t *j_value) { |
1510 | 0 | void *iter = oidc_json_object_iter(j_value); |
1511 | 0 | while (iter) { |
1512 | 0 | const char *s_value = oidc_json_object_iter_key(iter); |
1513 | 0 | s_text = |
1514 | 0 | apr_psprintf(pool, "%s%s,%s=\"%s\",%s=\"%s\"} %s\n", s_text, s_start, OIDC_METRICS_PROMETHEUS_NAME, |
1515 | 0 | oidc_metrics_prometheus_escape_label(pool, s_key), OIDC_METRICS_PROMETHEUS_VALUE, |
1516 | 0 | oidc_metrics_prometheus_escape_label(pool, s_value), |
1517 | 0 | _json_int2str(pool, oidc_json_integer_value(oidc_json_object_iter_value(iter)))); |
1518 | 0 | iter = oidc_json_object_iter_next(j_value, iter); |
1519 | 0 | } |
1520 | 0 | return s_text; |
1521 | 0 | } |
1522 | | |
1523 | | /* |
1524 | | * append per-key lines for one server's counter dict; integer entries are value-labeled, sub-dict |
1525 | | * entries are delegated to oidc_metrics_prometheus_counter_named for name+value labeling |
1526 | | */ |
1527 | | static char *oidc_metrics_prometheus_counter_keyed(apr_pool_t *pool, char *s_text, const char *s_start, |
1528 | 0 | oidc_json_t *j_counter) { |
1529 | 0 | void *iter = oidc_json_object_iter(j_counter); |
1530 | 0 | while (iter) { |
1531 | 0 | const char *s_key = oidc_json_object_iter_key(iter); |
1532 | 0 | oidc_json_t *j_value = oidc_json_object_iter_value(iter); |
1533 | 0 | if (oidc_json_is_integer(j_value)) |
1534 | 0 | s_text = |
1535 | 0 | apr_psprintf(pool, "%s%s,%s=\"%s\"} %s\n", s_text, s_start, OIDC_METRICS_PROMETHEUS_VALUE, |
1536 | 0 | oidc_metrics_prometheus_escape_label(pool, s_key), |
1537 | 0 | _json_int2str(pool, oidc_json_integer_value(j_value))); |
1538 | 0 | else |
1539 | 0 | s_text = oidc_metrics_prometheus_counter_named(pool, s_text, s_start, s_key, j_value); |
1540 | 0 | iter = oidc_json_object_iter_next(j_counter, iter); |
1541 | 0 | } |
1542 | 0 | return s_text; |
1543 | 0 | } |
1544 | | |
1545 | | /* |
1546 | | * loop function for converting counter metrics to Prometheus output |
1547 | | */ |
1548 | | static int oidc_metrics_prometheus_counters(oidc_metric_prometheus_callback_ctx_t *ctx, const char *key, |
1549 | 0 | oidc_json_t *value) { |
1550 | 0 | oidc_json_t *o_counter = value; |
1551 | 0 | const oidc_metrics_counter_info_t *info = _oidc_metrics_key2counter_info(key); |
1552 | 0 | const char *s_label = NULL; |
1553 | 0 | char *s_text = NULL; |
1554 | |
|
1555 | 0 | if (info == NULL) |
1556 | 0 | return 1; |
1557 | | |
1558 | 0 | s_label = oidc_metric_prometheus_normalize_name(ctx->pool, _oidc_metrics_counter_info2s(ctx->pool, info)); |
1559 | 0 | s_text = apr_psprintf(ctx->pool, "# HELP %s The number of %s.\n", s_label, info->desc); |
1560 | 0 | s_text = apr_psprintf(ctx->pool, "%s# TYPE %s counter\n", s_text, s_label); |
1561 | |
|
1562 | 0 | void *iter = oidc_json_object_iter(o_counter); |
1563 | 0 | while (iter) { |
1564 | 0 | const char *s_server = oidc_json_object_iter_key(iter); |
1565 | 0 | oidc_json_t *j_counter = oidc_json_object_iter_value(iter); |
1566 | 0 | const char *s_start = apr_psprintf(ctx->pool, "%s{%s=\"%s\"", s_label, OIDC_METRICS_PROMETHEUS_SERVER, |
1567 | 0 | oidc_metrics_prometheus_escape_label(ctx->pool, s_server)); |
1568 | 0 | if (oidc_json_is_integer(j_counter)) |
1569 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s} %s\n", s_text, s_start, |
1570 | 0 | _json_int2str(ctx->pool, oidc_json_integer_value(j_counter))); |
1571 | 0 | else |
1572 | 0 | s_text = oidc_metrics_prometheus_counter_keyed(ctx->pool, s_text, s_start, j_counter); |
1573 | 0 | iter = oidc_json_object_iter_next(o_counter, iter); |
1574 | 0 | } |
1575 | 0 | ctx->s_result = apr_pstrcat(ctx->pool, ctx->s_result, s_text, "\n", NULL); |
1576 | 0 | oidc_json_decref(o_counter); |
1577 | 0 | return 1; |
1578 | 0 | } |
1579 | | |
1580 | | /* |
1581 | | * loop function for converting timing metrics to Prometheus output |
1582 | | */ |
1583 | | |
1584 | | static int oidc_metrics_prometheus_timings(oidc_metric_prometheus_callback_ctx_t *ctx, const char *key, |
1585 | 0 | oidc_json_t *value) { |
1586 | 0 | const char *s_server = NULL; |
1587 | 0 | const char *s_key = NULL; |
1588 | 0 | const oidc_metrics_bucket_t *bucket = NULL; |
1589 | 0 | oidc_json_t *j_timing = NULL; |
1590 | 0 | const oidc_json_t *j_member = NULL; |
1591 | 0 | oidc_json_t *o_timer = value; |
1592 | 0 | oidc_json_int_t v = 0; |
1593 | 0 | const oidc_metrics_timing_info_t *info = _oidc_metrics_key2timing_info(key); |
1594 | 0 | const char *s_label = NULL; |
1595 | 0 | char *s_text = NULL; |
1596 | 0 | char *s_secs = NULL; |
1597 | |
|
1598 | 0 | if (info == NULL) |
1599 | 0 | return 1; |
1600 | | |
1601 | 0 | s_label = oidc_metric_prometheus_normalize_name(ctx->pool, _oidc_metrics_timing_info2s(ctx->pool, info)); |
1602 | | /* the unsuffixed millisecond family is deprecated as of 2.4.20.4 in favor of the |
1603 | | * Prometheus-idiomatic _seconds family emitted alongside it below, and will be |
1604 | | * removed in a future release */ |
1605 | 0 | s_text = apr_psprintf(ctx->pool, "# HELP %s A histogram of %s.\n", s_label, info->desc); |
1606 | 0 | s_text = apr_psprintf(ctx->pool, "%s# TYPE %s histogram\n", s_text, s_label); |
1607 | 0 | s_secs = apr_psprintf(ctx->pool, "# HELP %s_seconds A histogram of %s in seconds.\n", s_label, info->desc); |
1608 | 0 | s_secs = apr_psprintf(ctx->pool, "%s# TYPE %s_seconds histogram\n", s_secs, s_label); |
1609 | |
|
1610 | 0 | void *iter1 = oidc_json_object_iter(o_timer); |
1611 | 0 | while (iter1) { |
1612 | 0 | s_server = oidc_json_object_iter_key(iter1); |
1613 | 0 | const char *s_server_escaped = oidc_metrics_prometheus_escape_label(ctx->pool, s_server); |
1614 | 0 | j_timing = oidc_json_object_iter_value(iter1); |
1615 | 0 | void *iter3 = oidc_json_object_iter(j_timing); |
1616 | 0 | while (iter3) { |
1617 | 0 | s_key = oidc_json_object_iter_key(iter3); |
1618 | 0 | j_member = oidc_json_object_iter_value(iter3); |
1619 | 0 | v = oidc_json_integer_value(j_member); |
1620 | 0 | bucket = oidc_metrics_prometheus_bucket_get(s_key); |
1621 | 0 | if (bucket != NULL) { |
1622 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s_%s{%s,%s=\"%s\"} %s\n", s_text, s_label, |
1623 | 0 | OIDC_METRICS_PROMETHEUS_BUCKET, bucket->label, |
1624 | 0 | OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1625 | 0 | _json_int2str(ctx->pool, v)); |
1626 | 0 | s_secs = apr_psprintf(ctx->pool, "%s%s_seconds_%s{%s,%s=\"%s\"} %s\n", s_secs, s_label, |
1627 | 0 | OIDC_METRICS_PROMETHEUS_BUCKET, bucket->label_seconds, |
1628 | 0 | OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1629 | 0 | _json_int2str(ctx->pool, v)); |
1630 | 0 | } else if (_oidc_strcmp(s_key, OIDC_METRICS_SUM) == 0) { |
1631 | | /* the sum is kept in microseconds in shared memory: truncate to |
1632 | | * milliseconds for the legacy family, exact seconds for _seconds */ |
1633 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s_%s{%s=\"%s\"} %s\n", s_text, s_label, s_key, |
1634 | 0 | OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1635 | 0 | _json_int2str(ctx->pool, v / 1000)); |
1636 | 0 | s_secs = apr_psprintf(ctx->pool, "%s%s_seconds_%s{%s=\"%s\"} %.6f\n", s_secs, s_label, |
1637 | 0 | s_key, OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1638 | 0 | (double)v / 1000000.0); |
1639 | 0 | } else { |
1640 | 0 | s_text = apr_psprintf(ctx->pool, "%s%s_%s{%s=\"%s\"} %s\n", s_text, s_label, s_key, |
1641 | 0 | OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1642 | 0 | _json_int2str(ctx->pool, v)); |
1643 | 0 | s_secs = apr_psprintf(ctx->pool, "%s%s_seconds_%s{%s=\"%s\"} %s\n", s_secs, s_label, |
1644 | 0 | s_key, OIDC_METRICS_PROMETHEUS_SERVER, s_server_escaped, |
1645 | 0 | _json_int2str(ctx->pool, v)); |
1646 | 0 | } |
1647 | 0 | iter3 = oidc_json_object_iter_next(j_timing, iter3); |
1648 | 0 | } |
1649 | 0 | iter1 = oidc_json_object_iter_next(o_timer, iter1); |
1650 | 0 | } |
1651 | 0 | ctx->s_result = apr_pstrcat(ctx->pool, ctx->s_result, s_text, "\n", s_secs, "\n", NULL); |
1652 | 0 | oidc_json_decref(o_timer); |
1653 | 0 | return 1; |
1654 | 0 | } |
1655 | | |
1656 | | /* |
1657 | | * take a list of metrics from a server indexed list and add it to a type indexed list |
1658 | | */ |
1659 | 0 | static void oidc_metrics_prometheus_convert(apr_hash_t *hash, const char *server, oidc_json_t *list) { |
1660 | 0 | const char *type = NULL; |
1661 | 0 | oidc_json_t *src = NULL; |
1662 | 0 | oidc_json_t *dst = NULL; |
1663 | 0 | void *iter = oidc_json_object_iter(list); |
1664 | 0 | while (iter) { |
1665 | 0 | type = oidc_json_object_iter_key(iter); |
1666 | 0 | src = oidc_json_object_iter_value(iter); |
1667 | 0 | dst = (oidc_json_t *)apr_hash_get(hash, type, APR_HASH_KEY_STRING); |
1668 | 0 | if (dst) { |
1669 | 0 | oidc_json_object_set(dst, server, src); |
1670 | 0 | } else { |
1671 | 0 | dst = oidc_json_object(); |
1672 | 0 | oidc_json_object_set(dst, server, src); |
1673 | 0 | apr_hash_set(hash, type, APR_HASH_KEY_STRING, dst); |
1674 | 0 | } |
1675 | 0 | iter = oidc_json_object_iter_next(list, iter); |
1676 | 0 | } |
1677 | 0 | } |
1678 | | |
1679 | | /* |
1680 | | * generate output in Prometheus formatting |
1681 | | */ |
1682 | 0 | static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { |
1683 | 0 | oidc_json_t *json = NULL; |
1684 | 0 | const oidc_json_t *j_server = NULL; |
1685 | 0 | const char *s_server = NULL; |
1686 | 0 | apr_hash_t *t_counters = apr_hash_make(r->pool); |
1687 | 0 | apr_hash_t *t_timings = apr_hash_make(r->pool); |
1688 | 0 | apr_hash_index_t *hi = NULL; |
1689 | 0 | const char *name = NULL; |
1690 | 0 | void *value = NULL; |
1691 | | |
1692 | | /* start the output with the build info and the metrics subsystem's own health */ |
1693 | 0 | char *s_self = apr_psprintf( |
1694 | 0 | r->pool, |
1695 | 0 | "# HELP %s_build_info A metric with a constant '1' value labeled by the module build version.\n" |
1696 | 0 | "# TYPE %s_build_info gauge\n" |
1697 | 0 | "%s_build_info{version=\"%s\"} 1\n\n" |
1698 | 0 | "# HELP %s_metrics_flush_errors The number of metrics flushes dropped because the JSON data outgrew the " |
1699 | 0 | "shared memory segment.\n" |
1700 | 0 | "# TYPE %s_metrics_flush_errors counter\n" |
1701 | 0 | "%s_metrics_flush_errors %u\n\n" |
1702 | 0 | "# HELP %s_metrics_last_flush_timestamp_seconds The Unix time of the last completed metrics flush cycle in " |
1703 | 0 | "this server process.\n" |
1704 | 0 | "# TYPE %s_metrics_last_flush_timestamp_seconds gauge\n" |
1705 | 0 | "%s_metrics_last_flush_timestamp_seconds %u\n\n", |
1706 | 0 | OIDC_METRICS_PROMETHEUS_PREFIX, OIDC_METRICS_PROMETHEUS_PREFIX, OIDC_METRICS_PROMETHEUS_PREFIX, |
1707 | 0 | oidc_metrics_prometheus_escape_label(r->pool, NAMEVERSION), OIDC_METRICS_PROMETHEUS_PREFIX, |
1708 | 0 | OIDC_METRICS_PROMETHEUS_PREFIX, OIDC_METRICS_PROMETHEUS_PREFIX, |
1709 | 0 | apr_atomic_read32(&_oidc_metrics_shm_hdr()->flush_errors), OIDC_METRICS_PROMETHEUS_PREFIX, |
1710 | 0 | OIDC_METRICS_PROMETHEUS_PREFIX, OIDC_METRICS_PROMETHEUS_PREFIX, |
1711 | 0 | apr_atomic_read32(&_oidc_metrics_shm_hdr()->last_flush)); |
1712 | |
|
1713 | 0 | oidc_metric_prometheus_callback_ctx_t ctx = {s_self, r->pool}; |
1714 | 0 | void *iter = NULL; |
1715 | | |
1716 | | /* parse the metrics string to JSON */ |
1717 | 0 | json = oidc_metrics_json_parse_r(r, s_json); |
1718 | 0 | if (json == NULL) |
1719 | 0 | return OK; |
1720 | | |
1721 | 0 | iter = oidc_json_object_iter(json); |
1722 | 0 | while (iter) { |
1723 | 0 | s_server = oidc_json_object_iter_key(iter); |
1724 | 0 | j_server = oidc_json_object_iter_value(iter); |
1725 | 0 | oidc_metrics_prometheus_convert(t_counters, s_server, |
1726 | 0 | oidc_json_object_get(j_server, OIDC_METRICS_COUNTERS)); |
1727 | 0 | oidc_metrics_prometheus_convert(t_timings, s_server, |
1728 | 0 | oidc_json_object_get(j_server, OIDC_METRICS_TIMINGS)); |
1729 | 0 | iter = oidc_json_object_iter_next(json, iter); |
1730 | 0 | } |
1731 | |
|
1732 | 0 | for (hi = apr_hash_first(r->pool, t_counters); hi; hi = apr_hash_next(hi)) { |
1733 | 0 | apr_hash_this(hi, (const void **)&name, NULL, &value); |
1734 | 0 | oidc_metrics_prometheus_counters(&ctx, name, value); |
1735 | 0 | } |
1736 | |
|
1737 | 0 | for (hi = apr_hash_first(r->pool, t_timings); hi; hi = apr_hash_next(hi)) { |
1738 | 0 | apr_hash_this(hi, (const void **)&name, NULL, &value); |
1739 | 0 | oidc_metrics_prometheus_timings(&ctx, name, value); |
1740 | 0 | } |
1741 | |
|
1742 | 0 | oidc_json_decref(json); |
1743 | |
|
1744 | 0 | return oidc_util_http_send(r, ctx.s_result, _oidc_strlen(ctx.s_result), OIDC_METRICS_PROMETHEUS_CONTENT_TYPE, |
1745 | 0 | OK); |
1746 | 0 | } |
1747 | | |
1748 | | /* |
1749 | | * definitions for handler callbacks |
1750 | | */ |
1751 | | |
1752 | | typedef int (*oidc_metrics_handler_function_t)(request_rec *, const char *); |
1753 | | |
1754 | | // holder for output function callback context |
1755 | | typedef struct oidc_metrics_handler_t { |
1756 | | const char *format; |
1757 | | oidc_metrics_handler_function_t callback; |
1758 | | int reset; |
1759 | | } oidc_metrics_content_handler_t; |
1760 | | |
1761 | | // output handlers |
1762 | | // NB: no format resets the collected metrics by default (json did before 2.4.20.4); |
1763 | | // a caller that wants the pre-2.4.20.4 behavior passes reset=true explicitly |
1764 | | const oidc_metrics_content_handler_t _oidc_metrics_handlers[] = { |
1765 | | // first is default |
1766 | | {"prometheus", oidc_metrics_handle_prometheus, 0}, |
1767 | | {"json", oidc_metrics_handle_json, 0}, |
1768 | | {"internal", oidc_metrics_handle_internal, 0}, |
1769 | | {"status", oidc_metrics_handle_status, 0}, |
1770 | | }; |
1771 | | |
1772 | 0 | #define OIDC_CONTENT_HANDLER_MAX ((int)(sizeof(_oidc_metrics_handlers) / sizeof(oidc_metrics_content_handler_t))) |
1773 | | |
1774 | 0 | #define OIDC_METRICS_RESET_PARAM "reset" |
1775 | | |
1776 | | /* |
1777 | | * see if we are going to reset the cache after this |
1778 | | */ |
1779 | 0 | static int oidc_metric_reset(request_rec *r, int dvalue) { |
1780 | 0 | char *s_reset = NULL; |
1781 | 0 | int value = 0; |
1782 | |
|
1783 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_RESET_PARAM, &s_reset); |
1784 | |
|
1785 | 0 | if (s_reset == NULL) |
1786 | 0 | return dvalue; |
1787 | | |
1788 | 0 | if (_oidc_strnatcasecmp(s_reset, "true") == 0) |
1789 | 0 | value = 1; |
1790 | 0 | else if (_oidc_strnatcasecmp(s_reset, "false") == 0) |
1791 | 0 | value = 0; |
1792 | |
|
1793 | 0 | return value; |
1794 | 0 | } |
1795 | | |
1796 | 0 | #define OIDC_METRICS_FORMAT_PARAM "format" |
1797 | | |
1798 | | /* |
1799 | | * find the format handler |
1800 | | */ |
1801 | 0 | const oidc_metrics_content_handler_t *oidc_metrics_find_handler(request_rec *r) { |
1802 | 0 | const oidc_metrics_content_handler_t *handler = NULL; |
1803 | 0 | char *s_format = NULL; |
1804 | | |
1805 | | /* get the specified format */ |
1806 | 0 | oidc_util_url_parameter_get(r, OIDC_METRICS_FORMAT_PARAM, &s_format); |
1807 | |
|
1808 | 0 | if (s_format == NULL) |
1809 | 0 | return &_oidc_metrics_handlers[0]; |
1810 | | |
1811 | 0 | for (int i = 0; i < OIDC_CONTENT_HANDLER_MAX; i++) { |
1812 | 0 | if (_oidc_strcmp(s_format, _oidc_metrics_handlers[i].format) == 0) { |
1813 | 0 | handler = &_oidc_metrics_handlers[i]; |
1814 | 0 | break; |
1815 | 0 | } |
1816 | 0 | } |
1817 | |
|
1818 | 0 | if (handler == NULL) |
1819 | 0 | oidc_warn(r, "could not find a metrics handler for format: %s", s_format); |
1820 | |
|
1821 | 0 | return handler; |
1822 | 0 | } |
1823 | | |
1824 | | /* |
1825 | | * return the metrics to the caller and flush the storage |
1826 | | */ |
1827 | 0 | int oidc_metrics_handle_request(request_rec *r) { |
1828 | 0 | const char *s_json = NULL; |
1829 | 0 | const oidc_metrics_content_handler_t *handler = NULL; |
1830 | | |
1831 | | /* get the content handler for the format */ |
1832 | 0 | handler = oidc_metrics_find_handler(r); |
1833 | 0 | if (handler == NULL) |
1834 | 0 | return HTTP_NOT_FOUND; |
1835 | | |
1836 | | /* lock the global shared memory */ |
1837 | 0 | if (oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_global_mutex) == FALSE) |
1838 | 0 | return HTTP_INTERNAL_SERVER_ERROR; |
1839 | | |
1840 | | /* retrieve the JSON formatted metrics as a string; NB: on the request pool, since |
1841 | | * it is handed to the content handler below, after the mutex has been released */ |
1842 | 0 | s_json = _oidc_metrics_storage_get(r->server, r->pool); |
1843 | | |
1844 | | /* now that the metrics have been consumed, clear the shared memory segment */ |
1845 | 0 | if (oidc_metric_reset(r, handler->reset)) |
1846 | 0 | oidc_metrics_storage_reset(r->server, r->pool); |
1847 | | |
1848 | | /* unlock the global shared memory */ |
1849 | 0 | if (oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_global_mutex) == FALSE) |
1850 | 0 | return HTTP_INTERNAL_SERVER_ERROR; |
1851 | | |
1852 | | /* handle the specified format */ |
1853 | 0 | return handler->callback(r, s_json); |
1854 | 0 | } |