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