feat: add structured logging with tracing

main
Yoo1tic 2025-07-28 22:49:04 +08:00
parent f10c2a8afb
commit e5821f5b4c
10 changed files with 200 additions and 41 deletions

142
Cargo.lock generated
View File

@ -394,6 +394,8 @@ dependencies = [
"thiserror",
"tokio",
"toml 0.9.2",
"tracing",
"tracing-subscriber",
"url",
]
@ -746,6 +748,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
@ -780,6 +788,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.5"
@ -815,6 +832,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "object"
version = "0.36.7"
@ -836,6 +863,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pear"
version = "0.2.9"
@ -1024,8 +1057,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
@ -1036,9 +1078,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@ -1219,6 +1267,15 @@ dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -1326,6 +1383,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tinystr"
version = "0.8.1"
@ -1535,9 +1601,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
@ -1545,6 +1623,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -1598,6 +1706,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
@ -1728,6 +1842,28 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -23,3 +23,5 @@ figment = { version = "0.10", features = ["env", "toml"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.9"
thiserror = "2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }

View File

@ -6,3 +6,4 @@ timeout_sec = 20
concurrency = 30
proxy = "http://username:password@host:port"
enable_multiplexing = true
log_level = "info"

View File

@ -3,6 +3,7 @@ use crate::error::Result;
use std::{fs, io::Write};
use tokio::io::{AsyncWriteExt, BufWriter};
use toml::Value;
use tracing::info;
// Write valid key to output file
pub async fn write_keys_txt_file(
@ -31,6 +32,6 @@ pub fn write_keys_clewdr_format(file: &mut fs::File, key: &GeminiKey) -> Result<
pub fn write_keys_to_file(keys: &[String], filename: &str) -> Result<()> {
let content = keys.join("\n");
fs::write(filename, content)?;
println!("File '{}' created with {} keys", filename, keys.len());
info!("File '{}' created with {} keys", filename, keys.len());
Ok(())
}

View File

@ -77,6 +77,9 @@ pub struct KeyCheckerConfig {
// Whether to enable HTTP/2 multiplexing for requests.
#[serde(default)]
pub enable_multiplexing: bool,
#[serde(default)]
pub log_level: String,
}
impl Default for KeyCheckerConfig {
@ -110,33 +113,36 @@ impl KeyCheckerConfig {
/// Returns the complete Gemini API URL for generateContent endpoint
pub fn gemini_api_url(&self) -> Url {
self.api_host
.join("v1beta/models/gemini-2.0-flash-exp:generateContent")
.join("v1beta/models/gemini-2.5-flash-lite:generateContent")
.expect("Failed to join API URL")
}
}
impl Display for KeyCheckerConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "API Host: {}", self.api_host)?;
let proxy_status = match &self.proxy {
Some(proxy) => proxy.to_string(),
None => "Disabled".to_string(),
};
writeln!(f, "Proxy: {}", proxy_status)?;
let protocol_status = if self.enable_multiplexing {
"HTTP/2 (Multiplexing Enabled)"
} else {
"HTTP/1.1 (Multiplexing Disabled)"
};
writeln!(f, "Protocol: {}", protocol_status)?;
writeln!(f, "Timeout: {}s", self.timeout_sec)?;
writeln!(f, "Concurrency: {}", self.concurrency)?;
writeln!(f, "Input: {}", self.input_path.display())?;
writeln!(f, "Output: {}", self.output_path.display())?;
write!(f, "Backup: {}", self.backup_path.display())
write!(
f,
"Host={}, Proxy={}, Protocol={}, Timeout={}s, Concurrency={}, Input={}, Output={}, Backup={}",
self.api_host,
proxy_status,
protocol_status,
self.timeout_sec,
self.concurrency,
self.input_path.display(),
self.output_path.display(),
self.backup_path.display()
)
}
}
@ -150,6 +156,7 @@ static DEFAULT_CONFIG: LazyLock<KeyCheckerConfig> = LazyLock::new(|| KeyCheckerC
concurrency: 50,
proxy: None,
enable_multiplexing: true,
log_level: "info".to_string(),
});
// LazyLock for the test message body used in API key validation

View File

