PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
health_json.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 "health_status.h"
22
23#include <chrono>
24#include <cstdio>
25#include <iomanip>
26#include <sstream>
27#include <string>
28
30
36[[nodiscard]] inline std::string to_iso8601(
37 std::chrono::system_clock::time_point tp) {
38 const auto time_t_val = std::chrono::system_clock::to_time_t(tp);
39 std::tm tm_val{};
40
41#if defined(_MSC_VER)
42 gmtime_s(&tm_val, &time_t_val);
43#else
44 gmtime_r(&time_t_val, &tm_val);
45#endif
46
47 std::ostringstream oss;
48 oss << std::put_time(&tm_val, "%Y-%m-%dT%H:%M:%SZ");
49 return oss.str();
50}
51
57[[nodiscard]] inline std::string escape_json_string(std::string_view str) {
58 std::string result;
59 result.reserve(str.size() + 10); // Reserve extra space for escapes
60
61 for (char c : str) {
62 switch (c) {
63 case '"':
64 result += "\\\"";
65 break;
66 case '\\':
67 result += "\\\\";
68 break;
69 case '\b':
70 result += "\\b";
71 break;
72 case '\f':
73 result += "\\f";
74 break;
75 case '\n':
76 result += "\\n";
77 break;
78 case '\r':
79 result += "\\r";
80 break;
81 case '\t':
82 result += "\\t";
83 break;
84 default:
85 if (static_cast<unsigned char>(c) < 0x20) {
86 // Control character - encode as \u00XX
87 char buf[8];
88 std::snprintf(buf, sizeof(buf), "\\u%04x",
89 static_cast<unsigned char>(c));
90 result += buf;
91 } else {
92 result += c;
93 }
94 break;
95 }
96 }
97
98 return result;
99}
100
106[[nodiscard]] inline std::string to_json(const database_status& status) {
107 std::ostringstream oss;
108 oss << "{"
109 << R"("connected":)" << (status.connected ? "true" : "false");
110
111 if (status.last_connected) {
112 oss << R"(,"last_connected":")" << to_iso8601(*status.last_connected)
113 << "\"";
114 }
115
116 oss << R"(,"active_connections":)" << status.active_connections;
117
118 if (status.response_time) {
119 oss << R"(,"response_time_ms":)" << status.response_time->count();
120 }
121
122 if (status.error_message) {
123 oss << R"(,"error":")" << escape_json_string(*status.error_message)
124 << "\"";
125 }
126
127 oss << "}";
128 return oss.str();
129}
130
136[[nodiscard]] inline std::string to_json(const storage_status& status) {
137 std::ostringstream oss;
138 oss << "{"
139 << R"("writable":)" << (status.writable ? "true" : "false")
140 << R"(,"readable":)" << (status.readable ? "true" : "false")
141 << R"(,"total_bytes":)" << status.total_bytes
142 << R"(,"used_bytes":)" << status.used_bytes
143 << R"(,"available_bytes":)" << status.available_bytes
144 << R"(,"usage_percent":)" << std::fixed << std::setprecision(2)
145 << status.usage_percent();
146
147 if (status.error_message) {
148 oss << R"(,"error":")" << escape_json_string(*status.error_message)
149 << "\"";
150 }
151
152 oss << "}";
153 return oss.str();
154}
155
161[[nodiscard]] inline std::string to_json(const association_metrics& metrics) {
162 std::ostringstream oss;
163 oss << "{"
164 << R"("active":)" << metrics.active_associations
165 << R"(,"max":)" << metrics.max_associations
166 << R"(,"total":)" << metrics.total_associations
167 << R"(,"failed":)" << metrics.failed_associations << "}";
168 return oss.str();
169}
170
176[[nodiscard]] inline std::string to_json(const storage_metrics& metrics) {
177 std::ostringstream oss;
178 oss << "{"
179 << R"("total_instances":)" << metrics.total_instances
180 << R"(,"total_studies":)" << metrics.total_studies
181 << R"(,"total_series":)" << metrics.total_series
182 << R"(,"successful_stores":)" << metrics.successful_stores
183 << R"(,"failed_stores":)" << metrics.failed_stores << "}";
184 return oss.str();
185}
186
192[[nodiscard]] inline std::string to_json(const version_info& info) {
193 std::ostringstream oss;
194 oss << "{"
195 << R"("version":")" << info.version_string() << "\""
196 << R"(,"major":)" << info.major << R"(,"minor":)" << info.minor
197 << R"(,"patch":)" << info.patch;
198
199 if (!info.build_id.empty()) {
200 oss << R"(,"build_id":")" << escape_json_string(info.build_id) << "\"";
201 }
202
203 oss << R"(,"startup_time":")" << to_iso8601(info.startup_time) << "\""
204 << R"(,"uptime_seconds":)" << info.uptime().count() << "}";
205
206 return oss.str();
207}
208
229[[nodiscard]] inline std::string to_json(const health_status& status) {
230 std::ostringstream oss;
231 oss << "{"
232 << R"("status":")" << to_string(status.level) << "\""
233 << R"(,"timestamp":")" << to_iso8601(status.timestamp) << "\""
234 << R"(,"healthy":)" << (status.is_healthy() ? "true" : "false")
235 << R"(,"operational":)" << (status.is_operational() ? "true" : "false");
236
237 if (status.message) {
238 oss << R"(,"message":")" << escape_json_string(*status.message) << "\"";
239 }
240
241 oss << R"(,"database":)" << to_json(status.database)
242 << R"(,"storage":)" << to_json(status.storage)
243 << R"(,"associations":)" << to_json(status.associations)
244 << R"(,"metrics":)" << to_json(status.metrics)
245 << R"(,"version":)" << to_json(status.version) << "}";
246
247 return oss.str();
248}
249
259[[nodiscard]] inline std::string to_json_pretty(const health_status& status,
260 int indent = 2) {
261 const std::string ind(static_cast<std::size_t>(indent), ' ');
262 const std::string ind2(static_cast<std::size_t>(indent * 2), ' ');
263
264 std::ostringstream oss;
265 oss << "{\n"
266 << ind << R"("status": ")" << to_string(status.level) << "\",\n"
267 << ind << R"("timestamp": ")" << to_iso8601(status.timestamp) << "\",\n"
268 << ind << R"("healthy": )" << (status.is_healthy() ? "true" : "false")
269 << ",\n"
270 << ind << R"("operational": )"
271 << (status.is_operational() ? "true" : "false");
272
273 if (status.message) {
274 oss << ",\n"
275 << ind << R"("message": ")" << escape_json_string(*status.message)
276 << "\"";
277 }
278
279 // Database section
280 oss << ",\n"
281 << ind << R"("database": {)" << "\n"
282 << ind2 << R"("connected": )"
283 << (status.database.connected ? "true" : "false");
284
285 if (status.database.last_connected) {
286 oss << ",\n"
287 << ind2 << R"("last_connected": ")"
288 << to_iso8601(*status.database.last_connected) << "\"";
289 }
290
291 oss << ",\n"
292 << ind2
293 << R"("active_connections": )" << status.database.active_connections;
294
295 if (status.database.response_time) {
296 oss << ",\n"
297 << ind2 << R"("response_time_ms": )"
298 << status.database.response_time->count();
299 }
300
301 if (status.database.error_message) {
302 oss << ",\n"
303 << ind2 << R"("error": ")"
304 << escape_json_string(*status.database.error_message) << "\"";
305 }
306
307 oss << "\n" << ind << "}";
308
309 // Storage section
310 oss << ",\n"
311 << ind << R"("storage": {)" << "\n"
312 << ind2 << R"("writable": )"
313 << (status.storage.writable ? "true" : "false") << ",\n"
314 << ind2 << R"("readable": )"
315 << (status.storage.readable ? "true" : "false") << ",\n"
316 << ind2 << R"("total_bytes": )" << status.storage.total_bytes << ",\n"
317 << ind2 << R"("used_bytes": )" << status.storage.used_bytes << ",\n"
318 << ind2 << R"("available_bytes": )" << status.storage.available_bytes
319 << ",\n"
320 << ind2 << R"("usage_percent": )" << std::fixed << std::setprecision(2)
321 << status.storage.usage_percent();
322
323 if (status.storage.error_message) {
324 oss << ",\n"
325 << ind2 << R"("error": ")"
326 << escape_json_string(*status.storage.error_message) << "\"";
327 }
328
329 oss << "\n" << ind << "}";
330
331 // Associations section
332 oss << ",\n"
333 << ind << R"("associations": {)" << "\n"
334 << ind2 << R"("active": )" << status.associations.active_associations
335 << ",\n"
336 << ind2 << R"("max": )" << status.associations.max_associations << ",\n"
337 << ind2 << R"("total": )" << status.associations.total_associations
338 << ",\n"
339 << ind2 << R"("failed": )" << status.associations.failed_associations
340 << "\n"
341 << ind << "}";
342
343 // Metrics section
344 oss << ",\n"
345 << ind << R"("metrics": {)" << "\n"
346 << ind2 << R"("total_instances": )" << status.metrics.total_instances
347 << ",\n"
348 << ind2 << R"("total_studies": )" << status.metrics.total_studies
349 << ",\n"
350 << ind2 << R"("total_series": )" << status.metrics.total_series << ",\n"
351 << ind2
352 << R"("successful_stores": )" << status.metrics.successful_stores
353 << ",\n"
354 << ind2 << R"("failed_stores": )" << status.metrics.failed_stores
355 << "\n"
356 << ind << "}";
357
358 // Version section
359 oss << ",\n"
360 << ind << R"("version": {)" << "\n"
361 << ind2 << R"("version": ")" << status.version.version_string()
362 << "\",\n"
363 << ind2 << R"("major": )" << status.version.major << ",\n"
364 << ind2 << R"("minor": )" << status.version.minor << ",\n"
365 << ind2 << R"("patch": )" << status.version.patch;
366
367 if (!status.version.build_id.empty()) {
368 oss << ",\n"
369 << ind2 << R"("build_id": ")"
370 << escape_json_string(status.version.build_id) << "\"";
371 }
372
373 oss << ",\n"
374 << ind2 << R"("startup_time": ")"
375 << to_iso8601(status.version.startup_time) << "\",\n"
376 << ind2 << R"("uptime_seconds": )" << status.version.uptime().count()
377 << "\n"
378 << ind << "}"
379 << "\n}";
380
381 return oss.str();
382}
383
384} // namespace kcenon::pacs::monitoring
Health status data structures for PACS system monitoring.
std::string to_iso8601(std::chrono::system_clock::time_point tp)
Convert time_point to ISO 8601 string.
constexpr dicom_tag status
Status.
constexpr std::string_view to_string(health_level level) noexcept
Convert health level to string representation.