Skip to content

Commit a668065

Browse files
committed
feat: add --json output support and update README
1 parent 6a6d5b9 commit a668065

8 files changed

Lines changed: 206 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ libc = "0.2.180"
3131
ip2location = "0.6.0"
3232
dirs = "6.0.0"
3333
zip = "7.1.0"
34+
serde = { version = "1.0.228", features = ["derive"] }
35+
serde_json = "1.0.149"
3436

3537
[profile.release]
3638
opt-level = "z"

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ PingX is a simple and practical network diagnostic tool designed to replace syst
99
1. **Multi-Protocol**: ICMP, TCP, and HTTP probing.
1010
2. **Dual Stack**: Full support for IPv4, IPv6, and domain resolution.
1111
3. **Concurrency**: Probe multiple targets simultaneously.
12+
4. **GeoIP**: Retrieve geographical location (country, region, city, coordinates) for IP addresses.
13+
5. **JSON Output**: Export results to JSON for machine readability.
1214

1315
## Installation
1416

@@ -87,6 +89,33 @@ Supports probing multiple targets simultaneously. Results are displayed interlea
8789
pingx 1.1.1.1 www.github.com
8890
```
8991

92+
### GeoIP Lookup
93+
94+
Retrieve geographical information for IP addresses. The first run will guide you through downloading the IP2Location database.
95+
96+
```shell
97+
# Lookup physical location
98+
pingx -g 1.1.1.1 8.8.8.8
99+
100+
# Manually fetch/update database
101+
pingx --fetch-geo
102+
```
103+
104+
### JSON Output
105+
106+
Export results in structured JSON format. If one target, outputs an object; if multiple targets, outputs an array.
107+
108+
```shell
109+
# Print JSON to stdout
110+
pingx 1.1.1.1 --json
111+
112+
# Write JSON to file
113+
pingx 1.1.1.1 8.8.8.8 -c 5 --json result.json
114+
115+
# JSON for GeoIP
116+
pingx -g 8.8.8.8 --json
117+
```
118+
90119
### Common Options
91120

92121
- `-c <COUNT>`: Stop after sending count packets.
@@ -110,6 +139,8 @@ PingX 是一款简单实用的网络诊断工具,旨在替代系统的 `ping`
110139
1. **多协议支持**: 支持 ICMP、TCP 和 HTTP 协议探测。
111140
2. **双栈支持**: 完美支持 IPv4、IPv6 地址及域名解析。
112141
3. **并发探测**: 支持同时对多个目标发起探测。
142+
4. **GeoIP 信息**: 获取 IP 地址的物理地理位置(国家、地区、城市、经纬度)。
143+
5. **JSON 输出**: 支持将探测或定位结果以 JSON 格式输出,方便集成。
113144

114145
## 安装
115146

@@ -189,6 +220,33 @@ pingx 可以并发对多个目标以不同协议进行检测。结果将交替
189220
pingx 1.1.1.1 www.github.com
190221
```
191222

223+
### GeoIP 位置查询
224+
225+
获取 IP 地址的物理地理位置信息。第一次运行会引导你下载 IP2Location 数据库。
226+
227+
```shell
228+
# 查询物理位置
229+
pingx -g 1.1.1.1 8.8.8.8
230+
231+
# 手动更新/下载数据库
232+
pingx --fetch-geo
233+
```
234+
235+
### JSON 格式输出
236+
237+
将结果以结构化的 JSON 格式输出。单个目标输出对象 `{...}`,多个目标输出列表 `[{...}, ...]`
238+
239+
```shell
240+
# 打印 JSON 到屏幕
241+
pingx 1.1.1.1 --json
242+
243+
# 保存 JSON 到文件
244+
pingx 1.1.1.1 8.8.8.8 -c 5 --json result.json
245+
246+
# GeoIP 模式输出 JSON
247+
pingx -g 8.8.8.8 --json
248+
```
249+
192250
### 常用参数
193251

194252
- `-c <COUNT>`: 发送数据包的数量。

src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ pub struct Cli {
6464
/// Fetch/Update the GeoIP database.
6565
#[arg(long = "fetch-geo", group = "mode")]
6666
pub fetch_geo: bool,
67+
68+
/// Output results in JSON format. Optional value specifies output file.
69+
#[arg(long = "json", num_args = 0..=1, value_name = "FILE")]
70+
pub json: Option<Option<String>>,
6771
}
6872

6973
fn parse_duration(arg: &str) -> Result<Duration, std::num::ParseFloatError> {

src/geoip.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::{Result, anyhow};
22
use colored::Colorize;
33
use ip2location::{DB, Record};
44
use reqwest::Client;
5+
use serde::Serialize;
56
use std::fs::File;
67
use std::io::{self, Read, Seek, Write};
78
use std::net::IpAddr;
@@ -236,6 +237,7 @@ impl GeoIpManager {
236237
}
237238
}
238239

