PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
logger_adapter.cpp
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
11
12#ifdef PACS_WITH_LOGGER_SYSTEM
13#include <kcenon/common/interfaces/logger_interface.h>
14#include <kcenon/logger/core/logger.h>
15#include <kcenon/logger/interfaces/logger_types.h>
16#include <kcenon/logger/writers/console_writer.h>
17#include <kcenon/logger/writers/rotating_file_writer.h>
18#endif // PACS_WITH_LOGGER_SYSTEM
19
20#include <atomic>
21#include <chrono>
22#include <ctime>
23#include <fstream>
24#include <iomanip>
25#include <mutex>
26#include <sstream>
27
29
30// =============================================================================
31// Implementation Class
32// =============================================================================
33
34#ifdef PACS_WITH_LOGGER_SYSTEM
35
36class logger_adapter::impl {
37public:
38 impl() = default;
39 ~impl() { shutdown(); }
40
41 void initialize(const logger_config& config) {
42 std::lock_guard lock(mutex_);
43
44 if (initialized_) {
45 return;
46 }
47
48 config_ = config;
49 min_level_.store(config.min_level);
50
51 // Create log directory if needed
52 if (config.enable_file || config.enable_audit_log) {
53 std::filesystem::create_directories(config.log_directory);
54 }
55
56 // Initialize main logger
57 logger_ = std::make_unique<kcenon::logger::logger>(
58 config.async_mode, config.buffer_size);
59
60 // Set minimum log level (ignore result for initialization)
61 (void)logger_->set_level(convert_to_common_log_level(config.min_level));
62
63 // Add console writer if enabled
64 if (config.enable_console) {
65 logger_->add_writer(std::make_unique<kcenon::logger::console_writer>());
66 }
67
68 // Add file writer if enabled
69 if (config.enable_file) {
70 auto log_path = config.log_directory / "pacs.log";
71 auto writer = std::make_unique<kcenon::logger::rotating_file_writer>(
72 log_path.string(),
73 config.max_file_size_mb * 1024 * 1024, // Convert MB to bytes
74 config.max_files);
75 logger_->add_writer(std::move(writer));
76 }
77
78 // Start the logger
79 logger_->start();
80
81 // Initialize audit log file path if enabled
82 if (config.enable_audit_log) {
83 audit_log_path_ = config.log_directory / "audit.json";
84 }
85
86 initialized_ = true;
87 }
88
89 void shutdown() {
90 std::lock_guard lock(mutex_);
91
92 if (!initialized_) {
93 return;
94 }
95
96 if (logger_) {
97 logger_->flush();
98 logger_->stop();
99 logger_.reset();
100 }
101
102 initialized_ = false;
103 }
104
105 [[nodiscard]] auto is_initialized() const noexcept -> bool {
106 return initialized_.load();
107 }
108
109 void log(log_level level, const std::string& message) {
110 if (!initialized_ || !logger_) {
111 return;
112 }
113
114 if (!is_level_enabled(level)) {
115 return;
116 }
117
118 (void)logger_->log(convert_to_common_log_level(level), message);
119 }
120
121
122 [[nodiscard]] auto is_level_enabled(log_level level) const noexcept -> bool {
123 return static_cast<int>(level) >= static_cast<int>(min_level_.load());
124 }
125
126 void flush() {
127 if (logger_) {
128 logger_->flush();
129 }
130 }
131
132 void set_min_level(log_level level) {
133 min_level_.store(level);
134 if (logger_) {
135 (void)logger_->set_level(convert_to_common_log_level(level));
136 }
137 }
138
139 [[nodiscard]] auto get_min_level() const noexcept -> log_level {
140 return min_level_.load();
141 }
142
143 [[nodiscard]] auto get_config() const -> const logger_config& { return config_; }
144
145 void write_audit_log(const std::string& event_type,
146 const std::string& outcome,
147 const std::map<std::string, std::string>& fields) {
148 if (!config_.enable_audit_log) {
149 return;
150 }
151
152 std::lock_guard lock(audit_mutex_);
153
154 std::ofstream file(audit_log_path_, std::ios::app);
155 if (!file) {
156 return;
157 }
158
159 // Build JSON entry
160 std::ostringstream json;
161 json << "{";
162 json << "\"timestamp\":\"" << format_iso8601() << "\",";
163 json << "\"event_type\":\"" << escape_json(event_type) << "\",";
164 json << "\"outcome\":\"" << escape_json(outcome) << "\"";
165
166 for (const auto& [key, value] : fields) {
167 json << ",\"" << escape_json(key) << "\":\"" << escape_json(value) << "\"";
168 }
169
170 json << "}\n";
171
172 file << json.str();
173 file.flush();
174 }
175
176private:
177 [[nodiscard]] static auto convert_to_common_log_level(log_level level) -> kcenon::common::interfaces::log_level {
178 switch (level) {
179 case log_level::trace:
180 return kcenon::common::interfaces::log_level::trace;
181 case log_level::debug:
182 return kcenon::common::interfaces::log_level::debug;
183 case log_level::info:
184 return kcenon::common::interfaces::log_level::info;
185 case log_level::warn:
186 return kcenon::common::interfaces::log_level::warning;
187 case log_level::error:
188 return kcenon::common::interfaces::log_level::error;
189 case log_level::fatal:
190 return kcenon::common::interfaces::log_level::critical;
191 case log_level::off:
192 default:
193 return kcenon::common::interfaces::log_level::off;
194 }
195 }
196
197 [[nodiscard]] static auto format_iso8601() -> std::string {
198 auto now = std::chrono::system_clock::now();
199 auto time_t_val = std::chrono::system_clock::to_time_t(now);
200 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
201 now.time_since_epoch()) %
202 1000;
203
204 std::tm tm_val{};
205#ifdef _WIN32
206 localtime_s(&tm_val, &time_t_val);
207#else
208 localtime_r(&time_t_val, &tm_val);
209#endif
210
211 std::ostringstream oss;
212 oss << std::put_time(&tm_val, "%Y-%m-%dT%H:%M:%S");
213 oss << '.' << std::setfill('0') << std::setw(3) << ms.count();
214 oss << std::put_time(&tm_val, "%z");
215 return oss.str();
216 }
217
218 [[nodiscard]] static auto escape_json(const std::string& str) -> std::string {
219 std::ostringstream oss;
220 for (char c : str) {
221 switch (c) {
222 case '"':
223 oss << "\\\"";
224 break;
225 case '\\':
226 oss << "\\\\";
227 break;
228 case '\b':
229 oss << "\\b";
230 break;
231 case '\f':
232 oss << "\\f";
233 break;
234 case '\n':
235 oss << "\\n";
236 break;
237 case '\r':
238 oss << "\\r";
239 break;
240 case '\t':
241 oss << "\\t";
242 break;
243 default:
244 if (static_cast<unsigned char>(c) < 32) {
245 oss << "\\u" << std::hex << std::setw(4) << std::setfill('0')
246 << static_cast<int>(c);
247 } else {
248 oss << c;
249 }
250 break;
251 }
252 }
253 return oss.str();
254 }
255
256 mutable std::mutex mutex_;
257 mutable std::mutex audit_mutex_;
258 std::atomic<bool> initialized_{false};
259 std::atomic<log_level> min_level_{log_level::info};
260 logger_config config_;
261 std::unique_ptr<kcenon::logger::logger> logger_;
262 std::filesystem::path audit_log_path_;
263};
264
265// =============================================================================
266// Static Member Initialization
267// =============================================================================
268
269std::unique_ptr<logger_adapter::impl> logger_adapter::pimpl_ =
270 std::make_unique<logger_adapter::impl>();
271
272// =============================================================================
273// Initialization
274// =============================================================================
275
276void logger_adapter::initialize(const logger_config& config) {
277 pimpl_->initialize(config);
278}
279
280void logger_adapter::shutdown() { pimpl_->shutdown(); }
281
282auto logger_adapter::is_initialized() noexcept -> bool {
283 return pimpl_->is_initialized();
284}
285
286// =============================================================================
287// Standard Logging
288// =============================================================================
289
290void logger_adapter::log(log_level level, const std::string& message) {
291 pimpl_->log(level, message);
292}
293
294auto logger_adapter::is_level_enabled(log_level level) noexcept -> bool {
295 return pimpl_->is_level_enabled(level);
296}
297
298void logger_adapter::flush() { pimpl_->flush(); }
299
300// =============================================================================
301// Configuration
302// =============================================================================
303
305 pimpl_->set_min_level(level);
306}
307
308auto logger_adapter::get_min_level() noexcept -> log_level {
309 return pimpl_->get_min_level();
310}
311
312auto logger_adapter::get_config() -> const logger_config& {
313 return pimpl_->get_config();
314}
315
316// =============================================================================
317// Private Helpers
318// =============================================================================
319
321 const std::string& event_type,
322 const std::string& outcome,
323 const std::map<std::string, std::string>& fields) {
324 pimpl_->write_audit_log(event_type, outcome, fields);
325}
326
327#else // PACS_WITH_LOGGER_SYSTEM not defined — no-op stubs
328
329// =============================================================================
330// No-op Implementation (logger_system not available)
331// =============================================================================
332
334
335std::unique_ptr<logger_adapter::impl> logger_adapter::pimpl_ = nullptr;
336
339auto logger_adapter::is_initialized() noexcept -> bool { return false; }
340void logger_adapter::log(log_level, const std::string&) {}
341auto logger_adapter::is_level_enabled(log_level) noexcept -> bool { return false; }
345
347 static const logger_config default_config;
348 return default_config;
349}
350
352 const std::string&,
353 const std::string&,
354 const std::map<std::string, std::string>&) {}
355
356#endif // PACS_WITH_LOGGER_SYSTEM
357
358// =============================================================================
359// DICOM Audit Logging (delegates to log()/write_audit_log() — always compiled)
360// =============================================================================
361
362void logger_adapter::log_association_established(const std::string& calling_ae,
363 const std::string& called_ae,
364 const std::string& remote_ip) {
365 info("Association established: {} -> {} from {}",
366 calling_ae, called_ae, remote_ip);
367
368 write_audit_log("ASSOCIATION_ESTABLISHED", "success",
369 {{"calling_ae", calling_ae},
370 {"called_ae", called_ae},
371 {"remote_ip", remote_ip}});
372}
373
374void logger_adapter::log_association_released(const std::string& calling_ae,
375 const std::string& called_ae) {
376 debug("Association released: {} -> {}", calling_ae, called_ae);
377
378 write_audit_log("ASSOCIATION_RELEASED", "success",
379 {{"calling_ae", calling_ae}, {"called_ae", called_ae}});
380}
381
382void logger_adapter::log_c_store_received(const std::string& calling_ae,
383 const std::string& patient_id,
384 const std::string& study_uid,
385 const std::string& sop_instance_uid,
386 storage_status status) {
387 auto outcome = (status == storage_status::success) ? "success" : "failure";
388 auto status_str = storage_status_to_string(status);
389
390 if (status == storage_status::success) {
391 info("C-STORE received: patient={} study={} instance={} from {}",
392 patient_id, study_uid, sop_instance_uid, calling_ae);
393 } else {
394 warn("C-STORE failed: patient={} status={} from {}",
395 patient_id, status_str, calling_ae);
396 }
397
398 write_audit_log("C-STORE", outcome,
399 {{"calling_ae", calling_ae},
400 {"patient_id", patient_id},
401 {"study_uid", study_uid},
402 {"sop_instance_uid", sop_instance_uid},
403 {"status", status_str}});
404}
405
406void logger_adapter::log_c_find_executed(const std::string& calling_ae,
407 query_level level,
408 std::size_t matches_returned) {
409 auto level_str = query_level_to_string(level);
410
411 debug("C-FIND executed: level={} matches={} from {}",
412 level_str, matches_returned, calling_ae);
413
414 write_audit_log("C-FIND", "success",
415 {{"calling_ae", calling_ae},
416 {"query_level", level_str},
417 {"matches_returned", std::to_string(matches_returned)}});
418}
419
420void logger_adapter::log_c_move_executed(const std::string& calling_ae,
421 const std::string& destination_ae,
422 const std::string& study_uid,
423 std::size_t instances_moved,
424 move_status status) {
425 auto outcome = (status == move_status::success ||
427 ? "success"
428 : "failure";
429 auto status_str = move_status_to_string(status);
430
431 if (status == move_status::success) {
432 info("C-MOVE completed: study={} instances={} to {} from {}",
433 study_uid, instances_moved, destination_ae, calling_ae);
434 } else {
435 warn("C-MOVE failed: study={} status={} to {} from {}",
436 study_uid, status_str, destination_ae, calling_ae);
437 }
438
439 write_audit_log("C-MOVE", outcome,
440 {{"calling_ae", calling_ae},
441 {"destination_ae", destination_ae},
442 {"study_uid", study_uid},
443 {"instances_moved", std::to_string(instances_moved)},
444 {"status", status_str}});
445}
446
448 const std::string& description,
449 const std::string& user_id) {
450 auto type_str = security_event_to_string(type);
451
452 // Security events are always logged at warn level or higher
453 switch (type) {
455 info("Security event: {} - {}", type_str, description);
456 break;
461 warn("Security event: {} - {}", type_str, description);
462 break;
465 info("Security event: {} - {}", type_str, description);
466 break;
467 }
468
469 std::map<std::string, std::string> fields = {
470 {"security_event", type_str}, {"description", description}};
471
472 if (!user_id.empty()) {
473 fields["user_id"] = user_id;
474 }
475
476 write_audit_log("SECURITY", security_event_to_string(type), fields);
477}
478
479// =============================================================================
480// String Conversion Helpers (always compiled — no logger_system dependency)
481// =============================================================================
482
484 switch (status) {
486 return "Success";
488 return "OutOfResources";
490 return "DataSetError";
492 return "CannotUnderstand";
494 return "ProcessingFailure";
496 return "DuplicateRejected";
498 return "DuplicateStored";
500 default:
501 return "UnknownError";
502 }
503}
504
506 switch (status) {
508 return "Success";
510 return "PartialSuccess";
512 return "RefusedOutOfResources";
514 return "RefusedMoveDestinationUnknown";
516 return "IdentifierDoesNotMatch";
518 return "UnableToProcess";
520 return "Cancelled";
522 default:
523 return "UnknownError";
524 }
525}
526
528 switch (level) {
530 return "PATIENT";
532 return "STUDY";
534 return "SERIES";
536 return "IMAGE";
537 default:
538 return "UNKNOWN";
539 }
540}
541
543 switch (type) {
545 return "authentication_success";
547 return "authentication_failure";
549 return "access_denied";
551 return "configuration_change";
553 return "data_export";
555 return "association_rejected";
557 return "invalid_request";
558 default:
559 return "unknown";
560 }
561}
562
564 switch (level) {
565 case log_level::trace:
566 return "TRACE";
567 case log_level::debug:
568 return "DEBUG";
569 case log_level::info:
570 return "INFO";
571 case log_level::warn:
572 return "WARN";
573 case log_level::error:
574 return "ERROR";
575 case log_level::fatal:
576 return "FATAL";
577 case log_level::off:
578 default:
579 return "OFF";
580 }
581}
582
583} // namespace kcenon::pacs::integration
static std::unique_ptr< impl > pimpl_
static void flush()
Flush all pending log messages.
static void log_c_find_executed(const std::string &calling_ae, query_level level, std::size_t matches_returned)
Log C-FIND operation.
static void log_security_event(security_event_type type, const std::string &description, const std::string &user_id="")
Log a security-related event.
static void write_audit_log(const std::string &event_type, const std::string &outcome, const std::map< std::string, std::string > &fields)
static void log_c_store_received(const std::string &calling_ae, const std::string &patient_id, const std::string &study_uid, const std::string &sop_instance_uid, storage_status status)
Log C-STORE operation.
static void log_association_released(const std::string &calling_ae, const std::string &called_ae)
Log DICOM association release.
static void log_c_move_executed(const std::string &calling_ae, const std::string &destination_ae, const std::string &study_uid, std::size_t instances_moved, move_status status)
Log C-MOVE operation.
static void shutdown()
Shutdown the logger.
static auto security_event_to_string(security_event_type type) -> std::string
static void log(log_level level, const std::string &message)
Log a message at the specified level.
static auto get_config() -> const logger_config &
Get the current configuration.
static auto log_level_to_string(log_level level) -> std::string
static auto storage_status_to_string(storage_status status) -> std::string
static auto is_level_enabled(log_level level) noexcept -> bool
Check if a log level is enabled.
static void set_min_level(log_level level)
Set the minimum log level.
static auto move_status_to_string(move_status status) -> std::string
static auto is_initialized() noexcept -> bool
Check if the logger is initialized.
static void initialize(const logger_config &config)
Initialize the logger with configuration.
static auto get_min_level() noexcept -> log_level
Get the current minimum log level.
static void log_association_established(const std::string &calling_ae, const std::string &called_ae, const std::string &remote_ip)
Log DICOM association establishment.
static auto query_level_to_string(query_level level) -> std::string
Adapter for DICOM audit logging using logger_system.
storage_status
Status of DICOM C-STORE operations.
security_event_type
Types of security events for audit logging.
log_level
Log severity levels.
query_level
DICOM query retrieve level.
move_status
Status of DICOM C-MOVE operations.
Configuration options for the logger adapter.