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