Add miner-telemetry crate and integrate telemetry support

- Introduce miner-telemetry crate for WebSocket-based telemetry
reporting - Add telemetry configuration options to miner-cli CLI - Pass
telemetry CLI args as environment variables for miner-service - Enable
miner-service to bootstrap telemetry from env vars and emit events -
Emit periodic system.interval telemetry with mining stats and metadata -
Add miner-telemetry as dependency to miner-service and workspace member
This commit is contained in:
rob thijssen
2025-09-17 14:26:51 +08:00
parent 05a51fe5df
commit 1103eb3efe
7 changed files with 1075 additions and 14 deletions
Generated
+380 -14
View File
@@ -157,6 +157,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byte-slice-cast"
version = "1.2.3"
@@ -181,6 +187,16 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
@@ -339,7 +355,7 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -538,6 +554,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
[[package]]
name = "find_cuda_helper"
version = "0.2.0"
@@ -554,7 +576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
dependencies = [
"byteorder",
"rand",
"rand 0.8.5",
"rustc-hex",
"static_assertions",
]
@@ -586,6 +608,21 @@ version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -602,6 +639,34 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.103",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -620,9 +685,13 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@@ -646,7 +715,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.7+wasi-0.2.4",
]
[[package]]
@@ -972,6 +1053,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6247da8b8658ad4e73a186e747fcc5fc2a29f979d6fe6269127fdb5fd08298d0"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.5"
@@ -1081,6 +1172,7 @@ dependencies = [
"hex",
"log",
"metrics",
"miner-telemetry",
"num_cpus",
"pow-core",
"primitive-types",
@@ -1092,6 +1184,21 @@ dependencies = [
"warp",
]
[[package]]
name = "miner-telemetry"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.31",
"log",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite 0.23.1",
"url",
"uuid",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1114,7 +1221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -1150,7 +1257,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits 0.2.19",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -1215,7 +1322,7 @@ version = "0.19.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4297afb442d411793e4e24ee5a2977d15b6c95c743418f1c0ce0a2397d7ec8a3"
dependencies = [
"futures",
"futures 0.1.31",
"nodrop",
"num-traits 0.2.19",
"ocl-core",
@@ -1441,9 +1548,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11778238e7d8b0e3ca62033fdc69e01ef5cdb08809cdc2398b2ce5ec873a1757"
dependencies = [
"crossbeam",
"futures",
"futures 0.1.31",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "radium"
version = "0.7.0"
@@ -1457,8 +1570,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
@@ -1468,7 +1591,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
@@ -1477,7 +1610,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
@@ -1526,6 +1668,20 @@ dependencies = [
"serde",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustacuda"
version = "0.1.3"
@@ -1576,6 +1732,39 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.23.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.21"
@@ -1682,6 +1871,12 @@ dependencies = [
"keccak",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
@@ -1850,6 +2045,16 @@ dependencies = [
"syn 2.0.103",
]
[[package]]
name = "tokio-rustls"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
@@ -1859,7 +2064,23 @@ dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
"tungstenite 0.21.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite 0.23.0",
"webpki-roots 0.26.11",
]
[[package]]
@@ -1936,13 +2157,33 @@ dependencies = [
"http 1.3.1",
"httparse",
"log",
"rand",
"rand 0.8.5",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.8.5",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.18.0"
@@ -1979,6 +2220,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
@@ -2008,6 +2255,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"rand 0.9.2",
"wasm-bindgen",
]
[[package]]
name = "vek"
version = "0.15.10"
@@ -2058,7 +2317,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.21.0",
"tokio-util",
"tower-service",
"tracing",
@@ -2070,6 +2329,101 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad224d2776649cfb4f4471124f8176e54c1cca67a88108e30a0cd98b90e7ad3"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1364104bdcd3c03f22b16a3b1c9620891469f5e9f09bc38b2db121e593e732"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.103",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d7ab4ca3e367bb1ed84ddbd83cc6e41e115f8337ed047239578210214e36c76"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a518014843a19e2dbbd0ed5dfb6b99b23fb886b14e6192a00803a3e14c552b0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.103",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "255eb0aa4cc2eea3662a00c2bbd66e93911b7361d5e0fcd62385acfd7e15dcee"
dependencies = [
"unicode-ident",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.2",
]
[[package]]
name = "webpki-roots"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -2170,6 +2524,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.1"
@@ -2250,6 +2610,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerotrie"
version = "0.2.2"
+1
View File
@@ -7,6 +7,7 @@ members = [
"crates/metrics",
"crates/miner-cli",
"crates/miner-service",
"crates/miner-telemetry",
"crates/pow-core",
]
default-members = ["crates/miner-cli"]
+77
View File
@@ -44,6 +44,49 @@ struct Args {
/// Note: GPU engines are currently unimplemented and will return a clear error at runtime.
#[arg(long, env = "MINER_ENGINE", value_enum, default_value_t = EngineCli::CpuFast)]
engine: EngineCli,
/// Telemetry endpoints (repeat --telemetry-endpoint or comma-separated)
#[arg(long = "telemetry-endpoint", env = "MINER_TELEMETRY_ENDPOINTS", value_delimiter = ',', num_args = 0.., value_name = "URL")]
telemetry_endpoints: Option<Vec<String>>,
/// Enable or disable telemetry explicitly
#[arg(long = "telemetry-enabled", env = "MINER_TELEMETRY_ENABLED")]
telemetry_enabled: Option<bool>,
/// Telemetry verbosity level (0..=4 typical)
#[arg(long = "telemetry-verbosity", env = "MINER_TELEMETRY_VERBOSITY")]
telemetry_verbosity: Option<u8>,
/// Interval seconds for system.interval messages
#[arg(
long = "telemetry-interval-secs",
env = "MINER_TELEMETRY_INTERVAL_SECS"
)]
telemetry_interval_secs: Option<u64>,
/// Default association: chain name
#[arg(long = "telemetry-chain", env = "MINER_TELEMETRY_CHAIN")]
telemetry_chain: Option<String>,
/// Default association: genesis hash (hex)
#[arg(long = "telemetry-genesis", env = "MINER_TELEMETRY_GENESIS")]
telemetry_genesis: Option<String>,
/// Default association: node telemetry id
#[arg(long = "telemetry-node-id", env = "MINER_TELEMETRY_NODE_ID")]
telemetry_node_id: Option<String>,
/// Default association: node libp2p peer id
#[arg(long = "telemetry-node-peer-id", env = "MINER_TELEMETRY_NODE_PEER_ID")]
telemetry_node_peer_id: Option<String>,
/// Default association: node name
#[arg(long = "telemetry-node-name", env = "MINER_TELEMETRY_NODE_NAME")]
telemetry_node_name: Option<String>,
/// Default association: node version
#[arg(long = "telemetry-node-version", env = "MINER_TELEMETRY_NODE_VERSION")]
telemetry_node_version: Option<String>,
}
#[allow(clippy::enum_variant_names)]
@@ -87,6 +130,40 @@ async fn main() {
}
env_logger::init();
// Telemetry CLI passthrough to env for miner-service bootstrap
if let Some(eps) = args.telemetry_endpoints.as_ref() {
if !eps.is_empty() {
std::env::set_var("MINER_TELEMETRY_ENDPOINTS", eps.join(","));
}
}
if let Some(v) = args.telemetry_enabled {
std::env::set_var("MINER_TELEMETRY_ENABLED", if v { "1" } else { "0" });
}
if let Some(v) = args.telemetry_verbosity {
std::env::set_var("MINER_TELEMETRY_VERBOSITY", v.to_string());
}
if let Some(v) = args.telemetry_interval_secs {
std::env::set_var("MINER_TELEMETRY_INTERVAL_SECS", v.to_string());
}
if let Some(v) = args.telemetry_chain.as_ref() {
std::env::set_var("MINER_TELEMETRY_CHAIN", v);
}
if let Some(v) = args.telemetry_genesis.as_ref() {
std::env::set_var("MINER_TELEMETRY_GENESIS", v);
}
if let Some(v) = args.telemetry_node_id.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_ID", v);
}
if let Some(v) = args.telemetry_node_peer_id.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_PEER_ID", v);
}
if let Some(v) = args.telemetry_node_name.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_NAME", v);
}
if let Some(v) = args.telemetry_node_version.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_VERSION", v);
}
// Log effective configuration (concise; see ServiceConfig Display)
log::info!("Starting external miner service...");
+1
View File
@@ -46,3 +46,4 @@ engine-montgomery = { path = "../engine-montgomery", optional = true }
engine-gpu-cuda = { path = "../engine-gpu-cuda", optional = true, features = ["cuda"] }
engine-gpu-opencl = { path = "../engine-gpu-opencl", optional = true }
metrics = { path = "../metrics", optional = true }
miner-telemetry = { path = "../miner-telemetry" }
+120
View File
@@ -1254,6 +1254,126 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> {
// Start mining loop
service.start_mining_loop().await;
// Telemetry bootstrap from environment variables (optional)
let telemetry_handle_opt = {
let endpoints: Vec<String> = std::env::var("MINER_TELEMETRY_ENDPOINTS")
.ok()
.map(|s| {
s.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect()
})
.unwrap_or_default();
let enabled = std::env::var("MINER_TELEMETRY_ENABLED")
.ok()
.map(|v| v != "0" && !v.eq_ignore_ascii_case("false"))
.unwrap_or(!endpoints.is_empty());
let verbosity = std::env::var("MINER_TELEMETRY_VERBOSITY")
.ok()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0);
if enabled && !endpoints.is_empty() {
let interval_secs = std::env::var("MINER_TELEMETRY_INTERVAL_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok());
let chain = std::env::var("MINER_TELEMETRY_CHAIN").ok();
let genesis = std::env::var("MINER_TELEMETRY_GENESIS").ok();
// Optional linked node info
let link = miner_telemetry::TelemetryNodeLink {
node_telemetry_id: std::env::var("MINER_TELEMETRY_NODE_ID").ok(),
node_peer_id: std::env::var("MINER_TELEMETRY_NODE_PEER_ID").ok(),
node_name: std::env::var("MINER_TELEMETRY_NODE_NAME").ok(),
node_version: std::env::var("MINER_TELEMETRY_NODE_VERSION").ok(),
chain: chain.clone(),
genesis_hash: genesis.clone(),
};
let default_link = if link.node_telemetry_id.is_some()
|| link.node_peer_id.is_some()
|| link.node_name.is_some()
|| link.node_version.is_some()
|| link.chain.is_some()
|| link.genesis_hash.is_some()
{
Some(link)
} else {
None
};
let cfg = miner_telemetry::TelemetryConfig {
enabled,
endpoints,
verbosity,
name: Some("quantus-miner".to_string()),
implementation: Some("quantus-miner".to_string()),
version: Some(
option_env!("MINER_VERSION")
.unwrap_or(env!("CARGO_PKG_VERSION"))
.to_string(),
),
chain,
genesis_hash: genesis,
interval_secs,
default_link,
};
Some(miner_telemetry::start(cfg))
} else {
None
}
};
if let Some(telemetry) = telemetry_handle_opt {
log::info!("Telemetry enabled; session_id={}", telemetry.session_id());
telemetry.emit_system_connected(None).await;
let telemetry_handle = telemetry.clone();
let svc = service.clone();
let engine_name_str = engine.name().to_string();
let interval_secs = std::env::var("MINER_TELEMETRY_INTERVAL_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(15);
let start_instant = std::time::Instant::now();
tokio::spawn(async move {
loop {
let (active_jobs, total_rate) = {
let jobs = svc.jobs.lock().await;
let mut running = 0i64;
let mut rate = 0.0;
for (_id, job) in jobs.iter() {
if job.status == JobStatus::Running {
running += 1;
rate += job.last_hash_rate;
}
}
(running, rate)
};
let uptime_ms = start_instant.elapsed().as_millis() as u64;
let interval = miner_telemetry::SystemInterval {
uptime_ms,
engine: Some(engine_name_str.clone()),
workers: Some(svc.workers as u32),
hash_rate: Some(total_rate),
active_jobs: Some(active_jobs),
linked_node_hint: None,
};
telemetry_handle.emit_system_interval(&interval, None).await;
tokio::time::sleep(tokio::time::Duration::from_secs(interval_secs)).await;
}
});
} else {
log::info!("Telemetry disabled (no endpoints configured)");
}
// Optionally start metrics exporter if enabled via CLI and feature flag.
if let Some(port) = config.metrics_port {
#[cfg(feature = "metrics")]
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "miner-telemetry"
version = "0.1.0"
edition = "2021"
publish = false
description = "Telemetry client for Quantus External Miner (Polkadot SDK-compatible envelopes over WebSocket)"
[lib]
name = "miner_telemetry"
path = "src/lib.rs"
[features]
default = []
[dependencies]
# Dependencies (alphabetical)
anyhow = { workspace = true }
futures = "0.3"
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.23", features = ["rustls-tls-webpki-roots"] }
url = "2"
uuid = { version = "1", features = ["fast-rng", "v4"] }
+471
View File
@@ -0,0 +1,471 @@
#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
//! Minimal telemetry client for the Quantus External Miner.
//!
//! Capabilities:
//! - Connects to one or more telemetry endpoints (wss://...) asynchronously.
//! - Sends upstream-compatible envelopes with a stable per-process session id.
//! - Provides helpers to emit `system.connected` and `system.interval` events.
//! - Non-blocking: messages are buffered via bounded channels; on overflow, they are dropped.
//! - Auto-reconnects on send error; subsequent messages trigger reconnect attempts.
//!
//! Notes:
//! - This is intentionally small and self-contained. It does not embed Substrate's sc-telemetry.
//! - Endpoints must currently be WebSocket URLs (e.g., wss://telemetry.example/submit/).
//! - If no endpoints are configured or telemetry is disabled, the handle becomes a no-op.
use anyhow::Result;
use futures::{SinkExt, StreamExt};
use log::{debug, info, warn};
use serde::Serialize;
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use uuid::Uuid;
/// Bounded channel capacity per endpoint writer task.
const TELEMETRY_CHANNEL_CAPACITY: usize = 1024;
/// Telemetry configuration.
#[derive(Clone, Debug, Default)]
pub struct TelemetryConfig {
/// Enable/disable telemetry entirely.
pub enabled: bool,
/// WebSocket endpoints to send telemetry to (e.g., "wss://telemetry.example/submit/").
pub endpoints: Vec<String>,
/// Default verbosity (u8), applied if not overridden per message.
pub verbosity: u8,
/// Optional static identity to put in system.connected.
pub name: Option<String>,
pub implementation: Option<String>,
pub version: Option<String>,
pub chain: Option<String>,
pub genesis_hash: Option<String>,
/// Interval (seconds) recommendation for system.interval from the service (not enforced here).
pub interval_secs: Option<u64>,
/// Optional default association with a node (used when job-specific context doesn't provide one).
pub default_link: Option<TelemetryNodeLink>,
}
/// Optional association with a node that the miner is working for.
#[derive(Clone, Debug, Default, Serialize)]
pub struct TelemetryNodeLink {
#[serde(skip_serializing_if = "Option::is_none")]
pub node_telemetry_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_peer_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub genesis_hash: Option<String>,
}
/// Snapshot used to populate `system.interval` payloads.
#[derive(Clone, Debug, Default)]
pub struct SystemInterval {
pub uptime_ms: u64,
pub engine: Option<String>,
pub workers: Option<u32>,
pub hash_rate: Option<f64>,
pub active_jobs: Option<i64>,
/// If you want to hint which node is primarily linked when multiple jobs exist.
pub linked_node_hint: Option<String>,
}
/// Cloneable handle to send telemetry events.
#[derive(Clone)]
pub struct TelemetryHandle {
session_id: String,
verbosity: u8,
// One sender per endpoint task.
senders: Arc<Vec<mpsc::Sender<EnvelopeMsg>>>,
// Keep config for convenience payload builders.
config: Arc<TelemetryConfig>,
}
impl TelemetryHandle {
/// Returns true if this handle will actually send to any endpoint.
pub fn is_enabled(&self) -> bool {
!self.senders.is_empty()
}
/// Returns the stable session id used in the envelope `id`.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Emit a fully custom payload (must include `"msg"`).
pub async fn emit_payload(&self, payload: Value, verbosity: Option<u8>) {
if self.senders.is_empty() {
return;
}
let msg = EnvelopeMsg { payload, verbosity };
for tx in self.senders.iter() {
// Drop on full; do not block mining paths. This is intentionally lossy under backpressure.
if tx.try_send(msg.clone()).is_err() {
// Best-effort: if try_send failed due to full buffer, attempt bounded send with small timeout.
// Avoid blocking indefinitely.
let tx = tx.clone();
let msg = msg.clone();
tokio::spawn(async move {
let _ = tx.send(msg).await;
});
}
}
}
/// Emit a `system.connected` message using configuration defaults and an optional link.
pub async fn emit_system_connected(&self, link: Option<&TelemetryNodeLink>) {
let payload = build_system_connected_payload(&self.config, link);
self.emit_payload(payload, Some(self.verbosity)).await;
}
/// Emit a `system.interval` message with lightweight health stats and an optional link hint.
pub async fn emit_system_interval(&self, interval: &SystemInterval, link_hint: Option<&str>) {
let mut payload = json!({
"msg": "system.interval",
"uptime_ms": interval.uptime_ms,
});
if let Some(engine) = &interval.engine {
payload
.as_object_mut()
.unwrap()
.insert("engine".to_string(), json!(engine));
}
if let Some(workers) = interval.workers {
payload
.as_object_mut()
.unwrap()
.insert("workers".to_string(), json!(workers));
}
if let Some(hash_rate) = interval.hash_rate {
payload
.as_object_mut()
.unwrap()
.insert("hash_rate".to_string(), json!(hash_rate));
}
if let Some(active_jobs) = interval.active_jobs {
payload
.as_object_mut()
.unwrap()
.insert("active_jobs".to_string(), json!(active_jobs));
}
if let Some(hint) = link_hint {
payload
.as_object_mut()
.unwrap()
.insert("linked_node_hint".to_string(), json!(hint));
}
self.emit_payload(payload, Some(self.verbosity)).await;
}
}
/// Build a `system.connected` payload with upstream-compatible keys and optional `linked_node`.
fn build_system_connected_payload(
config: &TelemetryConfig,
link: Option<&TelemetryNodeLink>,
) -> Value {
let name = config
.name
.as_deref()
.unwrap_or("quantus-miner")
.to_string();
let implementation = config
.implementation
.as_deref()
.unwrap_or("quantus-miner")
.to_string();
// Prefer build-time env var MINER_VERSION or fallback to crate version if the caller sets it.
let version = config.version.clone().unwrap_or_else(|| {
option_env!("MINER_VERSION")
.map(|s| s.to_string())
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string())
});
let mut payload = json!({
"msg": "system.connected",
"name": name,
"implementation": implementation,
"version": version,
"authority": false,
"platform": {
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH
}
});
if let Some(chain) = &config.chain {
payload
.as_object_mut()
.unwrap()
.insert("chain".to_string(), json!(chain));
}
if let Some(gh) = &config.genesis_hash {
payload
.as_object_mut()
.unwrap()
.insert("genesis_hash".to_string(), json!(gh));
}
let linked = link.or(config.default_link.as_ref());
if let Some(linked_node) = linked {
let v = serde_json::to_value(linked_node).unwrap_or(json!({}));
if let Some(obj) = v.as_object() {
// Only include non-empty association
if !obj.is_empty() {
payload
.as_object_mut()
.unwrap()
.insert("linked_node".to_string(), Value::Object(obj.clone()));
}
}
}
payload
}
/// Telemetry envelope to send to the transport.
#[derive(Clone, Debug)]
struct EnvelopeMsg {
payload: Value,
verbosity: Option<u8>,
}
/// Start the telemetry subsystem with the given configuration.
/// Returns a `TelemetryHandle` for emitting events. If `config.enabled` is false
/// or no endpoints are specified, the handle is a no-op.
pub fn start(config: TelemetryConfig) -> TelemetryHandle {
let enabled = config.enabled && !config.endpoints.is_empty();
let session_id = Uuid::new_v4().to_string();
let verbosity = config.verbosity;
let cfg = Arc::new(config);
let mut senders: Vec<mpsc::Sender<EnvelopeMsg>> = Vec::new();
if enabled {
for ep in &cfg.endpoints {
let (tx, rx) = mpsc::channel::<EnvelopeMsg>(TELEMETRY_CHANNEL_CAPACITY);
senders.push(tx);
spawn_endpoint_writer_task(ep.clone(), rx, session_id.clone(), verbosity);
}
info!(
"miner-telemetry: started with {} endpoint(s), session_id={}",
cfg.endpoints.len(),
session_id
);
} else {
info!("miner-telemetry: disabled (no endpoints or disabled in config)");
}
TelemetryHandle {
session_id,
verbosity,
senders: Arc::new(senders),
config: cfg,
}
}
/// Spawn a writer task for a single endpoint. It owns a receiver and attempts to keep
/// a write connection alive, reconnecting on failure. Messages received while disconnected
/// are dropped (best-effort model).
fn spawn_endpoint_writer_task(
endpoint: String,
mut rx: mpsc::Receiver<EnvelopeMsg>,
session_id: String,
default_verbosity: u8,
) {
tokio::spawn(async move {
// Writer half of the websocket.
type WsWrite =
futures::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
let mut writer: Option<WsWrite> = None;
let mut backoff_secs: u64 = 1;
loop {
// Receive next message to send. If all senders are gone, exit task gracefully.
let Some(msg) = rx.recv().await else {
debug!(
"miner-telemetry: endpoint task for {} shutting down (channel closed)",
endpoint
);
break;
};
// Ensure connectivity prior to sending.
if writer.is_none() {
match connect_ws(&endpoint).await {
Ok(w) => {
writer = Some(w);
backoff_secs = 1; // reset backoff after success
debug!("miner-telemetry: connected to {}", endpoint);
}
Err(e) => {
warn!(
"miner-telemetry: connect failed to {}: {e}, backing off {}s",
endpoint, backoff_secs
);
sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs.saturating_mul(2)).min(30);
// Drop this message and continue; newer messages will trigger subsequent attempts.
continue;
}
}
}
// Build envelope with timestamp and (optional) verbosity.
let ts = now_ms();
let verbosity = msg.verbosity.unwrap_or(default_verbosity);
let envelope = json!({
"id": session_id,
"ts": ts,
"verbosity": verbosity,
"payload": msg.payload
});
let line = match serde_json::to_string(&envelope) {
Ok(s) => s,
Err(e) => {
warn!("miner-telemetry: failed to serialize envelope: {e}");
continue;
}
};
// Send over WebSocket
let mut send_ok = false;
if let Some(w) = writer.as_mut() {
match w.send(Message::Text(line)).await {
Ok(()) => {
send_ok = true;
}
Err(e) => {
warn!(
"miner-telemetry: send error to {}: {e}. Will drop connection and reconnect.",
endpoint
);
}
}
}
if !send_ok {
writer = None; // drop and reconnect on next message
// Small backoff to avoid hot-loop if immediate failures keep happening.
sleep(Duration::from_millis(250)).await;
}
}
});
}
/// Establish a websocket connection and return the writer half.
/// A reader task is also spawned to continuously drain incoming frames (pings, acks, etc.).
async fn connect_ws(
endpoint: &str,
) -> Result<futures::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>> {
let url = endpoint.to_string();
let (ws, _resp) = tokio_tungstenite::connect_async(url).await?;
let (write, mut read) = ws.split();
// Spawn a task to drain incoming frames to keep the connection healthy.
tokio::spawn(async move {
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Ping(data)) => {
debug!("miner-telemetry: received ping ({} bytes)", data.len());
}
Ok(Message::Pong(_)) => {
// no-op
}
Ok(Message::Text(_)) | Ok(Message::Binary(_)) => {
// Most telemetry servers don't push data; ignore.
}
Ok(Message::Frame(_)) => {
// Ignore low-level frames; keep connection alive
}
Ok(Message::Close(frame)) => {
debug!("miner-telemetry: server closed connection: {:?}", frame);
break;
}
Err(e) => {
debug!("miner-telemetry: read error: {e}");
break;
}
}
}
debug!("miner-telemetry: reader task ended");
});
Ok(write)
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_millis() as u64
}
// -------------------------------------------------------------------------------------
// Tests (basic serialization and handle behavior)
// -------------------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_connected_payload() {
let cfg = TelemetryConfig {
enabled: true,
endpoints: vec![],
verbosity: 0,
name: Some("qm".into()),
implementation: Some("quantus-miner".into()),
version: Some("0.0.1".into()),
chain: Some("Resonance".into()),
genesis_hash: Some("0xabc".into()),
interval_secs: Some(15),
default_link: Some(TelemetryNodeLink {
node_telemetry_id: Some("node-uuid".into()),
node_peer_id: Some("12D3KooW..".into()),
node_name: Some("node01".into()),
node_version: Some("1.2.3".into()),
chain: Some("Resonance".into()),
genesis_hash: Some("0xabc".into()),
}),
};
let payload = build_system_connected_payload(&cfg, None);
assert_eq!(
payload.get("msg").and_then(|v| v.as_str()),
Some("system.connected")
);
assert_eq!(payload.get("name").and_then(|v| v.as_str()), Some("qm"));
assert_eq!(
payload.get("chain").and_then(|v| v.as_str()),
Some("Resonance")
);
assert!(payload.get("linked_node").is_some());
}
#[tokio::test]
async fn test_handle_noop_when_disabled() {
let cfg = TelemetryConfig::default();
let handle = start(cfg);
assert!(!handle.is_enabled());
handle
.emit_system_interval(&SystemInterval::default(), None)
.await;
handle.emit_system_connected(None).await;
}
}