240+
#[derive(Serialize)]
239241
pub struct GeoRecord {
240242
pub ip: IpAddr,
241243
pub country: String,

src/main.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,30 @@ async fn main() {
6767
}
6868

6969
if !records.is_empty() {
70-
geoip::print_geo_table(&records);
70+
if let Some(json_arg) = &args.json {
71+
// JSON Output
72+
use std::io::Write;
73+
let json_output = if records.len() == 1 {
74+
serde_json::to_string_pretty(&records[0]).unwrap()
75+
} else {
76+
serde_json::to_string_pretty(&records).unwrap()
77+
};
78+
79+
if let Some(path) = json_arg {
80+
// Write to file
81+
if let Ok(mut file) = std::fs::File::create(path) {
82+
let _ = file.write_all(json_output.as_bytes());
83+
} else {
84+
eprintln!("pingx: Failed to write JSON to {}", path);
85+
std::process::exit(1);
86+
}
87+
} else {
88+
// Write to stdout
89+
println!("{}", json_output);
90+
}
91+
} else {
92+
geoip::print_geo_table(&records);
93+
}
7194
}
7295

7396
return;

src/session.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::pinger::Pinger;
33
use crate::utils::{IpVersion, resolve_host};
44
use anyhow::Result;
55
use colored::*;
6+
use serde::Serialize;
67
use std::collections::HashMap;
78
use std::time::Duration;
89
use tokio::signal;
@@ -66,6 +67,24 @@ mod models {
6667

6768
use std::sync::Arc;
6869

70+
#[derive(Serialize)]
71+
pub struct JsonResult {
72+
pub target: String,
73+
pub protocol: String,
74+
pub ip: String,
75+
pub packet_size: usize,
76+
pub ttl: u32,
77+
pub sent: u64,
78+
pub received: u64,
79+
pub loss: f64,
80+
pub time: f64,
81+
pub min: f64,
82+
pub avg: f64,
83+
pub max: f64,
84+
pub mdev: f64,
85+
pub jitter: f64,
86+
}
87+
6988
pub struct Session {
7089
cli: Cli,
7190
}
@@ -286,6 +305,100 @@ impl Session {
286305
p.stop().await.ok();
287306
}
288307

308+
// JSON Output Logic
309+
if let Some(json_arg) = &self.cli.json {
310+
let mut json_results = Vec::new();
311+
312+
for target_host in targets {
313+
if let Some(stats) = all_stats.get(target_host) {
314+
let protocol = target_protocols.get(target_host).unwrap_or(&crate::cli::Protocol::Icmp);
315+
let protocol_str = match protocol {
316+
crate::cli::Protocol::Icmp => "ICMP",
317+
crate::cli::Protocol::Tcp(_) => "TCP",
318+
crate::cli::Protocol::Http(_) => "HTTP",
319+
}.to_string();
320+
321+
// Calculate stats
322+
let loss = if stats.transmitted > 0 {
323+
100.0 * (1.0 - stats.received as f64 / stats.transmitted as f64)
324+
} else {
325+
0.0
326+
};
327+
let total_time = stats.start_time.elapsed().as_secs_f64() * 1000.0;
328+
329+
let (min, max, avg, mdev, jitter) = if stats.received > 0 {
330+
let min = stats.rtts.iter().min().unwrap().as_secs_f64() * 1000.0;
331+
let max = stats.rtts.iter().max().unwrap().as_secs_f64() * 1000.0;
332+
let avg = stats.rtts.iter().sum::<Duration>().as_secs_f64() * 1000.0 / stats.rtts.len() as f64;
333+
334+
let avg_duration = Duration::from_secs_f64(avg / 1000.0);
335+
let sum_sq_diff: f64 = stats.rtts.iter()
336+
.map(|rtt| (rtt.as_secs_f64() - avg_duration.as_secs_f64()).abs())
337+
.sum();
338+
let mdev = sum_sq_diff / stats.rtts.len() as f64 * 1000.0;
339+
340+
let jitter = if stats.rtts.len() > 1 {
341+
let sum_diff: f64 = stats.rtts.windows(2)
342+
.map(|w| (w[1].as_secs_f64() - w[0].as_secs_f64()).abs())
343+
.sum();
344+
sum_diff / (stats.rtts.len() - 1) as f64 * 1000.0
345+
} else {
346+
0.0
347+
};
348+
(
349+
(min * 1000.0).round() / 1000.0,
350+
(max * 1000.0).round() / 1000.0,
351+
(avg * 1000.0).round() / 1000.0,
352+
(mdev * 1000.0).round() / 1000.0,
353+
(jitter * 1000.0).round() / 1000.0,
354+
)
355+
} else {
356+
(0.0, 0.0, 0.0, 0.0, 0.0)
357+
};
358+
359+
json_results.push(JsonResult {
360+
target: target_host.clone(),
361+
protocol: protocol_str,
362+
ip: stats._address.to_string(),
363+
packet_size: self.cli.size,
364+
ttl: self.cli.ttl,
365+
sent: stats.transmitted,
366+
received: stats.received,
367+
loss: (loss * 1000.0).round() / 1000.0, // Also round loss? Or keep precision? Instructions said "time values".
368+
// Let's stick to time values for strict compliance,
369+
// but usually nice to format loss too.
370+
// Re-reading: "json 中的各项时间保留 3 位小数" -> time values only.
371+
// Loss is percentage. I will round time values.
372+
time: (total_time * 1000.0).round() / 1000.0,
373+
min,
374+
avg,
375+
max,
376+
mdev,
377+
jitter,
378+
});
379+
}
380+
}
381+
382+
use std::io::Write;
383+
let json_output = if json_results.len() == 1 {
384+
serde_json::to_string_pretty(&json_results[0]).unwrap()
385+
} else {
386+
serde_json::to_string_pretty(&json_results).unwrap()
387+
};
388+
389+
if let Some(path) = json_arg {
390+
if let Ok(mut file) = std::fs::File::create(path) {
391+
let _ = file.write_all(json_output.as_bytes());
392+
} else {
393+
eprintln!("pingx: Failed to write JSON to {}", path);
394+
}
395+
} else {
396+
println!("{}", json_output);
397+
}
398+
399+
return Ok(());
400+
}
401+
289402
// Collect table data and calculate global column widths
290403
let mut tables = Vec::new();
291404
let mut global_key_widths = [0usize; 3];

src/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ mod tests {
424424
headers: vec![],
425425
geo: false,
426426
fetch_geo: false,
427+
json: None,
427428
};
428429

429430
// 1. Basic ICMP (Domain)

0 commit comments

Comments
 (0)