Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2025 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU Affero General Public License as published by the Free Software
7 : Foundation; either version 3, or (at your option) any later version.
8 :
9 : TALER is distributed in the hope that it will be useful, but WITHOUT ANY
10 : WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 : A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file taler-merchant-httpd_private-get-statistics-report-transactions.c
18 : * @brief implement GET /statistics-report/transactions
19 : * @author Christian Grothoff
20 : */
21 : #include "platform.h"
22 : #include "taler-merchant-httpd_private-get-statistics-report-transactions.h"
23 : #include <gnunet/gnunet_json_lib.h>
24 : #include <taler/taler_json_lib.h>
25 : #include <taler/taler_mhd_lib.h>
26 :
27 :
28 : /**
29 : * Closure for the detail_cb().
30 : */
31 : struct ResponseContext
32 : {
33 : /**
34 : * Format of the response we are to generate.
35 : */
36 : enum
37 : {
38 : RCF_JSON,
39 : RCF_PDF
40 : } format;
41 :
42 : /**
43 : * Stored in a DLL while suspended.
44 : */
45 : struct ResponseContext *next;
46 :
47 : /**
48 : * Stored in a DLL while suspended.
49 : */
50 : struct ResponseContext *prev;
51 :
52 : /**
53 : * Context for this request.
54 : */
55 : struct TMH_HandlerContext *hc;
56 :
57 : /**
58 : * Async context used to run Typst.
59 : */
60 : struct TALER_MHD_TypstContext *tc;
61 :
62 : /**
63 : * Response to return.
64 : */
65 : struct MHD_Response *response;
66 :
67 : /**
68 : * Time when we started processing the request.
69 : */
70 : struct GNUNET_TIME_Timestamp now;
71 :
72 : /**
73 : * Period of each bucket.
74 : */
75 : struct GNUNET_TIME_Relative period;
76 :
77 : /**
78 : * Granularity of the buckets. Matches @e period.
79 : */
80 : const char *granularity;
81 :
82 : /**
83 : * Number of buckets to return.
84 : */
85 : uint64_t count;
86 :
87 : /**
88 : * HTTP status to use with @e response.
89 : */
90 : unsigned int http_status;
91 :
92 : /**
93 : * Length of the @e labels array.
94 : */
95 : unsigned int labels_cnt;
96 :
97 : /**
98 : * Array of labels for the chart.
99 : */
100 : char **labels;
101 :
102 : /**
103 : * Data groups for the chart.
104 : */
105 : json_t *data_groups;
106 :
107 : };
108 :
109 :
110 : /**
111 : * DLL of requests awaiting Typst.
112 : */
113 : static struct ResponseContext *rctx_head;
114 :
115 : /**
116 : * DLL of requests awaiting Typst.
117 : */
118 : static struct ResponseContext *rctx_tail;
119 :
120 :
121 : void
122 15 : TMH_handler_statistic_report_transactions_cleanup ()
123 : {
124 : struct ResponseContext *rctx;
125 :
126 15 : while (NULL != (rctx = rctx_head))
127 : {
128 0 : GNUNET_CONTAINER_DLL_remove (rctx_head,
129 : rctx_tail,
130 : rctx);
131 0 : MHD_resume_connection (rctx->hc->connection);
132 : }
133 15 : }
134 :
135 :
136 : /**
137 : * Free resources from @a ctx
138 : *
139 : * @param[in] ctx the `struct ResponseContext` to clean up
140 : */
141 : static void
142 0 : free_rc (void *ctx)
143 : {
144 0 : struct ResponseContext *rctx = ctx;
145 :
146 0 : if (NULL != rctx->tc)
147 : {
148 0 : TALER_MHD_typst_cancel (rctx->tc);
149 0 : rctx->tc = NULL;
150 : }
151 0 : if (NULL != rctx->response)
152 : {
153 0 : MHD_destroy_response (rctx->response);
154 0 : rctx->response = NULL;
155 : }
156 0 : for (unsigned int i = 0; i<rctx->labels_cnt; i++)
157 0 : GNUNET_free (rctx->labels[i]);
158 0 : GNUNET_array_grow (rctx->labels,
159 : rctx->labels_cnt,
160 : 0);
161 0 : json_decref (rctx->data_groups);
162 0 : GNUNET_free (rctx);
163 0 : }
164 :
165 :
166 : /**
167 : * Function called with the result of a #TALER_MHD_typst() operation.
168 : *
169 : * @param cls closure
170 : * @param tr result of the operation
171 : */
172 : static void
173 0 : pdf_cb (void *cls,
174 : const struct TALER_MHD_TypstResponse *tr)
175 : {
176 0 : struct ResponseContext *rctx = cls;
177 :
178 0 : rctx->tc = NULL;
179 0 : GNUNET_CONTAINER_DLL_remove (rctx_head,
180 : rctx_tail,
181 : rctx);
182 0 : MHD_resume_connection (rctx->hc->connection);
183 0 : TALER_MHD_daemon_trigger ();
184 0 : if (TALER_EC_NONE != tr->ec)
185 : {
186 : rctx->http_status
187 0 : = TALER_ErrorCode_get_http_status (tr->ec);
188 : rctx->response
189 0 : = TALER_MHD_make_error (tr->ec,
190 0 : tr->details.hint);
191 0 : return;
192 : }
193 : rctx->http_status
194 0 : = MHD_HTTP_OK;
195 : rctx->response
196 0 : = TALER_MHD_response_from_pdf_file (tr->details.filename);
197 : }
198 :
199 :
200 : /**
201 : * Typically called by `lookup_statistics_amount_by_bucket2`.
202 : *
203 : * @param[in,out] cls our `struct ResponseContext` to update
204 : * @param bucket_start start time of the bucket
205 : * @param amounts_len the length of @a amounts array
206 : * @param amounts the cumulative amounts in the bucket
207 : */
208 : static void
209 0 : amount_by_bucket (void *cls,
210 : struct GNUNET_TIME_Timestamp bucket_start,
211 : unsigned int amounts_len,
212 : const struct TALER_Amount amounts[static amounts_len])
213 0 : {
214 0 : struct ResponseContext *rctx = cls;
215 : json_t *values;
216 :
217 0 : for (unsigned int i = 0; i<amounts_len; i++)
218 : {
219 0 : bool found = false;
220 :
221 0 : for (unsigned int j = 0; j<rctx->labels_cnt; j++)
222 : {
223 0 : if (0 == strcmp (amounts[i].currency,
224 0 : rctx->labels[j]))
225 : {
226 0 : found = true;
227 0 : break;
228 : }
229 : }
230 0 : if (! found)
231 : {
232 0 : GNUNET_array_append (rctx->labels,
233 : rctx->labels_cnt,
234 : GNUNET_strdup (amounts[i].currency));
235 : }
236 : }
237 :
238 0 : values = json_array ();
239 0 : GNUNET_assert (NULL != values);
240 0 : for (unsigned int i = 0; i<rctx->labels_cnt; i++)
241 : {
242 0 : const char *label = rctx->labels[i];
243 0 : double d = 0.0;
244 :
245 0 : for (unsigned int j = 0; j<amounts_len; j++)
246 : {
247 0 : const struct TALER_Amount *a = &amounts[j];
248 :
249 0 : if (0 != strcmp (amounts[j].currency,
250 : label))
251 0 : continue;
252 0 : d = a->value * 1.0
253 0 : + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE);
254 0 : break;
255 : } /* for all amounts */
256 0 : GNUNET_assert (0 ==
257 : json_array_append_new (values,
258 : json_real (d)));
259 : } /* for all labels */
260 :
261 : {
262 : json_t *dg;
263 :
264 0 : dg = GNUNET_JSON_PACK (
265 : GNUNET_JSON_pack_timestamp ("start_date",
266 : bucket_start),
267 : GNUNET_JSON_pack_array_steal ("values",
268 : values));
269 0 : GNUNET_assert (0 ==
270 : json_array_append_new (rctx->data_groups,
271 : dg));
272 :
273 : }
274 0 : }
275 :
276 :
277 : /**
278 : * Create the transaction volume report.
279 : *
280 : * @param[in,out] rctx request context to use
281 : * @param[in,out] charts JSON chart array to expand
282 : * @return #GNUNET_OK on success,
283 : * #GNUNET_NO to end with #MHD_YES,
284 : * #GNUNET_NO to end with #MHD_NO.
285 : */
286 : static enum GNUNET_GenericReturnValue
287 0 : make_transaction_volume_report (struct ResponseContext *rctx,
288 : json_t *charts)
289 : {
290 : // FIXME: this is from example-statistics, we probably want to hard-wire the stats from this endpoint!
291 0 : const char *bucket_name = "sales (before refunds)";
292 : enum GNUNET_DB_QueryStatus qs;
293 : json_t *chart;
294 : json_t *labels;
295 :
296 0 : rctx->data_groups = json_array ();
297 0 : GNUNET_assert (NULL != rctx->data_groups);
298 0 : qs = TMH_db->lookup_statistics_amount_by_bucket2 (
299 0 : TMH_db->cls,
300 0 : rctx->hc->instance->settings.id,
301 : bucket_name,
302 : rctx->granularity,
303 : rctx->count,
304 : &amount_by_bucket,
305 : rctx);
306 0 : if (0 > qs)
307 : {
308 0 : GNUNET_break (0);
309 : return (MHD_YES ==
310 0 : TALER_MHD_reply_with_error (
311 0 : rctx->hc->connection,
312 : MHD_HTTP_INTERNAL_SERVER_ERROR,
313 : TALER_EC_GENERIC_DB_FETCH_FAILED,
314 : "lookup_statistics_amount_by_bucket"))
315 0 : ? GNUNET_NO : GNUNET_SYSERR;
316 : }
317 :
318 0 : labels = json_array ();
319 0 : GNUNET_assert (NULL != labels);
320 0 : for (unsigned int i=0; i<rctx->labels_cnt; i++)
321 : {
322 0 : GNUNET_assert (0 ==
323 : json_array_append_new (labels,
324 : json_string (rctx->labels[i])));
325 0 : GNUNET_free (rctx->labels[i]);
326 : }
327 0 : GNUNET_array_grow (rctx->labels,
328 : rctx->labels_cnt,
329 : 0);
330 0 : chart = GNUNET_JSON_PACK (
331 : GNUNET_JSON_pack_string ("chart_name",
332 : "Sales volume"),
333 : GNUNET_JSON_pack_string ("y_label",
334 : "Sales"),
335 : GNUNET_JSON_pack_array_steal ("data_groups",
336 : rctx->data_groups),
337 : GNUNET_JSON_pack_array_steal ("labels",
338 : labels),
339 : GNUNET_JSON_pack_bool ("cumulative",
340 : false));
341 0 : rctx->data_groups = NULL;
342 0 : GNUNET_assert (0 ==
343 : json_array_append_new (charts,
344 : chart));
345 0 : return GNUNET_OK;
346 : }
347 :
348 :
349 : /**
350 : * Typically called by `lookup_statistics_counter_by_bucket2`.
351 : *
352 : * @param[in,out] cls our `struct ResponseContext` to update
353 : * @param bucket_start start time of the bucket
354 : * @param counters_len the length of @a cumulative_amounts
355 : * @param descriptions description for the counter in the bucket
356 : * @param counters the counters in the bucket
357 : */
358 : static void
359 0 : count_by_bucket (void *cls,
360 : struct GNUNET_TIME_Timestamp bucket_start,
361 : unsigned int counters_len,
362 : const char *descriptions[static counters_len],
363 : uint64_t counters[static counters_len])
364 0 : {
365 0 : struct ResponseContext *rctx = cls;
366 : json_t *values;
367 :
368 0 : for (unsigned int i = 0; i<counters_len; i++)
369 : {
370 0 : bool found = false;
371 :
372 0 : for (unsigned int j = 0; j<rctx->labels_cnt; j++)
373 : {
374 0 : if (0 == strcmp (descriptions[i],
375 0 : rctx->labels[j]))
376 : {
377 0 : found = true;
378 0 : break;
379 : }
380 : }
381 0 : if (! found)
382 : {
383 0 : GNUNET_array_append (rctx->labels,
384 : rctx->labels_cnt,
385 : GNUNET_strdup (descriptions[i]));
386 : }
387 : }
388 :
389 0 : values = json_array ();
390 0 : GNUNET_assert (NULL != values);
391 0 : for (unsigned int i = 0; i<rctx->labels_cnt; i++)
392 : {
393 0 : const char *label = rctx->labels[i];
394 0 : uint64_t v = 0;
395 :
396 0 : for (unsigned int j = 0; j<counters_len; j++)
397 : {
398 0 : if (0 != strcmp (descriptions[j],
399 : label))
400 0 : continue;
401 0 : v = counters[j];
402 0 : break;
403 : } /* for all amounts */
404 0 : GNUNET_assert (0 ==
405 : json_array_append_new (values,
406 : json_integer (v)));
407 : } /* for all labels */
408 :
409 : {
410 : json_t *dg;
411 :
412 0 : dg = GNUNET_JSON_PACK (
413 : GNUNET_JSON_pack_timestamp ("start_date",
414 : bucket_start),
415 : GNUNET_JSON_pack_array_steal ("values",
416 : values));
417 0 : GNUNET_assert (0 ==
418 : json_array_append_new (rctx->data_groups,
419 : dg));
420 :
421 : }
422 0 : }
423 :
424 :
425 : /**
426 : * Create the transaction count report.
427 : *
428 : * @param[in,out] rctx request context to use
429 : * @param[in,out] charts JSON chart array to expand
430 : * @return #GNUNET_OK on success,
431 : * #GNUNET_NO to end with #MHD_YES,
432 : * #GNUNET_NO to end with #MHD_NO.
433 : */
434 : static enum GNUNET_GenericReturnValue
435 0 : make_transaction_count_report (struct ResponseContext *rctx,
436 : json_t *charts)
437 : {
438 0 : const char *prefix = "transaction-state-";
439 : enum GNUNET_DB_QueryStatus qs;
440 : json_t *chart;
441 : json_t *labels;
442 :
443 0 : rctx->data_groups = json_array ();
444 0 : GNUNET_assert (NULL != rctx->data_groups);
445 0 : qs = TMH_db->lookup_statistics_counter_by_bucket2 (
446 0 : TMH_db->cls,
447 0 : rctx->hc->instance->settings.id,
448 : prefix, /* prefix to match against bucket name */
449 : rctx->granularity,
450 : rctx->count,
451 : &count_by_bucket,
452 : rctx);
453 0 : if (0 > qs)
454 : {
455 0 : GNUNET_break (0);
456 : return (MHD_YES ==
457 0 : TALER_MHD_reply_with_error (
458 0 : rctx->hc->connection,
459 : MHD_HTTP_INTERNAL_SERVER_ERROR,
460 : TALER_EC_GENERIC_DB_FETCH_FAILED,
461 : "lookup_statistics_XXX"))
462 0 : ? GNUNET_NO : GNUNET_SYSERR;
463 : }
464 :
465 0 : labels = json_array ();
466 0 : GNUNET_assert (NULL != labels);
467 0 : for (unsigned int i=0; i<rctx->labels_cnt; i++)
468 : {
469 0 : const char *label = rctx->labels[i];
470 :
471 : /* This condition should always hold. */
472 0 : if (0 ==
473 0 : strncmp (prefix,
474 : label,
475 : strlen (prefix)))
476 0 : label += strlen (prefix);
477 0 : GNUNET_assert (0 ==
478 : json_array_append_new (labels,
479 : json_string (label)));
480 0 : GNUNET_free (rctx->labels[i]);
481 : }
482 0 : GNUNET_array_grow (rctx->labels,
483 : rctx->labels_cnt,
484 : 0);
485 0 : chart = GNUNET_JSON_PACK (
486 : GNUNET_JSON_pack_string ("chart_name",
487 : "Transaction counts"),
488 : GNUNET_JSON_pack_string ("y_label",
489 : "Number of transactions"),
490 : GNUNET_JSON_pack_array_steal ("data_groups",
491 : rctx->data_groups),
492 : GNUNET_JSON_pack_array_steal ("labels",
493 : labels),
494 : GNUNET_JSON_pack_bool ("cumulative",
495 : false));
496 0 : rctx->data_groups = NULL;
497 0 : GNUNET_assert (0 ==
498 : json_array_append_new (charts,
499 : chart));
500 0 : return GNUNET_OK;
501 : }
502 :
503 :
504 : /**
505 : * Handle a GET "/private/statistics-report/transactions" request.
506 : *
507 : * @param rh context of the handler
508 : * @param connection the MHD connection to handle
509 : * @param[in,out] hc context with further information about the request
510 : * @return MHD result code
511 : */
512 : MHD_RESULT
513 0 : TMH_private_get_statistics_report_transactions (
514 : const struct TMH_RequestHandler *rh,
515 : struct MHD_Connection *connection,
516 : struct TMH_HandlerContext *hc)
517 : {
518 0 : struct ResponseContext *rctx = hc->ctx;
519 0 : struct TMH_MerchantInstance *mi = hc->instance;
520 : json_t *charts;
521 :
522 0 : if (NULL != rctx)
523 : {
524 0 : if (NULL == rctx->response)
525 : {
526 0 : GNUNET_break (0);
527 0 : return MHD_NO;
528 : }
529 0 : return MHD_queue_response (connection,
530 : rctx->http_status,
531 : rctx->response);
532 : }
533 0 : rctx = GNUNET_new (struct ResponseContext);
534 0 : rctx->hc = hc;
535 0 : rctx->now = GNUNET_TIME_timestamp_get ();
536 0 : hc->ctx = rctx;
537 0 : hc->cc = &free_rc;
538 0 : GNUNET_assert (NULL != mi);
539 :
540 0 : rctx->granularity = MHD_lookup_connection_value (connection,
541 : MHD_GET_ARGUMENT_KIND,
542 : "granularity");
543 0 : if (NULL == rctx->granularity)
544 : {
545 0 : rctx->granularity = "day";
546 0 : rctx->period = GNUNET_TIME_UNIT_DAYS;
547 0 : rctx->count = 95;
548 : }
549 : else
550 : {
551 : const struct
552 : {
553 : const char *name;
554 : struct GNUNET_TIME_Relative period;
555 : uint64_t default_counter;
556 0 : } map[] = {
557 : {
558 : .name = "second",
559 0 : .period = GNUNET_TIME_UNIT_SECONDS,
560 : .default_counter = 120,
561 : },
562 : {
563 : .name = "minute",
564 0 : .period = GNUNET_TIME_UNIT_MINUTES,
565 : .default_counter = 120,
566 : },
567 : {
568 : .name = "hour",
569 0 : .period = GNUNET_TIME_UNIT_HOURS,
570 : .default_counter = 48,
571 : },
572 : {
573 : .name = "day",
574 0 : .period = GNUNET_TIME_UNIT_DAYS,
575 : .default_counter = 95,
576 : },
577 : {
578 : .name = "month",
579 0 : .period = GNUNET_TIME_UNIT_MONTHS,
580 : .default_counter = 36,
581 : },
582 : {
583 : .name = "quarter",
584 0 : .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS,
585 : 3),
586 : .default_counter = 40,
587 : },
588 : {
589 : .name = "year",
590 0 : .period = GNUNET_TIME_UNIT_YEARS,
591 : .default_counter = 10
592 : },
593 : {
594 : .name = NULL
595 : }
596 : };
597 :
598 0 : rctx->count = 0;
599 0 : for (unsigned int i = 0; map[i].name != NULL; i++)
600 : {
601 0 : if (0 == strcasecmp (map[i].name,
602 : rctx->granularity))
603 : {
604 0 : rctx->count = map[i].default_counter;
605 0 : rctx->period = map[i].period;
606 0 : break;
607 : }
608 : }
609 0 : if (0 == rctx->count)
610 : {
611 0 : GNUNET_break_op (0);
612 0 : return TALER_MHD_reply_with_error (
613 : connection,
614 : MHD_HTTP_BAD_REQUEST,
615 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
616 : "granularity");
617 : }
618 : } /* end handling granularity */
619 :
620 : /* Figure out desired output format */
621 : {
622 : const char *mime;
623 :
624 0 : mime = MHD_lookup_connection_value (connection,
625 : MHD_HEADER_KIND,
626 : MHD_HTTP_HEADER_ACCEPT);
627 0 : if (NULL == mime)
628 0 : mime = "application/json";
629 0 : if (0 == strcmp (mime,
630 : "application/json"))
631 : {
632 0 : rctx->format = RCF_JSON;
633 : }
634 0 : else if (0 == strcmp (mime,
635 : "application/pdf"))
636 : {
637 :
638 0 : rctx->format = RCF_PDF;
639 : }
640 : else
641 : {
642 0 : GNUNET_break_op (0);
643 0 : return TALER_MHD_REPLY_JSON_PACK (
644 : connection,
645 : MHD_HTTP_NOT_ACCEPTABLE,
646 : GNUNET_JSON_pack_string ("hint",
647 : mime));
648 : }
649 : } /* end of determine output format */
650 :
651 0 : TALER_MHD_parse_request_number (connection,
652 : "count",
653 : &rctx->count);
654 :
655 : /* create charts */
656 0 : charts = json_array ();
657 0 : GNUNET_assert (NULL != charts);
658 : {
659 : enum GNUNET_GenericReturnValue ret;
660 :
661 0 : ret = make_transaction_volume_report (rctx,
662 : charts);
663 0 : if (GNUNET_OK != ret)
664 0 : return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
665 0 : ret = make_transaction_count_report (rctx,
666 : charts);
667 0 : if (GNUNET_OK != ret)
668 0 : return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
669 : }
670 :
671 : /* generate response */
672 : {
673 : struct GNUNET_TIME_Timestamp start_date;
674 : struct GNUNET_TIME_Timestamp end_date;
675 : json_t *root;
676 :
677 0 : end_date = rctx->now;
678 : start_date
679 0 : = GNUNET_TIME_absolute_to_timestamp (
680 : GNUNET_TIME_absolute_subtract (
681 : end_date.abs_time,
682 : GNUNET_TIME_relative_multiply (rctx->period,
683 0 : rctx->count)));
684 0 : root = GNUNET_JSON_PACK (
685 : GNUNET_JSON_pack_string ("business_name",
686 : mi->settings.name),
687 : GNUNET_JSON_pack_timestamp ("start_date",
688 : start_date),
689 : GNUNET_JSON_pack_timestamp ("end_date",
690 : end_date),
691 : GNUNET_JSON_pack_time_rel ("bucket_period",
692 : rctx->period),
693 : GNUNET_JSON_pack_array_steal ("charts",
694 : charts));
695 :
696 0 : switch (rctx->format)
697 : {
698 0 : case RCF_JSON:
699 0 : return TALER_MHD_reply_json (connection,
700 : root,
701 : MHD_HTTP_OK);
702 0 : case RCF_PDF:
703 : {
704 0 : struct TALER_MHD_TypstDocument doc = {
705 : .form_name = "transactions",
706 : .data = root
707 : };
708 :
709 0 : GNUNET_CONTAINER_DLL_insert (rctx_head,
710 : rctx_tail,
711 : rctx);
712 0 : MHD_suspend_connection (connection);
713 0 : rctx->tc = TALER_MHD_typst (TMH_cfg,
714 : false, /* remove on exit */
715 : "merchant",
716 : 1, /* one document, length of "array"! */
717 : &doc,
718 : &pdf_cb,
719 : rctx);
720 0 : json_decref (root);
721 0 : if (NULL == rctx->tc)
722 : {
723 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
724 : "Client requested PDF, but Typst is unavailable\n");
725 0 : return TALER_MHD_reply_with_error (
726 : connection,
727 : MHD_HTTP_NOT_IMPLEMENTED,
728 : TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK,
729 : NULL);
730 : }
731 0 : return MHD_YES;
732 : }
733 : } /* end switch */
734 : }
735 0 : GNUNET_assert (0);
736 : return MHD_NO;
737 : }
738 :
739 :
740 : /* end of taler-merchant-httpd_private-get-statistics-report-transactions.c */
|