1use super::{
4 client::{
5 ExtendedChannelInfo, StandardChannelInfo, Sv2ClientInfo, Sv2ClientMetadata,
6 Sv2ClientsMonitoring, Sv2ClientsSummary,
7 },
8 prometheus_metrics::PrometheusMetrics,
9 routes,
10 server::{
11 ServerExtendedChannelInfo, ServerMonitoring, ServerStandardChannelInfo, ServerSummary,
12 },
13 snapshot_cache::SnapshotCache,
14 sv1::{Sv1ClientInfo, Sv1ClientsMonitoring, Sv1ClientsSummary},
15 GlobalInfo,
16};
17use axum::{
18 extract::{Path, Query, State},
19 http::StatusCode,
20 response::{IntoResponse, Json, Response},
21 routing::get,
22 Router,
23};
24use prometheus::{Encoder, TextEncoder};
25use serde::{Deserialize, Serialize};
26use std::{
27 future::Future,
28 net::SocketAddr,
29 sync::Arc,
30 time::{Duration, SystemTime, UNIX_EPOCH},
31};
32use tokio::net::TcpListener;
33use tracing::info;
34use utoipa::{IntoParams, OpenApi, ToSchema};
35use utoipa_swagger_ui::SwaggerUi;
36
37#[derive(OpenApi)]
38#[openapi(
39 info(
40 title = "SRI Monitoring API",
44 version = "0.1.0",
45 description = "HTTP JSON API for monitoring SV2 applications"
46 ),
47 paths(
48 handle_health,
49 handle_global,
50 handle_server,
51 handle_server_channels,
52 handle_clients,
53 handle_client_by_id,
54 handle_client_channels,
55 handle_sv1_clients,
56 handle_sv1_client_by_id,
57 ),
58 components(schemas(
59 GlobalInfo,
60 ServerSummary,
61 Sv2ClientsSummary,
62 ServerExtendedChannelInfo,
63 ServerStandardChannelInfo,
64 Sv2ClientInfo,
65 Sv2ClientMetadata,
66 ExtendedChannelInfo,
67 StandardChannelInfo,
68 Sv1ClientInfo,
69 Sv1ClientsSummary,
70 HealthResponse,
71 ErrorResponse,
72 ServerResponse,
73 ServerChannelsResponse,
74 Sv2ClientsResponse,
75 Sv2ClientResponse,
76 Sv2ClientChannelsResponse,
77 Sv1ClientsResponse,
78 )),
79 tags(
80 (name = "health", description = "Health check endpoints"),
81 (name = "global", description = "Global statistics"),
82 (name = "server", description = "Server (upstream) monitoring"),
83 (name = "clients", description = "Clients (downstream) monitoring"),
84 (name = "sv1", description = "Sv1 clients monitoring (Translator Proxy only)")
85 )
86)]
87pub struct ApiDoc;
88
89#[derive(Clone)]
91struct ServerState {
92 cache: Arc<SnapshotCache>,
93 start_time: u64,
94 metrics: PrometheusMetrics,
95}
96
97const DEFAULT_LIMIT: usize = 25;
98const MAX_LIMIT: usize = 100;
99
100#[derive(Deserialize, IntoParams)]
101struct Pagination {
102 #[serde(default)]
104 offset: usize,
105 #[serde(default)]
107 limit: Option<usize>,
108}
109
110impl Pagination {
111 fn effective_limit(&self) -> usize {
112 self.limit
113 .map(|l| l.min(MAX_LIMIT))
114 .unwrap_or(DEFAULT_LIMIT)
115 }
116}
117
118fn paginate<T: Clone>(items: &[T], params: &Pagination) -> (usize, Vec<T>) {
119 let total = items.len();
120 let limit = params.effective_limit();
121 let offset = params.offset.min(total);
122 let sliced = items
123 .iter()
124 .skip(offset)
125 .take(limit)
126 .cloned()
127 .collect::<Vec<_>>();
128 (total, sliced)
129}
130
131pub struct MonitoringServer {
133 bind_address: SocketAddr,
134 state: ServerState,
135 refresh_interval: Duration,
136}
137
138impl MonitoringServer {
139 pub fn new(
155 bind_address: SocketAddr,
156 server_monitoring: Option<Arc<dyn ServerMonitoring + Send + Sync + 'static>>,
157 sv2_clients_monitoring: Option<Arc<dyn Sv2ClientsMonitoring + Send + Sync + 'static>>,
158 refresh_interval: Duration,
159 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
160 let start_time = SystemTime::now()
161 .duration_since(UNIX_EPOCH)
162 .unwrap_or_default()
163 .as_secs();
164
165 let has_server = server_monitoring.is_some();
166 let has_sv2_clients = sv2_clients_monitoring.is_some();
167
168 let metrics = PrometheusMetrics::new(has_server, has_sv2_clients, false)?;
169
170 let cache = Arc::new(
173 SnapshotCache::new(refresh_interval, server_monitoring, sv2_clients_monitoring)
174 .with_metrics(metrics.clone()),
175 );
176
177 cache.refresh();
179
180 Ok(Self {
181 bind_address,
182 refresh_interval,
183 state: ServerState {
184 cache,
185 start_time,
186 metrics,
187 },
188 })
189 }
190
191 pub fn with_sv1_monitoring(
195 mut self,
196 sv1_monitoring: Arc<dyn Sv1ClientsMonitoring + Send + Sync + 'static>,
197 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
198 let snapshot = self.state.cache.get_snapshot();
200 let has_server = snapshot.server_info.is_some();
201 let has_sv2_clients = snapshot.sv2_clients_summary.is_some();
202
203 let metrics = PrometheusMetrics::new(has_server, has_sv2_clients, true)?;
205
206 let cache = Arc::new(
208 Arc::try_unwrap(self.state.cache)
209 .unwrap_or_else(|arc| (*arc).clone())
210 .with_sv1_clients_source(sv1_monitoring)
211 .with_metrics(metrics.clone()),
212 );
213
214 cache.refresh();
216
217 self.state.metrics = metrics;
218 self.state.cache = cache;
219
220 Ok(self)
221 }
222
223 pub async fn run(
234 self,
235 shutdown_signal: impl Future<Output = ()> + Send + 'static,
236 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
237 info!("Starting monitoring server on http://{}", self.bind_address);
238 info!("Cache refresh interval: {:?}", self.refresh_interval);
239
240 let listener = TcpListener::bind(self.bind_address).await?;
241
242 let cache_for_refresh = self.state.cache.clone();
244 let refresh_interval = self.refresh_interval;
245 let refresh_handle = tokio::spawn(async move {
246 let mut interval = tokio::time::interval(refresh_interval);
247 loop {
248 interval.tick().await;
249 cache_for_refresh.refresh();
250 }
251 });
252
253 let api_v1 = Router::new()
255 .route(routes::segments::HEALTH, get(handle_health))
256 .route(routes::segments::GLOBAL, get(handle_global))
257 .route(routes::segments::SERVER, get(handle_server))
258 .route(
259 routes::segments::SERVER_CHANNELS,
260 get(handle_server_channels),
261 )
262 .route(routes::segments::CLIENTS, get(handle_clients))
263 .route(routes::segments::CLIENT_BY_ID, get(handle_client_by_id))
264 .route(
265 routes::segments::CLIENT_CHANNELS,
266 get(handle_client_channels),
267 )
268 .route(routes::segments::SV1_CLIENTS, get(handle_sv1_clients))
269 .route(
270 routes::segments::SV1_CLIENT_BY_ID,
271 get(handle_sv1_client_by_id),
272 );
273
274 let app = Router::new()
275 .route(routes::ROOT, get(handle_root))
276 .merge(SwaggerUi::new(routes::SWAGGER_UI).url(routes::OPENAPI_SPEC, ApiDoc::openapi()))
277 .nest(routes::API_V1_PREFIX, api_v1)
278 .route(routes::METRICS, get(handle_prometheus_metrics))
279 .with_state(self.state);
280
281 info!(
282 "Swagger UI available at http://{}/swagger-ui",
283 self.bind_address
284 );
285 info!(
286 "Prometheus metrics available at http://{}/metrics",
287 self.bind_address
288 );
289
290 let server_handle = axum::serve(listener, app).with_graceful_shutdown(async move {
291 shutdown_signal.await;
292 info!("Monitoring server received shutdown signal, stopping...");
293 });
294
295 let result = server_handle.await;
297
298 refresh_handle.abort();
300
301 info!("Monitoring server stopped");
302 result.map_err(|e| e.into())
303 }
304}
305
306#[derive(Serialize, Deserialize, Debug, ToSchema)]
310pub struct HealthResponse {
311 pub status: String,
312 pub timestamp: u64,
313}
314
315#[derive(Serialize, Deserialize, Debug, ToSchema)]
316pub struct ErrorResponse {
317 pub error: String,
318}
319
320#[derive(Serialize, Deserialize, Debug, ToSchema)]
321pub struct ServerResponse {
322 pub extended_channels_count: usize,
323 pub standard_channels_count: usize,
324 pub total_hashrate: f32,
325}
326
327#[derive(Serialize, Deserialize, Debug, ToSchema)]
328pub struct ServerChannelsResponse {
329 pub offset: usize,
330 pub limit: usize,
331 pub total_extended: usize,
332 pub total_standard: usize,
333 pub extended_channels: Vec<ServerExtendedChannelInfo>,
334 pub standard_channels: Vec<ServerStandardChannelInfo>,
335}
336
337#[derive(Serialize, Deserialize, Debug, ToSchema)]
338pub struct Sv2ClientsResponse {
339 pub offset: usize,
340 pub limit: usize,
341 pub total: usize,
342 pub items: Vec<Sv2ClientMetadata>,
343}
344
345#[derive(Serialize, Deserialize, Debug, ToSchema)]
346pub struct Sv2ClientResponse {
347 pub client_id: usize,
348 pub extended_channels_count: usize,
349 pub standard_channels_count: usize,
350 pub total_hashrate: f32,
351}
352
353#[derive(Serialize, Deserialize, Debug, ToSchema)]
354pub struct Sv2ClientChannelsResponse {
355 pub client_id: usize,
356 pub offset: usize,
357 pub limit: usize,
358 pub total_extended: usize,
359 pub total_standard: usize,
360 pub extended_channels: Vec<ExtendedChannelInfo>,
361 pub standard_channels: Vec<StandardChannelInfo>,
362}
363
364#[derive(Serialize, Deserialize, Debug, ToSchema)]
365pub struct Sv1ClientsResponse {
366 pub offset: usize,
367 pub limit: usize,
368 pub total: usize,
369 pub items: Vec<Sv1ClientInfo>,
370}
371
372#[derive(Serialize, Deserialize, Debug)]
375pub struct RootResponse {
376 pub service: String,
377 pub version: String,
378 pub endpoints: std::collections::BTreeMap<String, String>,
379}
380
381async fn handle_root() -> Json<RootResponse> {
383 let mut endpoints = std::collections::BTreeMap::new();
384 endpoints.insert(
385 routes::ROOT.to_string(),
386 "This endpoint - API listing".to_string(),
387 );
388 endpoints.insert(
389 routes::SWAGGER_UI.to_string(),
390 "Swagger UI (interactive API documentation)".to_string(),
391 );
392 endpoints.insert(
393 routes::OPENAPI_SPEC.to_string(),
394 "OpenAPI specification".to_string(),
395 );
396 endpoints.insert(routes::HEALTH.to_string(), "Health check".to_string());
397 endpoints.insert(routes::GLOBAL.to_string(), "Global statistics".to_string());
398 endpoints.insert(routes::SERVER.to_string(), "Server metadata".to_string());
399 endpoints.insert(
400 routes::SERVER_CHANNELS.to_string(),
401 "Server channels (paginated)".to_string(),
402 );
403 endpoints.insert(
404 routes::CLIENTS.to_string(),
405 "All Sv2 clients metadata (paginated)".to_string(),
406 );
407 endpoints.insert(
408 routes::CLIENT_BY_ID_PATTERN.to_string(),
409 "Single Sv2 client metadata".to_string(),
410 );
411 endpoints.insert(
412 routes::CLIENT_CHANNELS_PATTERN.to_string(),
413 "Sv2 client channels (paginated)".to_string(),
414 );
415 endpoints.insert(
416 routes::SV1_CLIENTS.to_string(),
417 "Sv1 clients (Translator Proxy only, paginated)".to_string(),
418 );
419 endpoints.insert(
420 routes::SV1_CLIENT_BY_ID_PATTERN.to_string(),
421 "Single Sv1 client (Translator Proxy only)".to_string(),
422 );
423 endpoints.insert(
424 routes::METRICS.to_string(),
425 "Prometheus metrics".to_string(),
426 );
427
428 let info = ApiDoc::openapi().info;
431 Json(RootResponse {
432 service: info.title,
433 version: info.version,
434 endpoints,
435 })
436}
437
438#[utoipa::path(
444 get,
445 path = "/api/v1/health",
446 tag = "health",
447 responses(
448 (status = 200, description = "Service is healthy", body = HealthResponse)
449 )
450)]
451async fn handle_health() -> Json<HealthResponse> {
452 Json(HealthResponse {
453 status: "ok".to_string(),
454 timestamp: SystemTime::now()
455 .duration_since(UNIX_EPOCH)
456 .unwrap_or_default()
457 .as_secs(),
458 })
459}
460
461#[utoipa::path(
470 get,
471 path = "/api/v1/global",
472 tag = "global",
473 responses(
474 (status = 200, description = "Global statistics", body = GlobalInfo)
475 )
476)]
477async fn handle_global(State(state): State<ServerState>) -> Json<GlobalInfo> {
478 let uptime_secs = SystemTime::now()
479 .duration_since(UNIX_EPOCH)
480 .unwrap_or_default()
481 .as_secs()
482 - state.start_time;
483
484 let snapshot = state.cache.get_snapshot();
485
486 Json(GlobalInfo {
487 server: snapshot.server_summary,
488 sv2_clients: snapshot.sv2_clients_summary,
489 sv1_clients: snapshot.sv1_clients_summary,
490 uptime_secs,
491 })
492}
493
494#[utoipa::path(
496 get,
497 path = "/api/v1/server",
498 tag = "server",
499 responses(
500 (status = 200, description = "Server metadata", body = ServerResponse),
501 (status = 404, description = "Server monitoring not available", body = ErrorResponse)
502 )
503)]
504async fn handle_server(State(state): State<ServerState>) -> Response {
505 let snapshot = state.cache.get_snapshot();
506
507 match snapshot.server_summary {
508 Some(summary) => Json(ServerResponse {
509 extended_channels_count: summary.extended_channels,
510 standard_channels_count: summary.standard_channels,
511 total_hashrate: summary.total_hashrate,
512 })
513 .into_response(),
514 None => (
515 StatusCode::NOT_FOUND,
516 Json(ErrorResponse {
517 error: "Server monitoring not available".to_string(),
518 }),
519 )
520 .into_response(),
521 }
522}
523
524#[utoipa::path(
526 get,
527 path = "/api/v1/server/channels",
528 tag = "server",
529 params(Pagination),
530 responses(
531 (status = 200, description = "Server channels (paginated)", body = ServerChannelsResponse),
532 (status = 404, description = "Server monitoring not available", body = ErrorResponse)
533 )
534)]
535async fn handle_server_channels(
536 Query(params): Query<Pagination>,
537 State(state): State<ServerState>,
538) -> Response {
539 let snapshot = state.cache.get_snapshot();
540
541 match snapshot.server_info {
542 Some(server) => {
543 let (total_extended, extended_channels) = paginate(&server.extended_channels, ¶ms);
544 let (total_standard, standard_channels) = paginate(&server.standard_channels, ¶ms);
545
546 Json(ServerChannelsResponse {
547 offset: params.offset,
548 limit: params.effective_limit(),
549 total_extended,
550 total_standard,
551 extended_channels,
552 standard_channels,
553 })
554 .into_response()
555 }
556 None => (
557 StatusCode::NOT_FOUND,
558 Json(ErrorResponse {
559 error: "Server monitoring not available".to_string(),
560 }),
561 )
562 .into_response(),
563 }
564}
565
566#[utoipa::path(
569 get,
570 path = "/api/v1/clients",
571 tag = "clients",
572 params(Pagination),
573 responses(
574 (status = 200, description = "List of Sv2 clients (metadata only)", body = Sv2ClientsResponse),
575 (status = 404, description = "Sv2 clients monitoring not available", body = ErrorResponse)
576 )
577)]
578async fn handle_clients(
579 Query(params): Query<Pagination>,
580 State(state): State<ServerState>,
581) -> Response {
582 let snapshot = state.cache.get_snapshot();
583
584 match snapshot.sv2_clients {
585 Some(ref sv2_clients) => {
586 let metadata: Vec<Sv2ClientMetadata> =
587 sv2_clients.iter().map(|c| c.to_metadata()).collect();
588 let (total, items) = paginate(&metadata, ¶ms);
589
590 Json(Sv2ClientsResponse {
591 offset: params.offset,
592 limit: params.effective_limit(),
593 total,
594 items,
595 })
596 .into_response()
597 }
598 None => (
599 StatusCode::NOT_FOUND,
600 Json(ErrorResponse {
601 error: "Sv2 clients monitoring not available".to_string(),
602 }),
603 )
604 .into_response(),
605 }
606}
607
608#[utoipa::path(
610 get,
611 path = "/api/v1/clients/{client_id}",
612 tag = "clients",
613 params(
614 ("client_id" = usize, Path, description = "Sv2 Client ID")
615 ),
616 responses(
617 (status = 200, description = "Sv2 client metadata", body = Sv2ClientResponse),
618 (status = 404, description = "Sv2 client not found", body = ErrorResponse)
619 )
620)]
621async fn handle_client_by_id(
622 Path(client_id): Path<usize>,
623 State(state): State<ServerState>,
624) -> Response {
625 let snapshot = state.cache.get_snapshot();
626
627 let sv2_clients = match snapshot.sv2_clients {
628 Some(ref clients) => clients,
629 None => {
630 return (
631 StatusCode::NOT_FOUND,
632 Json(ErrorResponse {
633 error: "Sv2 clients monitoring not available".to_string(),
634 }),
635 )
636 .into_response();
637 }
638 };
639
640 match sv2_clients.iter().find(|c| c.client_id == client_id) {
641 Some(client) => Json(Sv2ClientResponse {
642 client_id,
643 extended_channels_count: client.extended_channels.len(),
644 standard_channels_count: client.standard_channels.len(),
645 total_hashrate: client.total_hashrate(),
646 })
647 .into_response(),
648 None => (
649 StatusCode::NOT_FOUND,
650 Json(ErrorResponse {
651 error: format!("Sv2 client {} not found", client_id),
652 }),
653 )
654 .into_response(),
655 }
656}
657
658#[utoipa::path(
660 get,
661 path = "/api/v1/clients/{client_id}/channels",
662 tag = "clients",
663 params(
664 ("client_id" = usize, Path, description = "Sv2 Client ID"),
665 Pagination
666 ),
667 responses(
668 (status = 200, description = "Sv2 client channels (paginated)", body = Sv2ClientChannelsResponse),
669 (status = 404, description = "Sv2 client not found", body = ErrorResponse)
670 )
671)]
672async fn handle_client_channels(
673 Path(client_id): Path<usize>,
674 Query(params): Query<Pagination>,
675 State(state): State<ServerState>,
676) -> Response {
677 let snapshot = state.cache.get_snapshot();
678
679 let sv2_clients = match snapshot.sv2_clients {
680 Some(ref clients) => clients,
681 None => {
682 return (
683 StatusCode::NOT_FOUND,
684 Json(ErrorResponse {
685 error: "Sv2 clients monitoring not available".to_string(),
686 }),
687 )
688 .into_response();
689 }
690 };
691
692 match sv2_clients.iter().find(|c| c.client_id == client_id) {
693 Some(client) => {
694 let (total_extended, extended_channels) = paginate(&client.extended_channels, ¶ms);
695 let (total_standard, standard_channels) = paginate(&client.standard_channels, ¶ms);
696
697 Json(Sv2ClientChannelsResponse {
698 client_id,
699 offset: params.offset,
700 limit: params.effective_limit(),
701 total_extended,
702 total_standard,
703 extended_channels,
704 standard_channels,
705 })
706 .into_response()
707 }
708 None => (
709 StatusCode::NOT_FOUND,
710 Json(ErrorResponse {
711 error: format!("Sv2 client {} not found", client_id),
712 }),
713 )
714 .into_response(),
715 }
716}
717
718#[utoipa::path(
720 get,
721 path = "/api/v1/sv1/clients",
722 tag = "sv1",
723 params(Pagination),
724 responses(
725 (status = 200, description = "List of Sv1 clients", body = Sv1ClientsResponse),
726 (status = 404, description = "Sv1 monitoring not available", body = ErrorResponse)
727 )
728)]
729async fn handle_sv1_clients(
730 Query(params): Query<Pagination>,
731 State(state): State<ServerState>,
732) -> Response {
733 let snapshot = state.cache.get_snapshot();
734
735 match snapshot.sv1_clients {
736 Some(ref sv1_clients) => {
737 let (total, items) = paginate(sv1_clients, ¶ms);
738
739 Json(Sv1ClientsResponse {
740 offset: params.offset,
741 limit: params.effective_limit(),
742 total,
743 items,
744 })
745 .into_response()
746 }
747 None => (
748 StatusCode::NOT_FOUND,
749 Json(ErrorResponse {
750 error: "Sv1 client monitoring not available".to_string(),
751 }),
752 )
753 .into_response(),
754 }
755}
756
757#[utoipa::path(
759 get,
760 path = "/api/v1/sv1/clients/{client_id}",
761 tag = "sv1",
762 params(
763 ("client_id" = usize, Path, description = "Sv1 client ID")
764 ),
765 responses(
766 (status = 200, description = "Sv1 client details", body = Sv1ClientInfo),
767 (status = 404, description = "Sv1 client not found", body = ErrorResponse)
768 )
769)]
770async fn handle_sv1_client_by_id(
771 Path(client_id): Path<usize>,
772 State(state): State<ServerState>,
773) -> Response {
774 let snapshot = state.cache.get_snapshot();
775
776 let sv1_clients = match snapshot.sv1_clients {
777 Some(ref clients) => clients,
778 None => {
779 return (
780 StatusCode::NOT_FOUND,
781 Json(ErrorResponse {
782 error: "Sv1 client monitoring not available".to_string(),
783 }),
784 )
785 .into_response();
786 }
787 };
788
789 match sv1_clients.iter().find(|c| c.client_id == client_id) {
790 Some(client) => Json(client.clone()).into_response(),
791 None => (
792 StatusCode::NOT_FOUND,
793 Json(ErrorResponse {
794 error: format!("Sv1 client {} not found", client_id),
795 }),
796 )
797 .into_response(),
798 }
799}
800
801async fn handle_prometheus_metrics(State(state): State<ServerState>) -> Response {
812 let uptime_secs = SystemTime::now()
814 .duration_since(UNIX_EPOCH)
815 .unwrap_or_default()
816 .as_secs()
817 - state.start_time;
818 state.metrics.sv2_uptime_seconds.set(uptime_secs as f64);
819
820 let encoder = TextEncoder::new();
822 let metric_families = state.metrics.registry.gather();
823 let mut buffer = Vec::new();
824
825 match encoder.encode(&metric_families, &mut buffer) {
826 Ok(_) => match String::from_utf8(buffer) {
827 Ok(metrics_text) => (StatusCode::OK, metrics_text).into_response(),
828 Err(e) => (
829 StatusCode::INTERNAL_SERVER_ERROR,
830 Json(ErrorResponse {
831 error: format!("UTF-8 error: {}", e),
832 }),
833 )
834 .into_response(),
835 },
836 Err(e) => (
837 StatusCode::INTERNAL_SERVER_ERROR,
838 Json(ErrorResponse {
839 error: format!("Encoding error: {}", e),
840 }),
841 )
842 .into_response(),
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use crate::monitoring::server::ServerInfo;
850 use axum::body::Body;
851 use http_body_util::BodyExt;
852 use std::{collections::HashMap, sync::Mutex};
853 use stratum_core::mining_sv2::ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE;
854 use tower::ServiceExt;
855
856 fn create_extended_channel_info(channel_id: u32, hashrate: f32) -> ExtendedChannelInfo {
859 ExtendedChannelInfo {
860 channel_id,
861 user_identity: format!("user-ext-{}", channel_id),
862 nominal_hashrate: hashrate,
863 stable_hashrate: false,
864 target_hex: "00ff".into(),
865 requested_max_target_hex: "00ff".into(),
866 extranonce_prefix_hex: "aa".into(),
867 full_extranonce_size: 16,
868 rollable_extranonce_size: 4,
869 expected_shares_per_minute: 1.0,
870 shares_accepted: 10,
871 shares_rejected: 0,
872 shares_rejected_by_reason: HashMap::new(),
873 share_work_sum: 100.0,
874 last_share_sequence_number: 5,
875 best_diff: 50.0,
876 last_batch_accepted: 3,
877 last_batch_work_sum: 30,
878 share_batch_size: 10,
879 blocks_found: 0,
880 }
881 }
882
883 fn create_standard_channel_info(channel_id: u32, hashrate: f32) -> StandardChannelInfo {
884 StandardChannelInfo {
885 channel_id,
886 user_identity: format!("user-std-{}", channel_id),
887 nominal_hashrate: hashrate,
888 stable_hashrate: false,
889 target_hex: "00ff".into(),
890 requested_max_target_hex: "00ff".into(),
891 extranonce_prefix_hex: "bb".into(),
892 expected_shares_per_minute: 2.0,
893 shares_accepted: 20,
894 shares_rejected: 1,
895 shares_rejected_by_reason: HashMap::from([(
896 ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE.to_string(),
897 1,
898 )]),
899 share_work_sum: 200.0,
900 last_share_sequence_number: 8,
901 best_diff: 80.0,
902 last_batch_accepted: 5,
903 last_batch_work_sum: 50,
904 share_batch_size: 20,
905 blocks_found: 0,
906 }
907 }
908
909 fn create_server_extended_channel_info(
910 channel_id: u32,
911 hashrate: Option<f32>,
912 ) -> ServerExtendedChannelInfo {
913 ServerExtendedChannelInfo {
914 channel_id,
915 user_identity: format!("pool-ext-{}", channel_id),
916 nominal_hashrate: hashrate,
917 target_hex: "00ff".into(),
918 extranonce_prefix_hex: "aa".into(),
919 full_extranonce_size: 16,
920 rollable_extranonce_size: 4,
921 version_rolling: true,
922 shares_acknowledged: 10,
923 shares_rejected: 0,
924 shares_rejected_by_reason: HashMap::new(),
925 acknowledged_work_sum: 100,
926 validated_work_sum: 100.0,
927 shares_submitted: 12,
928 best_diff: 50.0,
929 blocks_found: 0,
930 }
931 }
932
933 fn create_server_standard_channel_info(
934 channel_id: u32,
935 hashrate: Option<f32>,
936 ) -> ServerStandardChannelInfo {
937 ServerStandardChannelInfo {
938 channel_id,
939 user_identity: format!("pool-std-{}", channel_id),
940 nominal_hashrate: hashrate,
941 target_hex: "00ff".into(),
942 extranonce_prefix_hex: "bb".into(),
943 shares_acknowledged: 20,
944 shares_submitted: 22,
945 shares_rejected: 1,
946 shares_rejected_by_reason: HashMap::from([(
947 ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE.to_string(),
948 1,
949 )]),
950 acknowledged_work_sum: 200,
951 validated_work_sum: 200.0,
952 best_diff: 80.0,
953 blocks_found: 0,
954 }
955 }
956
957 fn create_sv1_client_info(id: usize, hashrate: Option<f32>) -> Sv1ClientInfo {
958 Sv1ClientInfo {
959 client_id: id,
960 channel_id: Some(id as u32),
961 authorized_worker_name: format!("worker-{}", id),
962 user_identity: format!("miner-{}", id),
963 target_hex: "00ff".into(),
964 hashrate,
965 stable_hashrate: false,
966 extranonce1_hex: "aabb".into(),
967 extranonce2_len: 8,
968 version_rolling_mask: Some("ffffffff".into()),
969 version_rolling_min_bit: Some("00000000".into()),
970 }
971 }
972
973 struct MockServer(ServerInfo);
974 impl ServerMonitoring for MockServer {
975 fn get_server(&self) -> ServerInfo {
976 self.0.clone()
977 }
978 }
979
980 struct MockClients(Vec<Sv2ClientInfo>);
981 impl Sv2ClientsMonitoring for MockClients {
982 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo> {
983 self.0.clone()
984 }
985 }
986
987 struct MockSv1Clients(Vec<Sv1ClientInfo>);
988 impl Sv1ClientsMonitoring for MockSv1Clients {
989 fn get_sv1_clients(&self) -> Vec<Sv1ClientInfo> {
990 self.0.clone()
991 }
992 }
993
994 fn build_test_app(
996 server: Option<Arc<dyn ServerMonitoring + Send + Sync>>,
997 clients: Option<Arc<dyn Sv2ClientsMonitoring + Send + Sync>>,
998 sv1: Option<Arc<dyn Sv1ClientsMonitoring + Send + Sync>>,
999 ) -> Router {
1000 let has_server = server.is_some();
1001 let has_clients = clients.is_some();
1002 let has_sv1 = sv1.is_some();
1003
1004 let metrics = PrometheusMetrics::new(has_server, has_clients, has_sv1).unwrap();
1005
1006 let cache = Arc::new(
1007 SnapshotCache::new(Duration::from_secs(60), server, clients)
1008 .with_metrics(metrics.clone()),
1009 );
1010
1011 let cache = if let Some(sv1_source) = sv1 {
1012 Arc::new(
1013 Arc::try_unwrap(cache)
1014 .unwrap_or_else(|arc| (*arc).clone())
1015 .with_sv1_clients_source(sv1_source),
1016 )
1017 } else {
1018 cache
1019 };
1020
1021 cache.refresh();
1022
1023 let start_time = SystemTime::now()
1024 .duration_since(UNIX_EPOCH)
1025 .unwrap_or_default()
1026 .as_secs();
1027
1028 let state = ServerState {
1029 cache,
1030 start_time,
1031 metrics,
1032 };
1033
1034 let api_v1 = Router::new()
1035 .route(routes::segments::HEALTH, get(handle_health))
1036 .route(routes::segments::GLOBAL, get(handle_global))
1037 .route(routes::segments::SERVER, get(handle_server))
1038 .route(
1039 routes::segments::SERVER_CHANNELS,
1040 get(handle_server_channels),
1041 )
1042 .route(routes::segments::CLIENTS, get(handle_clients))
1043 .route(routes::segments::CLIENT_BY_ID, get(handle_client_by_id))
1044 .route(
1045 routes::segments::CLIENT_CHANNELS,
1046 get(handle_client_channels),
1047 )
1048 .route(routes::segments::SV1_CLIENTS, get(handle_sv1_clients))
1049 .route(
1050 routes::segments::SV1_CLIENT_BY_ID,
1051 get(handle_sv1_client_by_id),
1052 );
1053
1054 Router::new()
1055 .route(routes::ROOT, get(handle_root))
1056 .nest(routes::API_V1_PREFIX, api_v1)
1057 .route(routes::METRICS, get(handle_prometheus_metrics))
1058 .with_state(state)
1059 }
1060
1061 async fn get_body(response: axum::response::Response) -> String {
1062 let body = response.into_body();
1063 let bytes = body.collect().await.unwrap().to_bytes();
1064 String::from_utf8(bytes.to_vec()).unwrap()
1065 }
1066
1067 fn make_request(uri: &str) -> axum::http::Request<Body> {
1068 axum::http::Request::builder()
1069 .uri(uri)
1070 .body(Body::empty())
1071 .unwrap()
1072 }
1073
1074 #[test]
1077 fn pagination_effective_limit_default() {
1078 let p = Pagination {
1079 offset: 0,
1080 limit: None,
1081 };
1082 assert_eq!(p.effective_limit(), DEFAULT_LIMIT);
1083 }
1084
1085 #[test]
1086 fn pagination_effective_limit_capped_at_max() {
1087 let p = Pagination {
1088 offset: 0,
1089 limit: Some(500),
1090 };
1091 assert_eq!(p.effective_limit(), MAX_LIMIT);
1092 }
1093
1094 #[test]
1095 fn pagination_effective_limit_respects_small_value() {
1096 let p = Pagination {
1097 offset: 0,
1098 limit: Some(5),
1099 };
1100 assert_eq!(p.effective_limit(), 5);
1101 }
1102
1103 #[test]
1104 fn paginate_empty_slice() {
1105 let items: Vec<i32> = vec![];
1106 let params = Pagination {
1107 offset: 0,
1108 limit: Some(10),
1109 };
1110 let (total, result) = paginate(&items, ¶ms);
1111 assert_eq!(total, 0);
1112 assert!(result.is_empty());
1113 }
1114
1115 #[test]
1116 fn paginate_basic() {
1117 let items: Vec<i32> = (0..50).collect();
1118 let params = Pagination {
1119 offset: 10,
1120 limit: Some(5),
1121 };
1122 let (total, result) = paginate(&items, ¶ms);
1123 assert_eq!(total, 50);
1124 assert_eq!(result, vec![10, 11, 12, 13, 14]);
1125 }
1126
1127 #[test]
1128 fn paginate_offset_beyond_length() {
1129 let items: Vec<i32> = vec![1, 2, 3];
1130 let params = Pagination {
1131 offset: 100,
1132 limit: Some(10),
1133 };
1134 let (total, result) = paginate(&items, ¶ms);
1135 assert_eq!(total, 3);
1136 assert!(result.is_empty());
1137 }
1138
1139 #[test]
1140 fn paginate_limit_exceeds_remaining() {
1141 let items: Vec<i32> = vec![1, 2, 3, 4, 5];
1142 let params = Pagination {
1143 offset: 3,
1144 limit: Some(10),
1145 };
1146 let (total, result) = paginate(&items, ¶ms);
1147 assert_eq!(total, 5);
1148 assert_eq!(result, vec![4, 5]);
1149 }
1150
1151 #[tokio::test]
1154 async fn health_endpoint_returns_ok() {
1155 let app = build_test_app(None, None, None);
1156 let response = app.oneshot(make_request(routes::HEALTH)).await.unwrap();
1157 assert_eq!(response.status(), StatusCode::OK);
1158
1159 let body = get_body(response).await;
1160 let resp: HealthResponse = serde_json::from_str(&body).unwrap();
1161 assert_eq!(resp.status, "ok");
1162 }
1163
1164 #[tokio::test]
1165 async fn root_endpoint_lists_endpoints() {
1166 let app = build_test_app(None, None, None);
1167 let response = app.oneshot(make_request(routes::ROOT)).await.unwrap();
1168 assert_eq!(response.status(), StatusCode::OK);
1169
1170 let body = get_body(response).await;
1171 let resp: RootResponse = serde_json::from_str(&body).unwrap();
1172 assert_eq!(resp.service, ApiDoc::openapi().info.title);
1173 assert!(resp.endpoints.contains_key(routes::HEALTH));
1174 }
1175
1176 #[tokio::test]
1177 async fn run_returns_error_when_bind_address_is_in_use() {
1178 let occupied_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1179 let occupied_addr = occupied_listener.local_addr().unwrap();
1180 let server =
1181 MonitoringServer::new(occupied_addr, None, None, Duration::from_millis(10)).unwrap();
1182
1183 let err = server.run(std::future::pending()).await.unwrap_err();
1184
1185 assert_eq!(
1186 err.downcast_ref::<std::io::Error>()
1187 .map(std::io::Error::kind),
1188 Some(std::io::ErrorKind::AddrInUse)
1189 );
1190 }
1191
1192 #[tokio::test]
1193 async fn global_endpoint_with_no_sources() {
1194 let app = build_test_app(None, None, None);
1195 let response = app.oneshot(make_request(routes::GLOBAL)).await.unwrap();
1196 assert_eq!(response.status(), StatusCode::OK);
1197
1198 let body = get_body(response).await;
1199 let resp: GlobalInfo = serde_json::from_str(&body).unwrap();
1200 assert!(resp.server.is_none());
1201 assert!(resp.sv2_clients.is_none());
1202 }
1203
1204 #[tokio::test]
1205 async fn global_endpoint_with_data() {
1206 let server = Arc::new(MockServer(ServerInfo {
1207 extended_channels: vec![create_server_extended_channel_info(1, Some(100.0))],
1208 standard_channels: vec![],
1209 }));
1210 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1211 client_id: 1,
1212 extended_channels: vec![create_extended_channel_info(1, 50.0)],
1213 standard_channels: vec![],
1214 }]));
1215
1216 let app = build_test_app(
1217 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1218 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1219 None,
1220 );
1221 let response = app.oneshot(make_request(routes::GLOBAL)).await.unwrap();
1222 assert_eq!(response.status(), StatusCode::OK);
1223
1224 let body = get_body(response).await;
1225 let resp: GlobalInfo = serde_json::from_str(&body).unwrap();
1226 assert_eq!(resp.server.as_ref().unwrap().extended_channels, 1);
1227 assert_eq!(resp.sv2_clients.as_ref().unwrap().total_clients, 1);
1228 }
1229
1230 #[tokio::test]
1231 async fn server_endpoint_not_available() {
1232 let app = build_test_app(None, None, None);
1233 let response = app.oneshot(make_request(routes::SERVER)).await.unwrap();
1234 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1235 }
1236
1237 #[tokio::test]
1238 async fn server_endpoint_with_data() {
1239 let server = Arc::new(MockServer(ServerInfo {
1240 extended_channels: vec![create_server_extended_channel_info(1, Some(100.0))],
1241 standard_channels: vec![create_server_standard_channel_info(2, Some(50.0))],
1242 }));
1243
1244 let app = build_test_app(
1245 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1246 None,
1247 None,
1248 );
1249 let response = app.oneshot(make_request(routes::SERVER)).await.unwrap();
1250 assert_eq!(response.status(), StatusCode::OK);
1251
1252 let body = get_body(response).await;
1253 let resp: ServerResponse = serde_json::from_str(&body).unwrap();
1254 assert_eq!(resp.extended_channels_count, 1);
1255 assert_eq!(resp.standard_channels_count, 1);
1256 }
1257
1258 #[tokio::test]
1259 async fn server_channels_endpoint_with_pagination() {
1260 let server = Arc::new(MockServer(ServerInfo {
1261 extended_channels: vec![
1262 create_server_extended_channel_info(1, Some(100.0)),
1263 create_server_extended_channel_info(2, Some(200.0)),
1264 create_server_extended_channel_info(3, Some(300.0)),
1265 ],
1266 standard_channels: vec![],
1267 }));
1268
1269 let app = build_test_app(
1270 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1271 None,
1272 None,
1273 );
1274 let response = app
1275 .oneshot(make_request(&format!(
1276 "{}?offset=1&limit=1",
1277 routes::SERVER_CHANNELS
1278 )))
1279 .await
1280 .unwrap();
1281 assert_eq!(response.status(), StatusCode::OK);
1282
1283 let body = get_body(response).await;
1284 let resp: ServerChannelsResponse = serde_json::from_str(&body).unwrap();
1285 assert_eq!(resp.total_extended, 3);
1286 assert_eq!(resp.offset, 1);
1287 assert_eq!(resp.limit, 1);
1288 assert_eq!(resp.extended_channels.len(), 1);
1289 }
1290
1291 #[tokio::test]
1292 async fn server_channels_endpoint_keeps_rejected_shares_total_compatible() {
1293 let server = Arc::new(MockServer(ServerInfo {
1294 extended_channels: vec![],
1295 standard_channels: vec![create_server_standard_channel_info(1, Some(50.0))],
1296 }));
1297
1298 let app = build_test_app(
1299 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1300 None,
1301 None,
1302 );
1303 let response = app
1304 .oneshot(make_request(routes::SERVER_CHANNELS))
1305 .await
1306 .unwrap();
1307 assert_eq!(response.status(), StatusCode::OK);
1308
1309 let body = get_body(response).await;
1310 let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1311 let channel = &json["standard_channels"][0];
1312
1313 assert_eq!(channel["shares_rejected"], 1);
1314 assert_eq!(
1315 channel["shares_rejected_by_reason"][ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE],
1316 1
1317 );
1318 }
1319
1320 #[tokio::test]
1321 async fn clients_endpoint_not_available() {
1322 let app = build_test_app(None, None, None);
1323 let response = app.oneshot(make_request(routes::CLIENTS)).await.unwrap();
1324 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1325 }
1326
1327 #[tokio::test]
1328 async fn clients_endpoint_returns_metadata() {
1329 let clients = Arc::new(MockClients(vec![
1330 Sv2ClientInfo {
1331 client_id: 1,
1332 extended_channels: vec![create_extended_channel_info(1, 100.0)],
1333 standard_channels: vec![],
1334 },
1335 Sv2ClientInfo {
1336 client_id: 2,
1337 extended_channels: vec![],
1338 standard_channels: vec![create_standard_channel_info(1, 50.0)],
1339 },
1340 ]));
1341
1342 let app = build_test_app(
1343 None,
1344 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1345 None,
1346 );
1347 let response = app.oneshot(make_request(routes::CLIENTS)).await.unwrap();
1348 assert_eq!(response.status(), StatusCode::OK);
1349
1350 let body = get_body(response).await;
1351 let resp: Sv2ClientsResponse = serde_json::from_str(&body).unwrap();
1352 assert_eq!(resp.total, 2);
1353 assert_eq!(resp.items.len(), 2);
1354 assert_eq!(resp.items[0].client_id, 1);
1355 }
1356
1357 #[tokio::test]
1358 async fn client_by_id_found() {
1359 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1360 client_id: 42,
1361 extended_channels: vec![create_extended_channel_info(1, 100.0)],
1362 standard_channels: vec![create_standard_channel_info(2, 50.0)],
1363 }]));
1364
1365 let app = build_test_app(
1366 None,
1367 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1368 None,
1369 );
1370 let response = app
1371 .oneshot(make_request(&routes::client_by_id(42)))
1372 .await
1373 .unwrap();
1374 assert_eq!(response.status(), StatusCode::OK);
1375
1376 let body = get_body(response).await;
1377 let resp: Sv2ClientResponse = serde_json::from_str(&body).unwrap();
1378 assert_eq!(resp.client_id, 42);
1379 assert_eq!(resp.extended_channels_count, 1);
1380 assert_eq!(resp.standard_channels_count, 1);
1381 }
1382
1383 #[tokio::test]
1384 async fn client_by_id_not_found() {
1385 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1386 client_id: 1,
1387 extended_channels: vec![],
1388 standard_channels: vec![],
1389 }]));
1390
1391 let app = build_test_app(
1392 None,
1393 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1394 None,
1395 );
1396 let response = app
1397 .oneshot(make_request(&routes::client_by_id(999)))
1398 .await
1399 .unwrap();
1400 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1401 }
1402
1403 #[tokio::test]
1404 async fn client_channels_with_pagination() {
1405 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1406 client_id: 1,
1407 extended_channels: vec![
1408 create_extended_channel_info(10, 100.0),
1409 create_extended_channel_info(11, 200.0),
1410 create_extended_channel_info(12, 300.0),
1411 ],
1412 standard_channels: vec![create_standard_channel_info(20, 50.0)],
1413 }]));
1414
1415 let app = build_test_app(
1416 None,
1417 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1418 None,
1419 );
1420 let response = app
1421 .oneshot(make_request(&format!(
1422 "{}?offset=1&limit=2",
1423 routes::client_channels(1)
1424 )))
1425 .await
1426 .unwrap();
1427 assert_eq!(response.status(), StatusCode::OK);
1428
1429 let body = get_body(response).await;
1430 let resp: Sv2ClientChannelsResponse = serde_json::from_str(&body).unwrap();
1431 assert_eq!(resp.client_id, 1);
1432 assert_eq!(resp.total_extended, 3);
1433 assert_eq!(resp.total_standard, 1);
1434 assert_eq!(resp.extended_channels.len(), 2);
1435 }
1436
1437 #[tokio::test]
1438 async fn sv1_clients_not_available() {
1439 let app = build_test_app(None, None, None);
1440 let response = app
1441 .oneshot(make_request(routes::SV1_CLIENTS))
1442 .await
1443 .unwrap();
1444 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1445 }
1446
1447 #[tokio::test]
1448 async fn sv1_clients_with_data() {
1449 let sv1 = Arc::new(MockSv1Clients(vec![
1450 create_sv1_client_info(1, Some(100.0)),
1451 create_sv1_client_info(2, Some(200.0)),
1452 ]));
1453
1454 let app = build_test_app(
1455 None,
1456 None,
1457 Some(sv1 as Arc<dyn Sv1ClientsMonitoring + Send + Sync>),
1458 );
1459 let response = app
1460 .oneshot(make_request(routes::SV1_CLIENTS))
1461 .await
1462 .unwrap();
1463 assert_eq!(response.status(), StatusCode::OK);
1464
1465 let body = get_body(response).await;
1466 let resp: Sv1ClientsResponse = serde_json::from_str(&body).unwrap();
1467 assert_eq!(resp.total, 2);
1468 assert_eq!(resp.items.len(), 2);
1469 }
1470
1471 #[tokio::test]
1472 async fn sv1_client_by_id_found() {
1473 let sv1 = Arc::new(MockSv1Clients(vec![create_sv1_client_info(7, Some(500.0))]));
1474
1475 let app = build_test_app(
1476 None,
1477 None,
1478 Some(sv1 as Arc<dyn Sv1ClientsMonitoring + Send + Sync>),
1479 );
1480 let response = app
1481 .oneshot(make_request(&routes::sv1_client_by_id(7)))
1482 .await
1483 .unwrap();
1484 assert_eq!(response.status(), StatusCode::OK);
1485
1486 let body = get_body(response).await;
1487 let resp: Sv1ClientInfo = serde_json::from_str(&body).unwrap();
1488 assert_eq!(resp.client_id, 7);
1489 }
1490
1491 #[tokio::test]
1492 async fn sv1_client_by_id_not_found() {
1493 let sv1 = Arc::new(MockSv1Clients(vec![create_sv1_client_info(1, Some(100.0))]));
1494
1495 let app = build_test_app(
1496 None,
1497 None,
1498 Some(sv1 as Arc<dyn Sv1ClientsMonitoring + Send + Sync>),
1499 );
1500 let response = app
1501 .oneshot(make_request(&routes::sv1_client_by_id(999)))
1502 .await
1503 .unwrap();
1504 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1505 }
1506
1507 #[tokio::test]
1508 async fn metrics_endpoint_returns_prometheus_format() {
1509 let server = Arc::new(MockServer(ServerInfo {
1510 extended_channels: vec![create_server_extended_channel_info(1, Some(100.0))],
1511 standard_channels: vec![],
1512 }));
1513 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1514 client_id: 1,
1515 extended_channels: vec![create_extended_channel_info(1, 50.0)],
1516 standard_channels: vec![],
1517 }]));
1518
1519 let app = build_test_app(
1520 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1521 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1522 None,
1523 );
1524 let response = app.oneshot(make_request(routes::METRICS)).await.unwrap();
1525 assert_eq!(response.status(), StatusCode::OK);
1526
1527 let body = get_body(response).await;
1528 assert!(body.contains("sv2_uptime_seconds"));
1529 assert!(body.contains("sv2_server_channels"));
1530 assert!(body.contains("sv2_clients_total"));
1531 }
1532
1533 #[tokio::test]
1534 async fn metrics_endpoint_with_no_sources() {
1535 let app = build_test_app(None, None, None);
1536 let response = app.oneshot(make_request(routes::METRICS)).await.unwrap();
1537 assert_eq!(response.status(), StatusCode::OK);
1538
1539 let body = get_body(response).await;
1540 assert!(body.contains("sv2_uptime_seconds"));
1542 assert!(!body.contains("sv2_server_channels"));
1544 assert!(!body.contains("sv2_clients_total"));
1545 }
1546
1547 struct MutableMockClients(Mutex<Vec<Sv2ClientInfo>>);
1549 impl Sv2ClientsMonitoring for MutableMockClients {
1550 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo> {
1551 self.0.lock().unwrap().clone()
1552 }
1553 }
1554
1555 #[tokio::test]
1562 async fn metrics_stale_labels_removed_without_reset_gap() {
1563 let mut channel_2 = create_extended_channel_info(2, 200.0);
1564 channel_2.shares_rejected = 1;
1565 channel_2.shares_rejected_by_reason =
1566 HashMap::from([(ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE.to_string(), 1)]);
1567
1568 let initial_clients = vec![Sv2ClientInfo {
1569 client_id: 1,
1570 extended_channels: vec![create_extended_channel_info(1, 100.0), channel_2],
1571 standard_channels: vec![],
1572 }];
1573
1574 let mock_clients = Arc::new(MutableMockClients(Mutex::new(initial_clients)));
1575 let metrics = PrometheusMetrics::new(false, true, false).unwrap();
1576 let cache = Arc::new(
1577 SnapshotCache::new(
1578 Duration::from_secs(60),
1579 None,
1580 Some(mock_clients.clone() as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1581 )
1582 .with_metrics(metrics.clone()),
1583 );
1584 cache.refresh();
1585
1586 let start_time = SystemTime::now()
1587 .duration_since(UNIX_EPOCH)
1588 .unwrap_or_default()
1589 .as_secs();
1590
1591 let state = ServerState {
1592 cache: cache.clone(),
1593 start_time,
1594 metrics,
1595 };
1596
1597 let app = Router::new()
1598 .route(routes::METRICS, get(handle_prometheus_metrics))
1599 .with_state(state);
1600
1601 let response = app
1603 .clone()
1604 .oneshot(make_request(routes::METRICS))
1605 .await
1606 .unwrap();
1607 let body = get_body(response).await;
1608 assert!(
1610 body.contains("sv2_client_shares_accepted_total{channel_id=\"1\",client_id=\"1\""),
1611 "Channel 1 should be present on first scrape"
1612 );
1613 assert!(
1614 body.contains("sv2_client_shares_accepted_total{channel_id=\"2\",client_id=\"1\""),
1615 "Channel 2 should be present on first scrape"
1616 );
1617 assert!(
1618 body.contains("sv2_client_shares_rejected_total{channel_id=\"2\",client_id=\"1\",error_code=\"duplicate-share\""),
1619 "Channel 2 rejected-share metric should be present on first scrape"
1620 );
1621
1622 {
1624 let mut clients = mock_clients.0.lock().unwrap();
1625 clients[0].extended_channels.retain(|c| c.channel_id == 1);
1626 }
1627 cache.refresh();
1628
1629 let response = app
1631 .clone()
1632 .oneshot(make_request(routes::METRICS))
1633 .await
1634 .unwrap();
1635 let body = get_body(response).await;
1636 assert!(
1637 body.contains("sv2_client_shares_accepted_total{channel_id=\"1\",client_id=\"1\""),
1638 "Channel 1 should still be present after stale removal"
1639 );
1640 assert!(
1641 !body.contains("sv2_client_shares_accepted_total{channel_id=\"2\",client_id=\"1\""),
1642 "Channel 2 should be removed as stale"
1643 );
1644 assert!(
1645 !body.contains("sv2_client_shares_rejected_total{channel_id=\"2\",client_id=\"1\",error_code=\"duplicate-share\""),
1646 "Channel 2 rejected-share metric should be removed as stale"
1647 );
1648 }
1649
1650 #[tokio::test]
1662 async fn shares_rejected_metric_emitted_with_zero_rejections() {
1663 let mut server_info = super::super::server::ServerInfo {
1666 extended_channels: vec![create_server_extended_channel_info(1, Some(100.0))],
1667 standard_channels: vec![create_server_standard_channel_info(2, Some(50.0))],
1668 };
1669 server_info.standard_channels[0].shares_rejected = 0;
1670 server_info.standard_channels[0].shares_rejected_by_reason = HashMap::new();
1671
1672 let server = Arc::new(MockServer(server_info));
1673
1674 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1676 client_id: 1,
1677 extended_channels: vec![create_extended_channel_info(1, 100.0)],
1678 standard_channels: vec![],
1679 }]));
1680
1681 let app = build_test_app(
1682 Some(server as Arc<dyn ServerMonitoring + Send + Sync>),
1683 Some(clients as Arc<dyn super::super::client::Sv2ClientsMonitoring + Send + Sync>),
1684 None,
1685 );
1686
1687 let response = app.oneshot(make_request(routes::METRICS)).await.unwrap();
1688 let body = get_body(response).await;
1689
1690 assert!(
1692 body.contains("sv2_server_shares_rejected_total{channel_id=\"1\",error_code=\"stale-share\""),
1693 "sv2_server_shares_rejected_total stale-share label must be pre-seeded to 0; got:\n{body}"
1694 );
1695 assert!(
1696 body.contains("sv2_server_shares_rejected_total{channel_id=\"1\",error_code=\"duplicate-share\""),
1697 "sv2_server_shares_rejected_total duplicate-share label must be pre-seeded to 0; got:\n{body}"
1698 );
1699 assert!(
1700 body.contains("sv2_client_shares_rejected_total{channel_id=\"1\",client_id=\"1\",error_code=\"stale-share\""),
1701 "sv2_client_shares_rejected_total stale-share label must be pre-seeded to 0; got:\n{body}"
1702 );
1703 }
1704
1705 #[test]
1708 fn paginate_with_limit_zero() {
1709 let items: Vec<i32> = (0..50).collect();
1711 let params = Pagination {
1712 offset: 0,
1713 limit: Some(0),
1714 };
1715 let (total, result) = paginate(&items, ¶ms);
1716 assert_eq!(total, 50);
1717 assert!(result.is_empty(), "limit=0 should return no items");
1718 }
1719
1720 #[tokio::test]
1721 async fn server_channels_not_available() {
1722 let app = build_test_app(None, None, None);
1723 let response = app
1724 .oneshot(make_request(routes::SERVER_CHANNELS))
1725 .await
1726 .unwrap();
1727 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1728
1729 let body = get_body(response).await;
1730 let resp: ErrorResponse = serde_json::from_str(&body).unwrap();
1731 assert!(!resp.error.is_empty());
1732 }
1733
1734 #[tokio::test]
1735 async fn client_by_id_no_monitoring() {
1736 let app = build_test_app(None, None, None);
1738 let response = app
1739 .oneshot(make_request(&routes::client_by_id(1)))
1740 .await
1741 .unwrap();
1742 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1743
1744 let body = get_body(response).await;
1745 let resp: ErrorResponse = serde_json::from_str(&body).unwrap();
1746 assert!(!resp.error.is_empty());
1747 }
1748
1749 #[tokio::test]
1750 async fn client_channels_client_not_found() {
1751 let clients = Arc::new(MockClients(vec![Sv2ClientInfo {
1753 client_id: 1,
1754 extended_channels: vec![],
1755 standard_channels: vec![],
1756 }]));
1757
1758 let app = build_test_app(
1759 None,
1760 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1761 None,
1762 );
1763 let response = app
1764 .oneshot(make_request(&routes::client_channels(999)))
1765 .await
1766 .unwrap();
1767 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1768
1769 let body = get_body(response).await;
1770 let resp: ErrorResponse = serde_json::from_str(&body).unwrap();
1771 assert!(resp.error.contains("999"));
1772 }
1773
1774 #[tokio::test]
1775 async fn client_channels_no_monitoring() {
1776 let app = build_test_app(None, None, None);
1778 let response = app
1779 .oneshot(make_request(&routes::client_channels(1)))
1780 .await
1781 .unwrap();
1782 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1783 }
1784
1785 #[tokio::test]
1786 async fn sv1_client_by_id_no_monitoring() {
1787 let app = build_test_app(None, None, None);
1789 let response = app
1790 .oneshot(make_request(&routes::sv1_client_by_id(1)))
1791 .await
1792 .unwrap();
1793 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1794
1795 let body = get_body(response).await;
1796 let resp: ErrorResponse = serde_json::from_str(&body).unwrap();
1797 assert!(!resp.error.is_empty());
1798 }
1799
1800 #[tokio::test]
1801 async fn clients_pagination_offset_and_limit() {
1802 let clients = Arc::new(MockClients(vec![
1803 Sv2ClientInfo {
1804 client_id: 1,
1805 extended_channels: vec![create_extended_channel_info(1, 100.0)],
1806 standard_channels: vec![],
1807 },
1808 Sv2ClientInfo {
1809 client_id: 2,
1810 extended_channels: vec![],
1811 standard_channels: vec![create_standard_channel_info(1, 50.0)],
1812 },
1813 Sv2ClientInfo {
1814 client_id: 3,
1815 extended_channels: vec![create_extended_channel_info(2, 200.0)],
1816 standard_channels: vec![],
1817 },
1818 ]));
1819
1820 let app = build_test_app(
1821 None,
1822 Some(clients as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
1823 None,
1824 );
1825 let response = app
1826 .oneshot(make_request(&format!(
1827 "{}?offset=1&limit=1",
1828 routes::CLIENTS
1829 )))
1830 .await
1831 .unwrap();
1832 assert_eq!(response.status(), StatusCode::OK);
1833
1834 let body = get_body(response).await;
1835 let resp: Sv2ClientsResponse = serde_json::from_str(&body).unwrap();
1836 assert_eq!(resp.total, 3);
1837 assert_eq!(resp.offset, 1);
1838 assert_eq!(resp.limit, 1);
1839 assert_eq!(resp.items.len(), 1);
1840 assert_eq!(resp.items[0].client_id, 2);
1841 }
1842
1843 #[tokio::test]
1844 async fn sv1_clients_pagination() {
1845 let sv1 = Arc::new(MockSv1Clients(vec![
1846 create_sv1_client_info(1, Some(100.0)),
1847 create_sv1_client_info(2, Some(200.0)),
1848 create_sv1_client_info(3, Some(300.0)),
1849 ]));
1850
1851 let app = build_test_app(
1852 None,
1853 None,
1854 Some(sv1 as Arc<dyn Sv1ClientsMonitoring + Send + Sync>),
1855 );
1856 let response = app
1857 .oneshot(make_request(&format!(
1858 "{}?offset=2&limit=10",
1859 routes::SV1_CLIENTS
1860 )))
1861 .await
1862 .unwrap();
1863 assert_eq!(response.status(), StatusCode::OK);
1864
1865 let body = get_body(response).await;
1866 let resp: Sv1ClientsResponse = serde_json::from_str(&body).unwrap();
1867 assert_eq!(resp.total, 3);
1868 assert_eq!(resp.offset, 2);
1869 assert_eq!(resp.items.len(), 1);
1870 assert_eq!(resp.items[0].client_id, 3);
1871 }
1872
1873 #[tokio::test]
1874 async fn global_endpoint_with_sv1_data() {
1875 let sv1 = Arc::new(MockSv1Clients(vec![create_sv1_client_info(1, Some(100.0))]));
1876
1877 let app = build_test_app(
1878 None,
1879 None,
1880 Some(sv1 as Arc<dyn Sv1ClientsMonitoring + Send + Sync>),
1881 );
1882 let response = app.oneshot(make_request(routes::GLOBAL)).await.unwrap();
1883 assert_eq!(response.status(), StatusCode::OK);
1884
1885 let body = get_body(response).await;
1886 let resp: GlobalInfo = serde_json::from_str(&body).unwrap();
1887 assert!(resp.server.is_none());
1889 assert!(resp.sv2_clients.is_none());
1890 assert_eq!(resp.sv1_clients.as_ref().unwrap().total_clients, 1);
1892 }
1893}