@ -5,9 +5,6 @@ pub enum ValidatorError {
#[error("HTTP error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Key is unavailable or invalid")]
KeyInvalid,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
@ -26,8 +23,11 @@ pub enum ValidatorError {
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("Key validation failed: {0}")]
Validation(String),
#[error("Key is unavailable or invalid")]
KeyInvalid,
#[error("Invalid Google API key format: {0}")]
KeyFormatInvalid(String),
}
pub type Result<T> = std::result::Result<T, ValidatorError>;

View File

@ -1,7 +1,7 @@
use gemini_keychecker::error::ValidatorError;
use gemini_keychecker::{BANNER, config::KeyCheckerConfig, service::start_validation};
use mimalloc::MiMalloc;
use tracing::info;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
@ -9,11 +9,19 @@ static GLOBAL: MiMalloc = MiMalloc;
/// Main function - displays banner and starts validation service
#[tokio::main]
async fn main() -> Result<(), ValidatorError> {
let config = KeyCheckerConfig::load_config()?;
// Initialize tracing with professional format using configured log level
tracing_subscriber::fmt()
.with_target(false)
.with_level(true)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(&config.log_level)),
)
.init();
// Display banner and configuration status at startup
println!("{BANNER}");
let config = KeyCheckerConfig::load_config()?;
println!("{config}");
info!("Configuration loaded: {}", config);
// Start validation service
start_validation().await

View File

@ -1,6 +1,7 @@
use backon::{ExponentialBuilder, Retryable};
use reqwest::{Client, IntoUrl, StatusCode};
use tokio::time::Duration;
use tracing::{debug, error, info, warn};
use url::Url;
use crate::config::TEST_MESSAGE_BODY;
@ -15,19 +16,23 @@ pub async fn validate_key(
let api_endpoint = api_endpoint.into_url()?;
match send_test_request(client, &api_endpoint, api_key.clone()).await {
Ok(response) => {
let status = response.status();
match status {
StatusCode::OK => Ok(api_key),
StatusCode::UNAUTHORIZED
| StatusCode::FORBIDDEN
| StatusCode::TOO_MANY_REQUESTS => Err(ValidatorError::KeyInvalid),
_ => Err(ValidatorError::ReqwestError(
response.error_for_status().unwrap_err(),
)),
Ok(_) => {
info!("SUCCESS - {}... - Valid key found", &api_key.as_ref()[..10]);
Ok(api_key)
}
Err(e) => {
match e.status() {
Some(StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS) => {
warn!("INVALID - {}... - {}", &api_key.as_ref()[..10], ValidatorError::KeyInvalid);
Err(ValidatorError::KeyInvalid)
}
_ => {
let req_error = ValidatorError::ReqwestError(e);
error!("ERROR- {}... - {}", &api_key.as_ref()[..10], req_error);
Err(req_error)
}
}
}
Err(e) => Err(ValidatorError::ReqwestError(e)),
}
}
@ -49,7 +54,7 @@ async fn send_test_request(
.json(&*TEST_MESSAGE_BODY)
.send()
.await?;
debug!("Response for key {}: {:?}", key.as_ref(), response.status());
response.error_for_status()
})
.retry(&retry_policy)

View File

@ -59,7 +59,6 @@ impl ValidationService {
// Process validated keys and write to output file
while let Some(valid_key) = valid_keys_stream.next().await {
println!("Valid key found: {}", valid_key.as_ref());
if let Err(e) = write_keys_txt_file(&mut buffer_writer, &valid_key).await {
eprintln!("Failed to write key to output file: {}", e);
}
@ -73,7 +72,7 @@ impl ValidationService {
}
}
/// 启动验证服务 - 封装了所有启动逻辑
pub async fn start_validation() -> Result<(), ValidatorError> {
let config = KeyCheckerConfig::load_config()?;

View File

@ -1,3 +1,4 @@
use crate::error::ValidatorError;
use regex::Regex;
use std::str::FromStr;
use std::sync::LazyLock;
@ -14,9 +15,8 @@ impl AsRef<str> for GeminiKey {
}
impl FromStr for GeminiKey {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
type Err = ValidatorError;
fn from_str(s: &str) -> Result<Self, ValidatorError> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^AIzaSy[A-Za-z0-9_-]{33}$").unwrap());
@ -27,7 +27,7 @@ impl FromStr for GeminiKey {
inner: cleaned.to_string(),
})
} else {
Err("Invalid Google API key format")
Err(ValidatorError::KeyFormatInvalid(cleaned.to_string()))
}
}
}