Coverage Report

Created: 2026-11-12 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}