Coverage Report

Created: 2026-09-27 07:07

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