PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
pacs_metrics.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
21#include <atomic>
22#include <chrono>
23#include <cstdint>
24#include <memory>
25#include <mutex>
26#include <shared_mutex>
27#include <sstream>
28#include <string>
29#include <string_view>
30
32
40enum class dimse_operation {
41 c_echo,
42 c_store,
43 c_find,
44 c_move,
45 c_get,
46 n_create,
47 n_set,
48 n_get,
49 n_action,
50 n_event,
52};
53
59[[nodiscard]] constexpr std::string_view to_string(dimse_operation op) noexcept {
60 switch (op) {
62 return "c_echo";
64 return "c_store";
66 return "c_find";
68 return "c_move";
70 return "c_get";
72 return "n_create";
74 return "n_set";
76 return "n_get";
78 return "n_action";
80 return "n_event";
82 return "n_delete";
83 default:
84 return "unknown";
85 }
86}
87
96 std::atomic<std::uint64_t> success_count{0};
97 std::atomic<std::uint64_t> failure_count{0};
98 std::atomic<std::uint64_t> total_duration_us{0};
99 std::atomic<std::uint64_t> min_duration_us{UINT64_MAX};
100 std::atomic<std::uint64_t> max_duration_us{0};
101
103 [[nodiscard]] std::uint64_t total_count() const noexcept {
104 return success_count.load(std::memory_order_relaxed) +
105 failure_count.load(std::memory_order_relaxed);
106 }
107
109 [[nodiscard]] std::uint64_t average_duration_us() const noexcept {
110 const auto total = total_count();
111 if (total == 0) {
112 return 0;
113 }
114 return total_duration_us.load(std::memory_order_relaxed) / total;
115 }
116
118 void record_success(std::chrono::microseconds duration) noexcept {
119 success_count.fetch_add(1, std::memory_order_relaxed);
120 const auto duration_us = static_cast<std::uint64_t>(duration.count());
121 total_duration_us.fetch_add(duration_us, std::memory_order_relaxed);
122
123 // Update min/max with CAS loops
124 auto current_min = min_duration_us.load(std::memory_order_relaxed);
125 while (duration_us < current_min &&
126 !min_duration_us.compare_exchange_weak(current_min, duration_us,
127 std::memory_order_relaxed)) {
128 // Loop until successful
129 }
130
131 auto current_max = max_duration_us.load(std::memory_order_relaxed);
132 while (duration_us > current_max &&
133 !max_duration_us.compare_exchange_weak(current_max, duration_us,
134 std::memory_order_relaxed)) {
135 // Loop until successful
136 }
137 }
138
140 void record_failure(std::chrono::microseconds duration) noexcept {
141 failure_count.fetch_add(1, std::memory_order_relaxed);
142 const auto duration_us = static_cast<std::uint64_t>(duration.count());
143 total_duration_us.fetch_add(duration_us, std::memory_order_relaxed);
144
145 // Update min/max with CAS loops
146 auto current_min = min_duration_us.load(std::memory_order_relaxed);
147 while (duration_us < current_min &&
148 !min_duration_us.compare_exchange_weak(current_min, duration_us,
149 std::memory_order_relaxed)) {
150 }
151
152 auto current_max = max_duration_us.load(std::memory_order_relaxed);
153 while (duration_us > current_max &&
154 !max_duration_us.compare_exchange_weak(current_max, duration_us,
155 std::memory_order_relaxed)) {
156 }
157 }
158
160 void reset() noexcept {
161 success_count.store(0, std::memory_order_relaxed);
162 failure_count.store(0, std::memory_order_relaxed);
163 total_duration_us.store(0, std::memory_order_relaxed);
164 min_duration_us.store(UINT64_MAX, std::memory_order_relaxed);
165 max_duration_us.store(0, std::memory_order_relaxed);
166 }
167};
168
176 std::atomic<std::uint64_t> bytes_sent{0};
177 std::atomic<std::uint64_t> bytes_received{0};
178 std::atomic<std::uint64_t> images_stored{0};
179 std::atomic<std::uint64_t> images_retrieved{0};
180
182 void add_bytes_sent(std::uint64_t bytes) noexcept {
183 bytes_sent.fetch_add(bytes, std::memory_order_relaxed);
184 }
185
187 void add_bytes_received(std::uint64_t bytes) noexcept {
188 bytes_received.fetch_add(bytes, std::memory_order_relaxed);
189 }
190
192 void increment_images_stored() noexcept {
193 images_stored.fetch_add(1, std::memory_order_relaxed);
194 }
195
198 images_retrieved.fetch_add(1, std::memory_order_relaxed);
199 }
200
202 void reset() noexcept {
203 bytes_sent.store(0, std::memory_order_relaxed);
204 bytes_received.store(0, std::memory_order_relaxed);
205 images_stored.store(0, std::memory_order_relaxed);
206 images_retrieved.store(0, std::memory_order_relaxed);
207 }
208};
209
218 std::atomic<std::uint64_t> total_established{0};
219 std::atomic<std::uint64_t> total_rejected{0};
220 std::atomic<std::uint64_t> total_aborted{0};
221 std::atomic<std::uint32_t> current_active{0};
222 std::atomic<std::uint32_t> peak_active{0};
223
225 void record_established() noexcept {
226 total_established.fetch_add(1, std::memory_order_relaxed);
227 const auto active = current_active.fetch_add(1, std::memory_order_relaxed) + 1;
228
229 // Update peak with CAS loop
230 auto current_peak = peak_active.load(std::memory_order_relaxed);
231 while (active > current_peak &&
232 !peak_active.compare_exchange_weak(current_peak, active,
233 std::memory_order_relaxed)) {
234 }
235 }
236
238 void record_released() noexcept {
239 const auto prev = current_active.load(std::memory_order_relaxed);
240 if (prev > 0) {
241 current_active.fetch_sub(1, std::memory_order_relaxed);
242 }
243 }
244
246 void record_rejected() noexcept {
247 total_rejected.fetch_add(1, std::memory_order_relaxed);
248 }
249
251 void record_aborted() noexcept {
252 total_aborted.fetch_add(1, std::memory_order_relaxed);
253 const auto prev = current_active.load(std::memory_order_relaxed);
254 if (prev > 0) {
255 current_active.fetch_sub(1, std::memory_order_relaxed);
256 }
257 }
258
260 void reset() noexcept {
261 total_established.store(0, std::memory_order_relaxed);
262 total_rejected.store(0, std::memory_order_relaxed);
263 total_aborted.store(0, std::memory_order_relaxed);
264 current_active.store(0, std::memory_order_relaxed);
265 peak_active.store(0, std::memory_order_relaxed);
266 }
267};
268
279 std::atomic<std::uint64_t> total_acquisitions{0};
280 std::atomic<std::uint64_t> pool_hits{0};
281 std::atomic<std::uint64_t> pool_misses{0};
282 std::atomic<std::uint64_t> total_releases{0};
283 std::atomic<std::uint32_t> current_pool_size{0};
284
286 [[nodiscard]] double hit_ratio() const noexcept {
287 const auto total = total_acquisitions.load(std::memory_order_relaxed);
288 if (total == 0) {
289 return 0.0;
290 }
291 return static_cast<double>(pool_hits.load(std::memory_order_relaxed))
292 / static_cast<double>(total);
293 }
294
296 void record_acquisition(bool was_pool_hit) noexcept {
297 total_acquisitions.fetch_add(1, std::memory_order_relaxed);
298 if (was_pool_hit) {
299 pool_hits.fetch_add(1, std::memory_order_relaxed);
300 } else {
301 pool_misses.fetch_add(1, std::memory_order_relaxed);
302 }
303 }
304
306 void record_release() noexcept {
307 total_releases.fetch_add(1, std::memory_order_relaxed);
308 }
309
311 void set_pool_size(std::uint32_t size) noexcept {
312 current_pool_size.store(size, std::memory_order_relaxed);
313 }
314
316 void reset() noexcept {
317 total_acquisitions.store(0, std::memory_order_relaxed);
318 pool_hits.store(0, std::memory_order_relaxed);
319 pool_misses.store(0, std::memory_order_relaxed);
320 total_releases.store(0, std::memory_order_relaxed);
321 current_pool_size.store(0, std::memory_order_relaxed);
322 }
323};
324
355public:
356 // =========================================================================
357 // Construction and Global Access
358 // =========================================================================
359
363 pacs_metrics() = default;
364
371 [[nodiscard]] static pacs_metrics& global_metrics() noexcept {
372 static pacs_metrics instance;
373 return instance;
374 }
375
377 pacs_metrics(const pacs_metrics&) = delete;
379
383
384 // =========================================================================
385 // DIMSE Operation Recording
386 // =========================================================================
387
394 void record_store(bool success,
395 std::chrono::microseconds duration,
396 std::uint64_t bytes_stored = 0) noexcept {
397 if (success) {
398 c_store_.record_success(duration);
399 if (bytes_stored > 0) {
400 transfer_.add_bytes_received(bytes_stored);
402 }
403 } else {
404 c_store_.record_failure(duration);
405 }
406 }
407
414 void record_query(bool success,
415 std::chrono::microseconds duration,
416 [[maybe_unused]] std::uint32_t matches = 0) noexcept {
417 if (success) {
418 c_find_.record_success(duration);
419 } else {
420 c_find_.record_failure(duration);
421 }
422 }
423
429 void record_echo(bool success, std::chrono::microseconds duration) noexcept {
430 if (success) {
431 c_echo_.record_success(duration);
432 } else {
433 c_echo_.record_failure(duration);
434 }
435 }
436
443 void record_move(bool success,
444 std::chrono::microseconds duration,
445 std::uint32_t images_moved = 0) noexcept {
446 if (success) {
447 c_move_.record_success(duration);
448 for (std::uint32_t i = 0; i < images_moved; ++i) {
450 }
451 } else {
452 c_move_.record_failure(duration);
453 }
454 }
455
463 void record_get(bool success,
464 std::chrono::microseconds duration,
465 std::uint32_t images_retrieved = 0,
466 std::uint64_t bytes_retrieved = 0) noexcept {
467 if (success) {
468 c_get_.record_success(duration);
469 for (std::uint32_t i = 0; i < images_retrieved; ++i) {
471 }
472 if (bytes_retrieved > 0) {
473 transfer_.add_bytes_sent(bytes_retrieved);
474 }
475 } else {
476 c_get_.record_failure(duration);
477 }
478 }
479
487 bool success,
488 std::chrono::microseconds duration) noexcept {
489 auto& counter = get_counter(op);
490 if (success) {
491 counter.record_success(duration);
492 } else {
493 counter.record_failure(duration);
494 }
495 }
496
497 // =========================================================================
498 // Data Transfer Recording
499 // =========================================================================
500
505 void record_bytes_sent(std::uint64_t bytes) noexcept {
507 }
508
513 void record_bytes_received(std::uint64_t bytes) noexcept {
515 }
516
517 // =========================================================================
518 // Association Recording
519 // =========================================================================
520
527
534
541
548
549 // =========================================================================
550 // Metric Access
551 // =========================================================================
552
558 [[nodiscard]] const operation_counter& get_counter(dimse_operation op) const noexcept {
559 switch (op) {
561 return c_echo_;
563 return c_store_;
565 return c_find_;
567 return c_move_;
569 return c_get_;
571 return n_create_;
573 return n_set_;
575 return n_get_;
577 return n_action_;
579 return n_event_;
581 return n_delete_;
582 default:
583 return c_echo_; // Should never happen
584 }
585 }
586
592 [[nodiscard]] operation_counter& get_counter(dimse_operation op) noexcept {
593 return const_cast<operation_counter&>(
594 static_cast<const pacs_metrics*>(this)->get_counter(op));
595 }
596
601 [[nodiscard]] const data_transfer_metrics& transfer() const noexcept {
602 return transfer_;
603 }
604
609 [[nodiscard]] const association_counters& associations() const noexcept {
610 return associations_;
611 }
612
613 // =========================================================================
614 // Pool Metrics Access
615 // =========================================================================
616
621 [[nodiscard]] const pool_counters& element_pool() const noexcept {
622 return element_pool_;
623 }
624
629 [[nodiscard]] pool_counters& element_pool() noexcept {
630 return element_pool_;
631 }
632
637 [[nodiscard]] const pool_counters& dataset_pool() const noexcept {
638 return dataset_pool_;
639 }
640
645 [[nodiscard]] pool_counters& dataset_pool() noexcept {
646 return dataset_pool_;
647 }
648
653 [[nodiscard]] const pool_counters& pdu_buffer_pool() const noexcept {
654 return pdu_buffer_pool_;
655 }
656
661 [[nodiscard]] pool_counters& pdu_buffer_pool() noexcept {
662 return pdu_buffer_pool_;
663 }
664
665 // =========================================================================
666 // Export Methods
667 // =========================================================================
668
676 [[nodiscard]] std::string to_json() const;
677
686 [[nodiscard]] std::string to_prometheus(std::string_view prefix = "pacs") const;
687
688 // =========================================================================
689 // Reset
690 // =========================================================================
691
698 void reset() noexcept {
699 c_echo_.reset();
700 c_store_.reset();
701 c_find_.reset();
702 c_move_.reset();
703 c_get_.reset();
705 n_set_.reset();
706 n_get_.reset();
708 n_event_.reset();
715 }
716
717private:
718 // DIMSE operation counters
730
731 // Data transfer metrics
733
734 // Association lifecycle counters
736
737 // Object pool metrics
741};
742
743} // namespace kcenon::pacs::monitoring
Central metrics collection for PACS DICOM operations.
void record_get(bool success, std::chrono::microseconds duration, std::uint32_t images_retrieved=0, std::uint64_t bytes_retrieved=0) noexcept
Record a C-GET operation.
void record_association_aborted() noexcept
Record an association being aborted.
const pool_counters & dataset_pool() const noexcept
Get dataset pool counters.
const data_transfer_metrics & transfer() const noexcept
Get data transfer metrics.
std::string to_prometheus(std::string_view prefix="pacs") const
Export metrics in Prometheus text format.
const pool_counters & element_pool() const noexcept
Get element pool counters.
pool_counters & pdu_buffer_pool() noexcept
Get mutable PDU buffer pool counters.
pool_counters & dataset_pool() noexcept
Get mutable dataset pool counters.
void reset() noexcept
Reset all metrics to zero.
pacs_metrics & operator=(pacs_metrics &&)=delete
const operation_counter & get_counter(dimse_operation op) const noexcept
Get operation counter for a specific DIMSE operation.
void record_association_established() noexcept
Record an association being established.
void record_association_rejected() noexcept
Record an association being rejected.
operation_counter & get_counter(dimse_operation op) noexcept
Get mutable operation counter for a specific DIMSE operation.
void record_bytes_received(std::uint64_t bytes) noexcept
Record bytes received from the network.
static pacs_metrics & global_metrics() noexcept
Get the global singleton instance.
pacs_metrics(pacs_metrics &&)=delete
Non-movable (singleton)
pool_counters & element_pool() noexcept
Get mutable element pool counters.
void record_operation(dimse_operation op, bool success, std::chrono::microseconds duration) noexcept
Record a generic DIMSE operation.
const pool_counters & pdu_buffer_pool() const noexcept
Get PDU buffer pool counters.
void record_bytes_sent(std::uint64_t bytes) noexcept
Record bytes sent over the network.
std::string to_json() const
Export metrics as JSON string.
pacs_metrics(const pacs_metrics &)=delete
Non-copyable.
void record_echo(bool success, std::chrono::microseconds duration) noexcept
Record a C-ECHO (verification) operation.
void record_query(bool success, std::chrono::microseconds duration, std::uint32_t matches=0) noexcept
Record a C-FIND (query) operation.
pacs_metrics & operator=(const pacs_metrics &)=delete
const association_counters & associations() const noexcept
Get association counters.
void record_association_released() noexcept
Record an association being released.
pacs_metrics()=default
Default constructor.
void record_move(bool success, std::chrono::microseconds duration, std::uint32_t images_moved=0) noexcept
Record a C-MOVE operation.
void record_store(bool success, std::chrono::microseconds duration, std::uint64_t bytes_stored=0) noexcept
Record a C-STORE operation.
dimse_operation
DICOM Message Service Element (DIMSE) operation types.
@ c_store
C-STORE (Storage Service)
@ c_move
C-MOVE (Retrieve Service)
@ c_echo
C-ECHO (Verification Service)
@ counter
Monotonic increasing value.
constexpr std::string_view to_string(health_level level) noexcept
Convert health level to string representation.
Metrics for tracking DICOM association lifecycle.
void reset() noexcept
Reset all counters to zero.
std::atomic< std::uint64_t > total_rejected
void record_aborted() noexcept
Record an association being aborted.
std::atomic< std::uint64_t > total_aborted
void record_released() noexcept
Record an association being released normally.
std::atomic< std::uint32_t > current_active
void record_rejected() noexcept
Record an association being rejected.
std::atomic< std::uint64_t > total_established
void record_established() noexcept
Record an association being established.
Metrics for tracking data transfer volumes.
void add_bytes_received(std::uint64_t bytes) noexcept
Record bytes received.
std::atomic< std::uint64_t > images_retrieved
void reset() noexcept
Reset all counters to zero.
void increment_images_retrieved() noexcept
Record an image retrieved.
void add_bytes_sent(std::uint64_t bytes) noexcept
Record bytes sent.
void increment_images_stored() noexcept
Record an image stored.
std::atomic< std::uint64_t > bytes_received
Atomic counter for tracking operation success/failure counts.
std::atomic< std::uint64_t > max_duration_us
void reset() noexcept
Reset all counters to zero.
void record_failure(std::chrono::microseconds duration) noexcept
Record a failed operation with duration.
std::atomic< std::uint64_t > success_count
std::uint64_t average_duration_us() const noexcept
Get average duration in microseconds (0 if no operations)
std::atomic< std::uint64_t > total_duration_us
Total duration in microseconds.
std::atomic< std::uint64_t > failure_count
std::atomic< std::uint64_t > min_duration_us
void record_success(std::chrono::microseconds duration) noexcept
Record a successful operation with duration.
std::uint64_t total_count() const noexcept
Get total operation count (success + failure)
Metrics for tracking object pool usage.
void reset() noexcept
Reset all counters to zero.
std::atomic< std::uint64_t > pool_misses
std::atomic< std::uint64_t > total_acquisitions
double hit_ratio() const noexcept
Calculate hit ratio (0.0 to 1.0)
void set_pool_size(std::uint32_t size) noexcept
Update current pool size.
std::atomic< std::uint64_t > total_releases
std::atomic< std::uint64_t > pool_hits
std::atomic< std::uint32_t > current_pool_size
void record_release() noexcept
Record a pool release.
void record_acquisition(bool was_pool_hit) noexcept
Record a pool acquisition (hit or miss)