PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
metrics_endpoints.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
13// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
14// declaration conflicts
15#include "crow.h"
16
23
24#ifdef PACS_WITH_DATABASE_SYSTEM
25
26#include <sstream>
27
29
30namespace {
31
35void add_cors_headers(crow::response& res, const rest_server_context& ctx) {
36 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
37 res.add_header("Access-Control-Allow-Origin",
38 ctx.config->cors_allowed_origins);
39 }
40}
41
45std::string health_status_to_json_string(
46 services::monitoring::database_health::status status) {
47 switch (status) {
48 case services::monitoring::database_health::status::healthy:
49 return "healthy";
50 case services::monitoring::database_health::status::degraded:
51 return "degraded";
52 case services::monitoring::database_health::status::unhealthy:
53 return "unhealthy";
54 default:
55 return "unknown";
56 }
57}
58
62int get_query_param_int(const crow::request& req, const std::string& key,
63 int default_value) {
64 auto value = req.url_params.get(key);
65 if (value == nullptr) {
66 return default_value;
67 }
68 try {
69 return std::stoi(value);
70 } catch (...) {
71 return default_value;
72 }
73}
74
79bool check_metrics_auth(const std::shared_ptr<rest_server_context>& ctx,
80 const crow::request& req,
81 crow::response& res) {
82 if (ctx->oauth2 && ctx->oauth2->enabled()) {
83 auto auth = ctx->oauth2->authenticate(req, res);
84 if (!auth) return false;
85 if (!ctx->oauth2->require_any_scope(
86 auth->claims, res, {"metrics.read", "admin"})) {
87 return false;
88 }
89 }
90 return true;
91}
92
93} // namespace
94
95void register_metrics_endpoints_impl(
96 crow::SimpleApp& app,
97 std::shared_ptr<rest_server_context> ctx) {
98
99 // GET /api/health/database - Database health check
100 CROW_ROUTE(app, "/api/health/database")
101 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
102 crow::response res;
103 res.add_header("Content-Type", "application/json");
104 add_cors_headers(res, *ctx);
105
106 if (!check_metrics_auth(ctx, req, res)) return res;
107
108 // Check if database metrics service is available
109 if (!ctx->database_metrics) {
110 res.body = make_error_json(
111 "METRICS_UNAVAILABLE",
112 "Database metrics service not configured");
113 res.code = 503;
114 return res;
115 }
116
117 // Perform health check
118 auto health = ctx->database_metrics->check_health();
119
120 // Build JSON response
121 std::ostringstream oss;
122 oss << R"({"status":")" << health_status_to_json_string(health.current_status)
123 << R"(","message":")" << json_escape(health.message)
124 << R"(","response_time_ms":)" << health.response_time.count()
125 << R"(,"active_connections":)" << health.active_connections
126 << R"(,"error_rate":)" << health.error_rate;
127
128 if (!health.warnings.empty()) {
129 oss << R"(,"warnings":[)";
130 for (size_t i = 0; i < health.warnings.size(); ++i) {
131 if (i > 0) oss << ",";
132 oss << R"(")" << json_escape(health.warnings[i]) << R"(")";
133 }
134 oss << "]";
135 }
136
137 oss << "}";
138 res.body = oss.str();
139
140 // Set status code based on health
141 if (health.current_status ==
142 services::monitoring::database_health::status::healthy) {
143 res.code = 200;
144 } else if (health.current_status ==
145 services::monitoring::database_health::status::degraded) {
146 res.code = 200; // Degraded but still operational
147 } else {
148 res.code = 503; // Unhealthy - service unavailable
149 }
150
151 return res;
152 });
153
154 // GET /api/metrics/database - Current metrics in JSON format
155 CROW_ROUTE(app, "/api/metrics/database")
156 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
157 crow::response res;
158 res.add_header("Content-Type", "application/json");
159 add_cors_headers(res, *ctx);
160
161 if (!check_metrics_auth(ctx, req, res)) return res;
162
163 if (!ctx->database_metrics) {
164 res.body = make_error_json(
165 "METRICS_UNAVAILABLE",
166 "Database metrics service not configured");
167 res.code = 503;
168 return res;
169 }
170
171 auto metrics = ctx->database_metrics->get_current_metrics();
172
173 // Build JSON response
174 std::ostringstream oss;
175 oss << R"({"total_queries":)" << metrics.total_queries
176 << R"(,"successful_queries":)" << metrics.successful_queries
177 << R"(,"failed_queries":)" << metrics.failed_queries
178 << R"(,"queries_per_second":)" << metrics.queries_per_second
179 << R"(,"latency":{)"
180 << R"("avg_us":)" << metrics.avg_latency_us
181 << R"(,"min_us":)" << metrics.min_latency_us
182 << R"(,"max_us":)" << metrics.max_latency_us
183 << R"(,"p95_us":)" << metrics.p95_latency_us
184 << R"(,"p99_us":)" << metrics.p99_latency_us << R"(})"
185 << R"(,"connections":{)"
186 << R"("active":)" << metrics.active_connections
187 << R"(,"pool_size":)" << metrics.pool_size
188 << R"(,"utilization":)" << metrics.connection_utilization << R"(})"
189 << R"(,"error_rate":)" << metrics.error_rate
190 << R"(,"slow_query_count":)" << metrics.slow_query_count << "}";
191
192 res.body = oss.str();
193 res.code = 200;
194 return res;
195 });
196
197 // GET /api/metrics/database/slow-queries - Slow query list
198 CROW_ROUTE(app, "/api/metrics/database/slow-queries")
199 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
200 crow::response res;
201 res.add_header("Content-Type", "application/json");
202 add_cors_headers(res, *ctx);
203
204 if (!check_metrics_auth(ctx, req, res)) return res;
205
206 if (!ctx->database_metrics) {
207 res.body = make_error_json(
208 "METRICS_UNAVAILABLE",
209 "Database metrics service not configured");
210 res.code = 503;
211 return res;
212 }
213
214 // Parse query parameters
215 int limit = get_query_param_int(req, "limit", 10);
216 int since_minutes = get_query_param_int(req, "since_minutes", 5);
217
218 auto slow_queries = ctx->database_metrics->get_slow_queries(
219 std::chrono::minutes(since_minutes));
220
221 // Build JSON array
222 std::ostringstream oss;
223 oss << "[";
224
225 size_t count = 0;
226 for (const auto& sq : slow_queries) {
227 if (count >= static_cast<size_t>(limit)) break;
228
229 if (count > 0) oss << ",";
230
231 oss << R"({"query_preview":")" << json_escape(sq.query_preview)
232 << R"(","duration_us":)" << sq.duration_us
233 << R"(,"timestamp":")" << sq.timestamp
234 << R"(","rows_affected":)" << sq.rows_affected << "}";
235
236 ++count;
237 }
238
239 oss << "]";
240 res.body = oss.str();
241 res.code = 200;
242 return res;
243 });
244
245 // GET /metrics - Prometheus format
246 CROW_ROUTE(app, "/metrics")
247 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
248 crow::response res;
249 add_cors_headers(res, *ctx);
250
251 if (!check_metrics_auth(ctx, req, res)) return res;
252
253 if (!ctx->database_metrics) {
254 res.code = 503;
255 res.add_header("Content-Type", "text/plain");
256 res.body = "# Database metrics unavailable\n";
257 return res;
258 }
259
260 auto prometheus_output =
261 ctx->database_metrics->export_prometheus_metrics();
262 res.add_header("Content-Type", "text/plain; version=0.0.4");
263 res.body = prometheus_output;
264 res.code = 200;
265 return res;
266 });
267}
268
269} // namespace kcenon::pacs::web::endpoints
270
271#endif // PACS_WITH_DATABASE_SYSTEM
Database monitoring and metrics service.
Database metrics REST API endpoints.
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
Definition rest_types.h:79
std::string json_escape(std::string_view s)
Escape a string for JSON.
Definition rest_types.h:101
OAuth 2.0 middleware for DICOMweb endpoint authorization.
Configuration for REST API server.
Common types and utilities for REST API.
System API endpoints for REST server.