PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
dicom_metrics_collector.h
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
19#pragma once
20
22#include "../pacs_metrics.h"
23
24#include <array>
25#include <atomic>
26#include <chrono>
27#include <cstdint>
28#include <memory>
29#include <mutex>
30#include <string>
31#include <vector>
32
34
43 // Association metrics
44 std::uint64_t total_associations{0};
45 std::uint64_t active_associations{0};
46 std::uint64_t failed_associations{0};
47 std::uint64_t peak_active_associations{0};
48
49 // Transfer metrics
50 std::uint64_t images_sent{0};
51 std::uint64_t images_received{0};
52 std::uint64_t bytes_sent{0};
53 std::uint64_t bytes_received{0};
54
55 // Storage metrics
56 std::uint64_t store_operations{0};
57 std::uint64_t successful_stores{0};
58 std::uint64_t failed_stores{0};
60
61 // Query metrics
62 std::uint64_t query_operations{0};
63 std::uint64_t successful_queries{0};
64 std::uint64_t failed_queries{0};
66
67 // Timestamp
68 std::chrono::system_clock::time_point timestamp;
69};
70
109 : public dicom_collector_base<dicom_metrics_collector> {
110public:
112 static constexpr const char* collector_name = "dicom_metrics_collector";
113
114 // =========================================================================
115 // Construction
116 // =========================================================================
117
122
127 explicit dicom_metrics_collector(std::string ae_title);
128
132 ~dicom_metrics_collector() override = default;
133
134 // Non-copyable, non-movable
139
140 // =========================================================================
141 // CRTP Interface Implementation
142 // =========================================================================
143
154 bool do_initialize(const config_map& config);
155
160 std::vector<dicom_metric> do_collect();
161
166 [[nodiscard]] bool is_available() const;
167
172 [[nodiscard]] std::vector<std::string> do_get_metric_types() const;
173
178 void do_add_statistics(stats_map& stats) const;
179
180 // =========================================================================
181 // DICOM-Specific Methods
182 // =========================================================================
183
188 [[nodiscard]] dicom_metrics_snapshot get_snapshot() const;
189
194 [[nodiscard]] dicom_metrics_snapshot get_last_snapshot() const;
195
196 // =========================================================================
197 // Configuration
198 // =========================================================================
199
204 void set_collect_associations(bool enabled);
205
210 void set_collect_transfers(bool enabled);
211
216 void set_collect_storage(bool enabled);
217
222 void set_collect_queries(bool enabled);
223
228 void set_collect_pools(bool enabled);
229
230private:
231 // Configuration flags
236 bool collect_pools_{true};
237
238 // Cached snapshot
239 mutable std::mutex snapshot_mutex_;
241
242 // Helper methods for metric collection
243 void collect_association_metrics(std::vector<dicom_metric>& metrics);
244 void collect_transfer_metrics(std::vector<dicom_metric>& metrics);
245 void collect_storage_metrics(std::vector<dicom_metric>& metrics);
246 void collect_query_metrics(std::vector<dicom_metric>& metrics);
247 void collect_pool_metrics(std::vector<dicom_metric>& metrics);
249 std::vector<dicom_metric>& metrics,
250 const std::string& op_name,
252};
253
254// ─────────────────────────────────────────────────────────────────────────────
255// Inline Implementation
256// ─────────────────────────────────────────────────────────────────────────────
257
259 ae_title_ = std::move(ae_title);
260}
261
263 // Parse collection configuration
264 if (auto it = config.find("collect_associations"); it != config.end()) {
265 collect_associations_ = (it->second == "true" || it->second == "1");
266 }
267 if (auto it = config.find("collect_transfers"); it != config.end()) {
268 collect_transfers_ = (it->second == "true" || it->second == "1");
269 }
270 if (auto it = config.find("collect_storage"); it != config.end()) {
271 collect_storage_ = (it->second == "true" || it->second == "1");
272 }
273 if (auto it = config.find("collect_queries"); it != config.end()) {
274 collect_queries_ = (it->second == "true" || it->second == "1");
275 }
276 if (auto it = config.find("collect_pools"); it != config.end()) {
277 collect_pools_ = (it->second == "true" || it->second == "1");
278 }
279
280 return true;
281}
282
283inline std::vector<dicom_metric> dicom_metrics_collector::do_collect() {
284 std::vector<dicom_metric> metrics;
285 metrics.reserve(64); // Pre-allocate for typical metric count
286
289 }
290 if (collect_transfers_) {
292 }
293 if (collect_storage_) {
295 }
296 if (collect_queries_) {
297 collect_query_metrics(metrics);
298 }
299 if (collect_pools_) {
300 collect_pool_metrics(metrics);
301 }
302
303 return metrics;
304}
305
307 return true; // pacs_metrics singleton is always available
308}
309
310inline std::vector<std::string> dicom_metrics_collector::do_get_metric_types() const {
311 std::vector<std::string> types;
312
313 // Association metrics
314 types.push_back("dicom_associations_active");
315 types.push_back("dicom_associations_peak");
316 types.push_back("dicom_associations_total");
317 types.push_back("dicom_associations_rejected_total");
318 types.push_back("dicom_associations_aborted_total");
319
320 // Transfer metrics
321 types.push_back("dicom_images_sent_total");
322 types.push_back("dicom_images_received_total");
323 types.push_back("dicom_bytes_sent_total");
324 types.push_back("dicom_bytes_received_total");
325 types.push_back("dicom_transfer_rate_mbps");
326
327 // DIMSE operation metrics
328 static constexpr std::array<const char*, 11> ops = {
329 "c_echo", "c_store", "c_find", "c_move", "c_get",
330 "n_create", "n_set", "n_get", "n_action", "n_event", "n_delete"
331 };
332 for (const auto* op : ops) {
333 types.push_back(std::string("dicom_") + op + "_total");
334 types.push_back(std::string("dicom_") + op + "_success_total");
335 types.push_back(std::string("dicom_") + op + "_failure_total");
336 types.push_back(std::string("dicom_") + op + "_duration_seconds");
337 }
338
339 // Pool metrics
340 types.push_back("dicom_element_pool_hits");
341 types.push_back("dicom_element_pool_misses");
342 types.push_back("dicom_dataset_pool_hits");
343 types.push_back("dicom_dataset_pool_misses");
344
345 return types;
346}
347
349 std::lock_guard<std::mutex> lock(snapshot_mutex_);
350
351 stats["active_associations"] =
352 static_cast<double>(last_snapshot_.active_associations);
353 stats["total_images_processed"] =
355 stats["total_bytes_transferred"] =
357}
358
360 dicom_metrics_snapshot snapshot;
361 snapshot.timestamp = std::chrono::system_clock::now();
362
363 const auto& pacs = pacs_metrics::global_metrics();
364 const auto& assoc = pacs.associations();
365 const auto& transfer = pacs.transfer();
366
367 // Association metrics
368 snapshot.total_associations =
369 assoc.total_established.load(std::memory_order_relaxed);
370 snapshot.active_associations =
371 assoc.current_active.load(std::memory_order_relaxed);
372 snapshot.failed_associations =
373 assoc.total_rejected.load(std::memory_order_relaxed) +
374 assoc.total_aborted.load(std::memory_order_relaxed);
375 snapshot.peak_active_associations =
376 assoc.peak_active.load(std::memory_order_relaxed);
377
378 // Transfer metrics
379 snapshot.images_sent = 0; // Not tracked separately
380 snapshot.images_received =
381 transfer.images_stored.load(std::memory_order_relaxed);
382 snapshot.bytes_sent =
383 transfer.bytes_sent.load(std::memory_order_relaxed);
384 snapshot.bytes_received =
385 transfer.bytes_received.load(std::memory_order_relaxed);
386
387 // Storage metrics (from C-STORE counter)
388 const auto& store = pacs.get_counter(dimse_operation::c_store);
389 snapshot.store_operations = store.total_count();
390 snapshot.successful_stores =
391 store.success_count.load(std::memory_order_relaxed);
392 snapshot.failed_stores =
393 store.failure_count.load(std::memory_order_relaxed);
394 snapshot.avg_store_latency_ms =
395 static_cast<double>(store.average_duration_us()) / 1000.0;
396
397 // Query metrics (from C-FIND counter)
398 const auto& query = pacs.get_counter(dimse_operation::c_find);
399 snapshot.query_operations = query.total_count();
400 snapshot.successful_queries =
401 query.success_count.load(std::memory_order_relaxed);
402 snapshot.failed_queries =
403 query.failure_count.load(std::memory_order_relaxed);
404 snapshot.avg_query_latency_ms =
405 static_cast<double>(query.average_duration_us()) / 1000.0;
406
407 return snapshot;
408}
409
411 std::lock_guard<std::mutex> lock(snapshot_mutex_);
412 return last_snapshot_;
413}
414
416 collect_associations_ = enabled;
417}
418
420 collect_transfers_ = enabled;
421}
422
424 collect_storage_ = enabled;
425}
426
428 collect_queries_ = enabled;
429}
430
432 collect_pools_ = enabled;
433}
434
436 std::vector<dicom_metric>& metrics) {
437
438 const auto& assoc = pacs_metrics::global_metrics().associations();
439
440 // Active associations (gauge)
441 metrics.push_back(create_base_metric(
442 "dicom_associations_active",
443 static_cast<double>(assoc.current_active.load(std::memory_order_relaxed)),
444 "gauge"));
445
446 // Peak active associations (gauge)
447 metrics.push_back(create_base_metric(
448 "dicom_associations_peak",
449 static_cast<double>(assoc.peak_active.load(std::memory_order_relaxed)),
450 "gauge"));
451
452 // Total established (counter)
453 metrics.push_back(create_base_metric(
454 "dicom_associations_total",
455 static_cast<double>(assoc.total_established.load(std::memory_order_relaxed)),
456 "counter"));
457
458 // Rejected (counter)
459 metrics.push_back(create_base_metric(
460 "dicom_associations_rejected_total",
461 static_cast<double>(assoc.total_rejected.load(std::memory_order_relaxed)),
462 "counter"));
463
464 // Aborted (counter)
465 metrics.push_back(create_base_metric(
466 "dicom_associations_aborted_total",
467 static_cast<double>(assoc.total_aborted.load(std::memory_order_relaxed)),
468 "counter"));
469
470 // Success rate (gauge)
471 const auto total = assoc.total_established.load(std::memory_order_relaxed);
472 const auto rejected = assoc.total_rejected.load(std::memory_order_relaxed);
473 const auto attempted = total + rejected;
474 const double success_rate = (attempted > 0)
475 ? static_cast<double>(total) / static_cast<double>(attempted)
476 : 1.0;
477
478 metrics.push_back(create_base_metric(
479 "dicom_associations_success_rate",
480 success_rate,
481 "gauge"));
482}
483
485 std::vector<dicom_metric>& metrics) {
486
487 const auto& transfer = pacs_metrics::global_metrics().transfer();
488
489 // Images stored (counter)
490 metrics.push_back(create_base_metric(
491 "dicom_images_stored_total",
492 static_cast<double>(transfer.images_stored.load(std::memory_order_relaxed)),
493 "counter"));
494
495 // Images retrieved (counter)
496 metrics.push_back(create_base_metric(
497 "dicom_images_retrieved_total",
498 static_cast<double>(transfer.images_retrieved.load(std::memory_order_relaxed)),
499 "counter"));
500
501 // Bytes sent (counter)
502 metrics.push_back(create_base_metric(
503 "dicom_bytes_sent_total",
504 static_cast<double>(transfer.bytes_sent.load(std::memory_order_relaxed)),
505 "counter"));
506
507 // Bytes received (counter)
508 metrics.push_back(create_base_metric(
509 "dicom_bytes_received_total",
510 static_cast<double>(transfer.bytes_received.load(std::memory_order_relaxed)),
511 "counter"));
512}
513
515 std::vector<dicom_metric>& metrics) {
516
517 const auto& pacs = pacs_metrics::global_metrics();
518
519 // C-STORE operation metrics
521 metrics, "c_store", pacs.get_counter(dimse_operation::c_store));
522}
523
525 std::vector<dicom_metric>& metrics) {
526
527 const auto& pacs = pacs_metrics::global_metrics();
528
529 // All query-related DIMSE operations
531 metrics, "c_echo", pacs.get_counter(dimse_operation::c_echo));
533 metrics, "c_find", pacs.get_counter(dimse_operation::c_find));
535 metrics, "c_move", pacs.get_counter(dimse_operation::c_move));
537 metrics, "c_get", pacs.get_counter(dimse_operation::c_get));
538
539 // N-services
541 metrics, "n_create", pacs.get_counter(dimse_operation::n_create));
543 metrics, "n_set", pacs.get_counter(dimse_operation::n_set));
545 metrics, "n_get", pacs.get_counter(dimse_operation::n_get));
547 metrics, "n_action", pacs.get_counter(dimse_operation::n_action));
549 metrics, "n_event", pacs.get_counter(dimse_operation::n_event));
551 metrics, "n_delete", pacs.get_counter(dimse_operation::n_delete));
552}
553
555 std::vector<dicom_metric>& metrics) {
556
557 const auto& pacs = pacs_metrics::global_metrics();
558
559 // Element pool
560 const auto& elem_pool = pacs.element_pool();
561 metrics.push_back(create_base_metric(
562 "dicom_element_pool_acquisitions_total",
563 static_cast<double>(elem_pool.total_acquisitions.load(std::memory_order_relaxed)),
564 "counter",
565 {{"pool", "element"}}));
566 metrics.push_back(create_base_metric(
567 "dicom_element_pool_hits_total",
568 static_cast<double>(elem_pool.pool_hits.load(std::memory_order_relaxed)),
569 "counter",
570 {{"pool", "element"}}));
571 metrics.push_back(create_base_metric(
572 "dicom_element_pool_misses_total",
573 static_cast<double>(elem_pool.pool_misses.load(std::memory_order_relaxed)),
574 "counter",
575 {{"pool", "element"}}));
576 metrics.push_back(create_base_metric(
577 "dicom_element_pool_hit_ratio",
578 elem_pool.hit_ratio(),
579 "gauge",
580 {{"pool", "element"}}));
581
582 // Dataset pool
583 const auto& ds_pool = pacs.dataset_pool();
584 metrics.push_back(create_base_metric(
585 "dicom_dataset_pool_acquisitions_total",
586 static_cast<double>(ds_pool.total_acquisitions.load(std::memory_order_relaxed)),
587 "counter",
588 {{"pool", "dataset"}}));
589 metrics.push_back(create_base_metric(
590 "dicom_dataset_pool_hits_total",
591 static_cast<double>(ds_pool.pool_hits.load(std::memory_order_relaxed)),
592 "counter",
593 {{"pool", "dataset"}}));
594 metrics.push_back(create_base_metric(
595 "dicom_dataset_pool_misses_total",
596 static_cast<double>(ds_pool.pool_misses.load(std::memory_order_relaxed)),
597 "counter",
598 {{"pool", "dataset"}}));
599 metrics.push_back(create_base_metric(
600 "dicom_dataset_pool_hit_ratio",
601 ds_pool.hit_ratio(),
602 "gauge",
603 {{"pool", "dataset"}}));
604
605 // PDU buffer pool
606 const auto& pdu_pool = pacs.pdu_buffer_pool();
607 metrics.push_back(create_base_metric(
608 "dicom_pdu_buffer_pool_acquisitions_total",
609 static_cast<double>(pdu_pool.total_acquisitions.load(std::memory_order_relaxed)),
610 "counter",
611 {{"pool", "pdu_buffer"}}));
612 metrics.push_back(create_base_metric(
613 "dicom_pdu_buffer_pool_hits_total",
614 static_cast<double>(pdu_pool.pool_hits.load(std::memory_order_relaxed)),
615 "counter",
616 {{"pool", "pdu_buffer"}}));
617 metrics.push_back(create_base_metric(
618 "dicom_pdu_buffer_pool_misses_total",
619 static_cast<double>(pdu_pool.pool_misses.load(std::memory_order_relaxed)),
620 "counter",
621 {{"pool", "pdu_buffer"}}));
622 metrics.push_back(create_base_metric(
623 "dicom_pdu_buffer_pool_hit_ratio",
624 pdu_pool.hit_ratio(),
625 "gauge",
626 {{"pool", "pdu_buffer"}}));
627}
628
629inline void dicom_metrics_collector::collect_dimse_operation_metrics(
630 std::vector<dicom_metric>& metrics,
631 const std::string& op_name,
632 const operation_counter& counter) {
633
634 std::unordered_map<std::string, std::string> op_tags{{"operation", op_name}};
635
636 // Total requests (counter)
637 metrics.push_back(create_base_metric(
638 "dicom_" + op_name + "_total",
639 static_cast<double>(counter.total_count()),
640 "counter",
641 op_tags));
642
643 // Successful requests (counter)
644 metrics.push_back(create_base_metric(
645 "dicom_" + op_name + "_success_total",
646 static_cast<double>(counter.success_count.load(std::memory_order_relaxed)),
647 "counter",
648 op_tags));
649
650 // Failed requests (counter)
651 metrics.push_back(create_base_metric(
652 "dicom_" + op_name + "_failure_total",
653 static_cast<double>(counter.failure_count.load(std::memory_order_relaxed)),
654 "counter",
655 op_tags));
656
657 // Duration metrics (only if there have been requests)
658 if (counter.total_count() > 0) {
659 // Average duration in seconds (gauge)
660 metrics.push_back(create_base_metric(
661 "dicom_" + op_name + "_duration_seconds_avg",
662 static_cast<double>(counter.average_duration_us()) / 1'000'000.0,
663 "gauge",
664 op_tags));
665
666 // Total duration in seconds (counter) - for rate calculations
667 metrics.push_back(create_base_metric(
668 "dicom_" + op_name + "_duration_seconds_sum",
669 static_cast<double>(counter.total_duration_us.load(std::memory_order_relaxed)) / 1'000'000.0,
670 "counter",
671 op_tags));
672
673 // Min duration (gauge)
674 const auto min_us = counter.min_duration_us.load(std::memory_order_relaxed);
675 if (min_us != UINT64_MAX) {
676 metrics.push_back(create_base_metric(
677 "dicom_" + op_name + "_duration_seconds_min",
678 static_cast<double>(min_us) / 1'000'000.0,
679 "gauge",
680 op_tags));
681 }
682
683 // Max duration (gauge)
684 const auto max_us = counter.max_duration_us.load(std::memory_order_relaxed);
685 if (max_us > 0) {
686 metrics.push_back(create_base_metric(
687 "dicom_" + op_name + "_duration_seconds_max",
688 static_cast<double>(max_us) / 1'000'000.0,
689 "gauge",
690 op_tags));
691 }
692 }
693}
694
695} // namespace kcenon::pacs::monitoring
CRTP base class for DICOM metric collectors.
dicom_metric create_base_metric(const std::string &name, double value, const std::string &type, const std::unordered_map< std::string, std::string > &extra_tags={}) const
CRTP-based unified DICOM metrics collector.
void set_collect_storage(bool enabled)
Enable or disable storage metrics collection.
dicom_metrics_collector(dicom_metrics_collector &&)=delete
dicom_metrics_collector()=default
Default constructor.
void set_collect_pools(bool enabled)
Enable or disable pool metrics collection.
void do_add_statistics(stats_map &stats) const
Add collector-specific statistics.
void collect_query_metrics(std::vector< dicom_metric > &metrics)
void collect_transfer_metrics(std::vector< dicom_metric > &metrics)
dicom_metrics_snapshot get_last_snapshot() const
Get last collected snapshot.
dicom_metrics_collector & operator=(const dicom_metrics_collector &)=delete
void set_collect_queries(bool enabled)
Enable or disable query metrics collection.
void collect_storage_metrics(std::vector< dicom_metric > &metrics)
dicom_metrics_snapshot get_snapshot() const
Get a snapshot of current metrics.
static constexpr const char * collector_name
Collector name for CRTP base class.
dicom_metrics_collector(const dicom_metrics_collector &)=delete
void collect_dimse_operation_metrics(std::vector< dicom_metric > &metrics, const std::string &op_name, const operation_counter &counter)
void set_collect_associations(bool enabled)
Enable or disable association metrics collection.
bool do_initialize(const config_map &config)
Collector-specific initialization.
void collect_association_metrics(std::vector< dicom_metric > &metrics)
bool is_available() const
Check if DICOM metrics collection is available.
std::vector< std::string > do_get_metric_types() const
Get supported metric types.
void set_collect_transfers(bool enabled)
Enable or disable transfer metrics collection.
void collect_pool_metrics(std::vector< dicom_metric > &metrics)
std::vector< dicom_metric > do_collect()
Collect all DICOM metrics.
~dicom_metrics_collector() override=default
Destructor.
dicom_metrics_collector & operator=(dicom_metrics_collector &&)=delete
const data_transfer_metrics & transfer() const noexcept
Get data transfer metrics.
static pacs_metrics & global_metrics() noexcept
Get the global singleton instance.
const association_counters & associations() const noexcept
Get association counters.
CRTP base class for DICOM metrics collectors.
@ c_store
C-STORE (Storage Service)
@ c_move
C-MOVE (Retrieve Service)
@ c_echo
C-ECHO (Verification Service)
std::unordered_map< std::string, double > stats_map
Type alias for statistics map.
@ counter
Monotonic increasing value.
std::unordered_map< std::string, std::string > config_map
Type alias for configuration map.
Operation metrics collection for PACS DICOM services.
Snapshot of all DICOM metrics at a point in time.
std::chrono::system_clock::time_point timestamp
Atomic counter for tracking operation success/failure counts.