SC Day 30
Rust 文件 IO + toml 配置 + tracing 日志 + CLI v3 完整版
### 一、std::fs — 文件系统操作
2026-04-30
第二阶段:框架实战 (Day 25-30)Rust文件IOtomltracing日志CLI多链
日期: 2026-04-30 方向: Rust 阶段: 第二阶段:框架实战 (Day 25-30) 标签: #Rust #文件IO #toml #tracing #日志 #CLI #多链
今日目标
- 掌握 Rust 文件 IO(
std::fs读写文件) - 使用 toml + serde 管理配置文件
- 学会 tracing 日志框架(替代 println! 调试)
- 完成 CLI v3 完整版:配置文件 + 日志 + 多链支持
- 总结 Production-ready Rust 代码的最佳实践
核心概念
一、std::fs — 文件系统操作
Rust 标准库提供同步的文件 IO API,对于配置文件读写足够使用。
1.1 基础读写
use std::fs;
use std::io::{self, Write, BufRead};
use std::path::Path;
fn file_io_basics() -> io::Result<()> {
// ====== 读取文件 ======
// 一次性读取整个文件为字符串
let content = fs::read_to_string("config.toml")?;
println!("File content:\n{}", content);
// 一次性读取为字节
let bytes = fs::read("binary_file.dat")?;
println!("File size: {} bytes", bytes.len());
// 按行读取 (大文件友好)
let file = fs::File::open("large_file.txt")?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.contains("error") {
println!("Found: {}", line);
}
}
// ====== 写入文件 ======
// 一次性写入 (覆盖)
fs::write("output.txt", "Hello, Blockchain!\n")?;
// 追加写入
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open("log.txt")?;
writeln!(file, "[{}] New entry", "2026-04-30")?;
// ====== 文件操作 ======
// 检查文件是否存在
if Path::new("config.toml").exists() {
println!("Config file found");
}
// 创建目录 (包含父目录)
fs::create_dir_all("data/cache/rpc")?;
// 复制文件
fs::copy("config.toml", "config.toml.bak")?;
// 删除文件
fs::remove_file("temp.txt")?;
// 列出目录内容
for entry in fs::read_dir(".")? {
let entry = entry?;
let file_type = if entry.file_type()?.is_dir() { "DIR " } else { "FILE" };
println!("{} {}", file_type, entry.path().display());
}
Ok(())
}
1.2 路径处理
use std::path::{Path, PathBuf};
fn path_examples() {
// 构建路径
let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
let app_config = config_dir.join("chain-cli").join("config.toml");
println!("Config path: {}", app_config.display());
// 路径操作
let path = Path::new("/home/user/projects/chain-cli/config.toml");
println!("File name: {:?}", path.file_name()); // "config.toml"
println!("Extension: {:?}", path.extension()); // "toml"
println!("Parent: {:?}", path.parent()); // "/home/user/projects/chain-cli"
println!("Exists: {}", path.exists());
// 展开 ~ (home directory)
// 需要 dirs 或 shellexpand crate
let home = dirs::home_dir().unwrap();
let expanded = home.join(".chain-cli").join("config.toml");
println!("Expanded: {}", expanded.display());
}
二、TOML 配置文件
TOML (Tom's Obvious, Minimal Language) 是 Rust 生态首选的配置格式(Cargo.toml 就是 TOML)。
2.1 TOML 语法快速入门
# 这是注释
# 基础类型
title = "Chain CLI"
version = "0.3.0"
debug = false
timeout = 10
# 数组
supported_chains = ["ethereum", "arbitrum", "optimism", "base", "polygon"]
# 表 (类似 JSON object)
[default]
chain = "ethereum"
format = "table"
# 嵌套表
[api_keys]
etherscan = "YOUR_KEY_HERE"
alchemy = "YOUR_KEY_HERE"
# 表数组 (数组中的每个元素是一个表)
[[chains]]
name = "ethereum"
chain_id = 1
rpc_url = "https://eth.llamarpc.com"
explorer = "https://etherscan.io"
symbol = "ETH"
[[chains]]
name = "arbitrum"
chain_id = 42161
rpc_url = "https://arb1.arbitrum.io/rpc"
explorer = "https://arbiscan.io"
symbol = "ETH"
[[chains]]
name = "polygon"
chain_id = 137
rpc_url = "https://polygon-rpc.com"
explorer = "https://polygonscan.com"
symbol = "MATIC"
2.2 使用 serde 解析 TOML
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
title: String,
version: String,
debug: bool,
timeout: u64,
supported_chains: Vec<String>,
default: DefaultConfig,
api_keys: ApiKeys,
chains: Vec<ChainConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DefaultConfig {
chain: String,
format: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiKeys {
etherscan: String,
alchemy: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChainConfig {
name: String,
chain_id: u64,
rpc_url: String,
explorer: String,
symbol: String,
}
fn load_config() -> Result<AppConfig, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("config.toml")?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
fn save_config(config: &AppConfig) -> Result<(), Box<dyn std::error::Error>> {
let content = toml::to_string_pretty(config)?;
std::fs::write("config.toml", content)?;
Ok(())
}
// 带默认值的配置
impl Default for AppConfig {
fn default() -> Self {
Self {
title: "Chain CLI".to_string(),
version: "0.3.0".to_string(),
debug: false,
timeout: 10,
supported_chains: vec![
"ethereum".to_string(),
"arbitrum".to_string(),
"polygon".to_string(),
],
default: DefaultConfig {
chain: "ethereum".to_string(),
format: "table".to_string(),
},
api_keys: ApiKeys {
etherscan: String::new(),
alchemy: String::new(),
},
chains: vec![
ChainConfig {
name: "ethereum".to_string(),
chain_id: 1,
rpc_url: "https://eth.llamarpc.com".to_string(),
explorer: "https://etherscan.io".to_string(),
symbol: "ETH".to_string(),
},
],
}
}
}
三、tracing — 结构化日志
tracing 是 Rust 生态中最推荐的日志框架,比 log + env_logger 更强大。它提供结构化日志、span(跨度)和异步支持。
3.1 为什么用 tracing 而不是 println!
| 维度 | println! | tracing |
|---|---|---|
| 级别 | 无 | ERROR/WARN/INFO/DEBUG/TRACE |
| 过滤 | 不可过滤 | 按级别/模块过滤 |
| 结构化 | 文本 | 结构化键值对 |
| 性能 | 不可关闭 | 编译期可零成本消除 |
| 异步 | 不感知 | 异步 span 跟踪 |
| 生产级 | 不适合 | 可输出到文件/JSON/OpenTelemetry |
3.2 基础使用
use tracing::{info, warn, error, debug, trace, instrument};
use tracing_subscriber::{self, fmt, EnvFilter};
fn setup_logging(verbose: bool) {
let filter = if verbose {
"chain_cli=debug,reqwest=info"
} else {
"chain_cli=info,reqwest=warn"
};
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(filter))
)
.with_target(true) // 显示模块路径
.with_thread_ids(false) // 不显示线程 ID
.with_file(true) // 显示文件名
.with_line_number(true) // 显示行号
.init();
}
// 基础日志
fn logging_examples() {
// 不同级别
error!("Critical failure: database connection lost");
warn!("API rate limit approaching: {}/100 requests", 90);
info!("Server started on port {}", 8080);
debug!("Processing request: {:?}", "GET /balance");
trace!("Raw response bytes: {:?}", vec![0u8; 10]);
// 结构化字段
info!(
chain = "ethereum",
address = "0x1234...5678",
balance_eth = 42.5,
"Balance query completed"
);
// 输出类似:
// 2026-04-30T10:30:00Z INFO chain_cli::client: Balance query completed
// chain="ethereum" address="0x1234...5678" balance_eth=42.5
}
// #[instrument] 自动创建 span
#[instrument(skip(client), fields(chain = %chain_name))]
async fn fetch_balance(
client: &reqwest::Client,
rpc_url: &str,
address: &str,
chain_name: &str,
) -> Result<f64, Box<dyn std::error::Error>> {
debug!("Sending RPC request to {}", rpc_url);
// ... RPC 调用逻辑 ...
info!(balance = 42.5, latency_ms = 150, "Balance fetched");
Ok(42.5)
}
3.3 多层输出
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn setup_advanced_logging() {
// 控制台输出层
let console_layer = fmt::layer()
.with_target(true)
.with_ansi(true) // 终端颜色
.compact(); // 紧凑格式
// 文件输出层 (JSON 格式, 方便解析)
let file = std::fs::File::create("chain-cli.log").unwrap();
let file_layer = fmt::layer()
.json() // JSON 格式
.with_writer(file)
.with_ansi(false); // 文件不需要颜色
tracing_subscriber::registry()
.with(EnvFilter::new("chain_cli=debug"))
.with(console_layer)
.with(file_layer)
.init();
}
代码实战
CLI v3 完整版
Cargo.toml
[package]
name = "chain-cli"
version = "0.3.0"
edition = "2021"
description = "Multi-chain blockchain query CLI"
[dependencies]
clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
dirs = "5"
config.toml
# Chain CLI v3 Configuration
[app]
version = "0.3.0"
timeout_secs = 10
default_chain = "ethereum"
output_format = "table"
[api_keys]
etherscan = ""
[logging]
level = "info"
file = "chain-cli.log"
json_output = false
[[chains]]
name = "Ethereum"
id = "ethereum"
chain_id = 1
rpc_url = "https://eth.llamarpc.com"
explorer_url = "https://etherscan.io"
native_symbol = "ETH"
enabled = true
[[chains]]
name = "Arbitrum One"
id = "arbitrum"
chain_id = 42161
rpc_url = "https://arb1.arbitrum.io/rpc"
explorer_url = "https://arbiscan.io"
native_symbol = "ETH"
enabled = true
[[chains]]
name = "Optimism"
id = "optimism"
chain_id = 10
rpc_url = "https://mainnet.optimism.io"
explorer_url = "https://optimistic.etherscan.io"
native_symbol = "ETH"
enabled = true
[[chains]]
name = "Base"
id = "base"
chain_id = 8453
rpc_url = "https://mainnet.base.org"
explorer_url = "https://basescan.org"
native_symbol = "ETH"
enabled = true
[[chains]]
name = "Polygon"
id = "polygon"
chain_id = 137
rpc_url = "https://polygon-rpc.com"
explorer_url = "https://polygonscan.com"
native_symbol = "MATIC"
enabled = true
src/config.rs (v3)
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::{info, warn};
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub app: AppSection,
pub api_keys: ApiKeysSection,
pub logging: LoggingSection,
pub chains: Vec<ChainEntry>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppSection {
pub version: String,
pub timeout_secs: u64,
pub default_chain: String,
pub output_format: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiKeysSection {
pub etherscan: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoggingSection {
pub level: String,
pub file: String,
pub json_output: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainEntry {
pub name: String,
pub id: String,
pub chain_id: u64,
pub rpc_url: String,
pub explorer_url: String,
pub native_symbol: String,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
}
impl Config {
/// 从文件加载配置
pub fn load(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
if !path.exists() {
warn!("Config file not found at {}, creating default", path.display());
let config = Config::default();
config.save(path)?;
return Ok(config);
}
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
info!("Config loaded from {}", path.display());
Ok(config)
}
/// 保存配置到文件
pub fn save(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
info!("Config saved to {}", path.display());
Ok(())
}
/// 获取默认配置文件路径
pub fn default_path() -> PathBuf {
// 优先从当前目录读取
let local = PathBuf::from("config.toml");
if local.exists() {
return local;
}
// 否则用用户配置目录
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("chain-cli")
.join("config.toml")
}
/// 获取启用的链配置
pub fn enabled_chains(&self) -> Vec<&ChainEntry> {
self.chains.iter().filter(|c| c.enabled).collect()
}
/// 根据 id 查找链配置
pub fn find_chain(&self, id: &str) -> Option<&ChainEntry> {
self.chains.iter().find(|c| c.id == id && c.enabled)
}
/// 合并环境变量 (覆盖配置文件)
pub fn merge_env(&mut self) {
if let Ok(key) = std::env::var("ETHERSCAN_API_KEY") {
self.api_keys.etherscan = key;
}
if let Ok(level) = std::env::var("LOG_LEVEL") {
self.logging.level = level;
}
if let Ok(timeout) = std::env::var("TIMEOUT_SECS") {
if let Ok(t) = timeout.parse() {
self.app.timeout_secs = t;
}
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
app: AppSection {
version: "0.3.0".to_string(),
timeout_secs: 10,
default_chain: "ethereum".to_string(),
output_format: "table".to_string(),
},
api_keys: ApiKeysSection {
etherscan: String::new(),
},
logging: LoggingSection {
level: "info".to_string(),
file: "chain-cli.log".to_string(),
json_output: false,
},
chains: vec![
ChainEntry {
name: "Ethereum".to_string(),
id: "ethereum".to_string(),
chain_id: 1,
rpc_url: "https://eth.llamarpc.com".to_string(),
explorer_url: "https://etherscan.io".to_string(),
native_symbol: "ETH".to_string(),
enabled: true,
},
ChainEntry {
name: "Arbitrum One".to_string(),
id: "arbitrum".to_string(),
chain_id: 42161,
rpc_url: "https://arb1.arbitrum.io/rpc".to_string(),
explorer_url: "https://arbiscan.io".to_string(),
native_symbol: "ETH".to_string(),
enabled: true,
},
ChainEntry {
name: "Optimism".to_string(),
id: "optimism".to_string(),
chain_id: 10,
rpc_url: "https://mainnet.optimism.io".to_string(),
explorer_url: "https://optimistic.etherscan.io".to_string(),
native_symbol: "ETH".to_string(),
enabled: true,
},
ChainEntry {
name: "Base".to_string(),
id: "base".to_string(),
chain_id: 8453,
rpc_url: "https://mainnet.base.org".to_string(),
explorer_url: "https://basescan.org".to_string(),
native_symbol: "ETH".to_string(),
enabled: true,
},
ChainEntry {
name: "Polygon".to_string(),
id: "polygon".to_string(),
chain_id: 137,
rpc_url: "https://polygon-rpc.com".to_string(),
explorer_url: "https://polygonscan.com".to_string(),
native_symbol: "MATIC".to_string(),
enabled: true,
},
],
}
}
}
src/logging.rs
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
/// 初始化日志系统
pub fn init(level: &str, verbose: bool) {
let effective_level = if verbose { "debug" } else { level };
let filter = format!(
"chain_cli={},reqwest=warn,hyper=warn",
effective_level
);
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&filter))
)
.with_target(false)
.with_timer(fmt::time::ChronoLocal::new("%H:%M:%S".to_string()))
.init();
tracing::debug!("Logging initialized at level: {}", effective_level);
}
src/client.rs (v3)
use crate::config::ChainEntry;
use reqwest::Client;
use serde::Serialize;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, instrument, warn};
#[derive(Serialize)]
struct RpcRequest {
jsonrpc: String,
method: String,
params: Vec<serde_json::Value>,
id: u64,
}
#[derive(serde::Deserialize)]
struct RpcResponse {
result: Option<serde_json::Value>,
error: Option<RpcError>,
}
#[derive(serde::Deserialize, Debug)]
struct RpcError {
code: i64,
message: String,
}
/// 查询结果
#[derive(Debug)]
pub struct BalanceResult {
pub chain_name: String,
pub chain_id: String,
pub native_symbol: String,
pub address: String,
pub balance_wei: u128,
pub balance_display: f64,
pub latency_ms: u128,
pub success: bool,
pub error: Option<String>,
}
pub struct BlockchainClient {
http: Client,
}
impl BlockchainClient {
pub fn new(timeout_secs: u64) -> Self {
let http = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self { http }
}
/// 查询单条链的余额
#[instrument(skip(self), fields(chain = %chain.name))]
pub async fn get_balance(
&self,
chain: &ChainEntry,
address: &str,
) -> BalanceResult {
let start = Instant::now();
debug!("Querying balance via {}", chain.rpc_url);
let request = RpcRequest {
jsonrpc: "2.0".to_string(),
method: "eth_getBalance".to_string(),
params: vec![
serde_json::Value::String(address.to_string()),
serde_json::Value::String("latest".to_string()),
],
id: 1,
};
let result = self.http
.post(&chain.rpc_url)
.json(&request)
.send()
.await;
let latency = start.elapsed().as_millis();
match result {
Ok(response) => {
match response.json::<RpcResponse>().await {
Ok(rpc_resp) => {
if let Some(err) = rpc_resp.error {
error!(code = err.code, "RPC error: {}", err.message);
return BalanceResult {
chain_name: chain.name.clone(),
chain_id: chain.id.clone(),
native_symbol: chain.native_symbol.clone(),
address: address.to_string(),
balance_wei: 0,
balance_display: 0.0,
latency_ms: latency,
success: false,
error: Some(err.message),
};
}
if let Some(hex) = rpc_resp.result.and_then(|v| v.as_str().map(String::from)) {
let balance_wei = u128::from_str_radix(
hex.trim_start_matches("0x"), 16
).unwrap_or(0);
let balance_display = balance_wei as f64 / 1e18;
info!(
balance = balance_display,
latency_ms = latency,
"Balance fetched"
);
BalanceResult {
chain_name: chain.name.clone(),
chain_id: chain.id.clone(),
native_symbol: chain.native_symbol.clone(),
address: address.to_string(),
balance_wei,
balance_display,
latency_ms: latency,
success: true,
error: None,
}
} else {
warn!("Unexpected response format");
BalanceResult {
chain_name: chain.name.clone(),
chain_id: chain.id.clone(),
native_symbol: chain.native_symbol.clone(),
address: address.to_string(),
balance_wei: 0,
balance_display: 0.0,
latency_ms: latency,
success: false,
error: Some("Invalid response format".to_string()),
}
}
}
Err(e) => {
error!("Failed to parse response: {}", e);
BalanceResult {
chain_name: chain.name.clone(),
chain_id: chain.id.clone(),
native_symbol: chain.native_symbol.clone(),
address: address.to_string(),
balance_wei: 0,
balance_display: 0.0,
latency_ms: latency,
success: false,
error: Some(format!("Parse error: {}", e)),
}
}
}
}
Err(e) => {
error!("Request failed: {}", e);
BalanceResult {
chain_name: chain.name.clone(),
chain_id: chain.id.clone(),
native_symbol: chain.native_symbol.clone(),
address: address.to_string(),
balance_wei: 0,
balance_display: 0.0,
latency_ms: latency,
success: false,
error: Some(format!("Request error: {}", e)),
}
}
}
}
/// 并发查询多条链的余额
#[instrument(skip(self, chains))]
pub async fn get_multichain_balance(
&self,
chains: &[ChainEntry],
address: &str,
) -> Vec<BalanceResult> {
info!(chain_count = chains.len(), "Starting multi-chain query");
let start = Instant::now();
let mut handles = Vec::new();
for chain in chains {
let chain = chain.clone();
let addr = address.to_string();
let client = BlockchainClient::new(10);
handles.push(tokio::spawn(async move {
client.get_balance(&chain, &addr).await
}));
}
let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => error!("Task failed: {}", e),
}
}
let total_time = start.elapsed().as_millis();
info!(
total_ms = total_time,
successful = results.iter().filter(|r| r.success).count(),
failed = results.iter().filter(|r| !r.success).count(),
"Multi-chain query completed"
);
results
}
}
src/main.rs (v3)
use clap::{Parser, Subcommand, Args};
use tracing::info;
mod config;
mod logging;
mod client;
use config::Config;
use client::BlockchainClient;
#[derive(Parser)]
#[command(name = "chain-cli", version = "0.3.0")]
#[command(about = "Multi-chain blockchain query CLI with config & logging")]
struct Cli {
/// Config file path
#[arg(long, short = 'C')]
config: Option<String>,
/// Verbose output (debug level)
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Query balance on one or all chains
Balance(BalanceArgs),
/// Show current configuration
Config,
/// Initialize default config file
Init,
}
#[derive(Args)]
struct BalanceArgs {
/// Ethereum address (0x...)
#[arg(short, long)]
address: String,
/// Chain ID (ethereum, arbitrum, polygon, etc.)
#[arg(short, long)]
chain: Option<String>,
/// Query all enabled chains
#[arg(long)]
all: bool,
}
fn format_balance(value: f64, symbol: &str) -> String {
if value >= 1000.0 {
format!("{:.2} {}", value, symbol)
} else if value >= 1.0 {
format!("{:.4} {}", value, symbol)
} else if value >= 0.001 {
format!("{:.6} {}", value, symbol)
} else if value > 0.0 {
format!("{:.10} {}", value, symbol)
} else {
format!("0 {}", symbol)
}
}
fn shorten(addr: &str) -> String {
if addr.len() > 10 {
format!("{}...{}", &addr[..6], &addr[addr.len() - 4..])
} else {
addr.to_string()
}
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
// 加载配置
let config_path = cli.config
.map(std::path::PathBuf::from)
.unwrap_or_else(Config::default_path);
let mut config = Config::load(&config_path).unwrap_or_else(|e| {
eprintln!("Warning: Failed to load config: {}. Using defaults.", e);
Config::default()
});
config.merge_env();
// 初始化日志
logging::init(&config.logging.level, cli.verbose);
info!(config_path = %config_path.display(), "CLI started");
match cli.command {
Commands::Balance(args) => cmd_balance(&config, args).await,
Commands::Config => cmd_config(&config),
Commands::Init => cmd_init(&config, &config_path),
}
}
async fn cmd_balance(config: &Config, args: BalanceArgs) {
let client = BlockchainClient::new(config.app.timeout_secs);
if args.all {
// 查询所有启用的链
let chains: Vec<_> = config.enabled_chains().into_iter().cloned().collect();
println!("\nMulti-chain balance for {}\n", shorten(&args.address));
let results = client.get_multichain_balance(&chains, &args.address).await;
println!("{:<14} {:>18} {:>10}", "Chain", "Balance", "Latency");
println!("{}", "-".repeat(45));
for r in &results {
if r.success {
println!(
"{:<14} {:>18} {:>8}ms",
r.chain_name,
format_balance(r.balance_display, &r.native_symbol),
r.latency_ms
);
} else {
println!(
"{:<14} {:>18} {:>10}",
r.chain_name,
"Error",
r.error.as_deref().unwrap_or("Unknown")
);
}
}
println!("{}", "-".repeat(45));
let total: f64 = results.iter()
.filter(|r| r.success && r.native_symbol == "ETH")
.map(|r| r.balance_display)
.sum();
println!("{:<14} {:>18}", "Total ETH", format_balance(total, "ETH"));
} else {
// 查询单条链
let chain_id = args.chain.as_deref().unwrap_or(&config.app.default_chain);
match config.find_chain(chain_id) {
Some(chain) => {
let result = client.get_balance(chain, &args.address).await;
if result.success {
println!("\nChain: {}", result.chain_name);
println!("Address: {}", shorten(&result.address));
println!("Balance: {}", format_balance(result.balance_display, &result.native_symbol));
println!("Latency: {}ms", result.latency_ms);
} else {
eprintln!("Error: {}", result.error.unwrap_or_default());
}
}
None => {
eprintln!("Chain '{}' not found. Available chains:", chain_id);
for chain in config.enabled_chains() {
eprintln!(" - {}", chain.id);
}
}
}
}
}
fn cmd_config(config: &Config) {
println!("Current configuration:\n");
println!("{}", toml::to_string_pretty(config).unwrap());
}
fn cmd_init(config: &Config, path: &std::path::Path) {
match config.save(path) {
Ok(()) => println!("Config file created at: {}", path.display()),
Err(e) => eprintln!("Failed to create config: {}", e),
}
}
关键要点总结
Production-Ready Rust 模式清单
| 模式 | 实现方式 | 说明 |
|---|---|---|
| 配置分层 | 默认值 -> 配置文件 -> 环境变量 -> CLI 参数 | 优先级从低到高 |
| 结构化日志 | tracing + tracing-subscriber | 可过滤、可输出到文件 |
| 错误处理 | Result + 自定义 Error + ? | 不要 unwrap |
| 异步并发 | tokio::spawn + 超时 | 不阻塞主线程 |
| 模块化 | lib.rs + 子模块 | 可测试、可复用 |
| 配置文件 | TOML + serde | Rust 生态标准 |
配置优先级
CLI 参数 (最高优先级)
↑
环境变量 (ETHERSCAN_API_KEY, LOG_LEVEL)
↑
配置文件 (config.toml)
↑
默认值 (Default trait)
tracing 级别选择
| 级别 | 用途 | 示例 |
|---|---|---|
error! | 不可恢复的错误 | RPC 连接完全失败 |
warn! | 可恢复但需关注 | 单条链超时,其他正常 |
info! | 正常操作记录 | 查询成功,余额多少 |
debug! | 调试详情 | 请求 URL、响应内容 |
trace! | 极度详细 | 原始字节、每次 poll |
常见误区
- 配置文件路径硬编码: 应支持 CLI 参数 + 默认路径 + 环境变量
- 忘记 merge 环境变量: 环境变量应覆盖配置文件
- tracing 不设 filter: 所有日志都输出,包括依赖库的噪音
- 同步文件 IO 在 async 中: 读配置文件用
std::fs没问题(启动时一次),但不要在热路径用 - TOML 字段名不匹配: serde 默认匹配 Rust 字段名,TOML 中下划线不需要 rename
面试关联
| 面试题 | 本课关联 |
|---|---|
| "如何设计 CLI 工具的配置系统?" | 分层配置: 默认 -> 文件 -> 环境变量 -> CLI |
| "Rust 项目如何做日志?" | tracing 框架 + 结构化日志 |
| "如何处理多链查询?" | tokio::spawn 并发 + 配置驱动 |
| "如何组织 Rust 项目结构?" | lib.rs + mod + 关注点分离 |
| "TOML vs YAML vs JSON 配置文件?" | TOML: 可读性好,Rust 原生,适合配置 |