项目初始化
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(mkdir:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
163
Makefile
Normal file
163
Makefile
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 项目信息
|
||||||
|
BINARY_NAME=nodeprobe
|
||||||
|
MODULE_NAME=github.com/meowrain/nodeprobe
|
||||||
|
MAIN_PATH=./cmd/nodeprobe
|
||||||
|
|
||||||
|
# Go 相关命令
|
||||||
|
GO=go
|
||||||
|
GOFLAGS=-v
|
||||||
|
GOBUILD=$(GO) build $(GOFLAGS)
|
||||||
|
GOCLEAN=$(GO) clean
|
||||||
|
GOTEST=$(GO) test
|
||||||
|
GOGET=$(GO) get
|
||||||
|
GOMOD=$(GO) mod
|
||||||
|
|
||||||
|
# 输出目录
|
||||||
|
BUILD_DIR=build
|
||||||
|
DIST_DIR=dist
|
||||||
|
|
||||||
|
# 版本信息
|
||||||
|
VERSION?=dev
|
||||||
|
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
|
||||||
|
|
||||||
|
# 默认目标
|
||||||
|
.PHONY: all
|
||||||
|
all: clean build
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE) $(MAIN_PATH)
|
||||||
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
# 快速构建(不清理)
|
||||||
|
.PHONY: quick
|
||||||
|
quick:
|
||||||
|
@echo "Quick building $(BINARY_NAME)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE) $(MAIN_PATH)
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
.PHONY: run
|
||||||
|
run: build
|
||||||
|
@echo "Running $(BINARY_NAME)..."
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME)$(shell go env GOEXE)
|
||||||
|
|
||||||
|
# 直接运行(不构建)
|
||||||
|
.PHONY: dev
|
||||||
|
dev:
|
||||||
|
@echo "Running $(BINARY_NAME) in development mode..."
|
||||||
|
$(GO) run $(MAIN_PATH)
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
$(GOTEST) -v -cover ./...
|
||||||
|
|
||||||
|
# 测试覆盖率
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage:
|
||||||
|
@echo "Running tests with coverage..."
|
||||||
|
$(GOTEST) -v -coverprofile=coverage.out ./...
|
||||||
|
$(GO) tool cover -html=coverage.out -o coverage.html
|
||||||
|
@echo "Coverage report generated: coverage.html"
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning..."
|
||||||
|
$(GOCLEAN)
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@rm -rf $(DIST_DIR)
|
||||||
|
@rm -f coverage.out coverage.html
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# 依赖管理
|
||||||
|
.PHONY: deps
|
||||||
|
deps:
|
||||||
|
@echo "Downloading dependencies..."
|
||||||
|
$(GOMOD) download
|
||||||
|
$(GOMOD) tidy
|
||||||
|
|
||||||
|
# 更新依赖
|
||||||
|
.PHONY: deps-update
|
||||||
|
deps-update:
|
||||||
|
@echo "Updating dependencies..."
|
||||||
|
$(GOGET) -u ./...
|
||||||
|
$(GOMOD) tidy
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@echo "Running linter..."
|
||||||
|
@which golangci-lint > /dev/null || (echo "golangci-lint not found. Install it from https://golangci-lint.run/usage/install/" && exit 1)
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt:
|
||||||
|
@echo "Formatting code..."
|
||||||
|
$(GO) fmt ./...
|
||||||
|
|
||||||
|
# 代码审查
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
@echo "Running go vet..."
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
|
# 交叉编译(多平台)
|
||||||
|
.PHONY: build-all
|
||||||
|
build-all: clean
|
||||||
|
@echo "Building for multiple platforms..."
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
@echo "Building for Windows amd64..."
|
||||||
|
@GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH)
|
||||||
|
@echo "Building for Linux amd64..."
|
||||||
|
@GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH)
|
||||||
|
@echo "Building for Linux arm64..."
|
||||||
|
@GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PATH)
|
||||||
|
@echo "Building for macOS amd64..."
|
||||||
|
@GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH)
|
||||||
|
@echo "Building for macOS arm64..."
|
||||||
|
@GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH)
|
||||||
|
@echo "All builds complete in $(DIST_DIR)/"
|
||||||
|
|
||||||
|
# 安装到系统
|
||||||
|
.PHONY: install
|
||||||
|
install: build
|
||||||
|
@echo "Installing $(BINARY_NAME)..."
|
||||||
|
$(GO) install $(LDFLAGS) $(MAIN_PATH)
|
||||||
|
@echo "Installed to $(shell go env GOPATH)/bin/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
# 卸载
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall:
|
||||||
|
@echo "Uninstalling $(BINARY_NAME)..."
|
||||||
|
@rm -f $(shell go env GOPATH)/bin/$(BINARY_NAME)$(shell go env GOEXE)
|
||||||
|
@echo "Uninstall complete"
|
||||||
|
|
||||||
|
# 帮助信息
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " make build - Build the binary"
|
||||||
|
@echo " make quick - Quick build without cleaning"
|
||||||
|
@echo " make run - Build and run the binary"
|
||||||
|
@echo " make dev - Run without building (using go run)"
|
||||||
|
@echo " make test - Run tests"
|
||||||
|
@echo " make coverage - Run tests with coverage report"
|
||||||
|
@echo " make clean - Clean build artifacts"
|
||||||
|
@echo " make deps - Download and tidy dependencies"
|
||||||
|
@echo " make deps-update - Update dependencies"
|
||||||
|
@echo " make lint - Run linter (requires golangci-lint)"
|
||||||
|
@echo " make fmt - Format code"
|
||||||
|
@echo " make vet - Run go vet"
|
||||||
|
@echo " make build-all - Build for multiple platforms"
|
||||||
|
@echo " make install - Install binary to GOPATH/bin"
|
||||||
|
@echo " make uninstall - Uninstall binary from GOPATH/bin"
|
||||||
|
@echo " make help - Show this help message"
|
||||||
92
cmd/nodeprobe/main.go
Normal file
92
cmd/nodeprobe/main.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/meowrain/nodeprobe/internal/netstat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. 获取初始数据
|
||||||
|
prevStats, err := netstat.GetInterfaceStats()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(prevStats) == 0 {
|
||||||
|
fmt.Println("No network interfaces found")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置刷新间隔(默认 1 秒)
|
||||||
|
interval := 1 * time.Second
|
||||||
|
|
||||||
|
// 3. 打印表头
|
||||||
|
printHeader()
|
||||||
|
|
||||||
|
// 4. 创建 ticker 定时器
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// 5. 监听 Ctrl+C 退出
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// 6. 主循环
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// 获取当前统计数据
|
||||||
|
currStats, err := netstat.GetInterfaceStats()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算速率
|
||||||
|
rates := netstat.CalculateRate(prevStats, currStats, interval)
|
||||||
|
|
||||||
|
// 打印一行统计
|
||||||
|
printStats(rates, currStats)
|
||||||
|
|
||||||
|
// 更新上一次的数据
|
||||||
|
prevStats = currStats
|
||||||
|
|
||||||
|
case <-sigChan:
|
||||||
|
fmt.Println("\nExiting...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHeader() {
|
||||||
|
fmt.Println("Network Interface Traffic Monitor")
|
||||||
|
fmt.Println("----------------------------------")
|
||||||
|
fmt.Printf("%-10s %15s %15s %15s %15s\n",
|
||||||
|
"Interface", "RX Rate", "TX Rate", "RX Total", "TX Total")
|
||||||
|
fmt.Println("----------------------------------")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStats(rates []netstat.TrafficRate, stats []netstat.InterfaceStats) {
|
||||||
|
// 创建统计 map
|
||||||
|
statsMap := make(map[string]netstat.InterfaceStats)
|
||||||
|
for _, s := range stats {
|
||||||
|
statsMap[s.Name] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rates {
|
||||||
|
s := statsMap[r.Name]
|
||||||
|
fmt.Printf("%-10s %15s %15s %15s %15s\n",
|
||||||
|
r.Name,
|
||||||
|
netstat.FormatBps(r.RxKbps*1000),
|
||||||
|
netstat.FormatBps(r.TxKbps*1000),
|
||||||
|
netstat.FormatBytes(float64(s.RxBytes)),
|
||||||
|
netstat.FormatBytes(float64(s.TxBytes)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
134
internal/netstat/netstat.go
Normal file
134
internal/netstat/netstat.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package netstat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InterfaceStats 网卡统计信息
|
||||||
|
|
||||||
|
type InterfaceStats struct {
|
||||||
|
Name string //网卡名称
|
||||||
|
RxBytes uint64 //接收字节数
|
||||||
|
TxBytes uint64 //发送字节数
|
||||||
|
RxPackets uint64 //接收数据包数
|
||||||
|
TxPackets uint64 //发送数据包数
|
||||||
|
RxErrors uint64 //接收错误数
|
||||||
|
TxErrors uint64 //发送错误数
|
||||||
|
}
|
||||||
|
type TrafficRate struct {
|
||||||
|
Name string // 网卡名称
|
||||||
|
RxBytesRate float64 // 接收字节速率
|
||||||
|
TxBytesRate float64 // 发送字节速率
|
||||||
|
RxKbps float64 // 接收比特速率
|
||||||
|
TxKbps float64 // 发送比特速率
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaceStats 获取所有网卡的统计信息
|
||||||
|
func GetInterfaceStats() ([]InterfaceStats, error) {
|
||||||
|
// 打开/proc/net/dev文件
|
||||||
|
file, err := os.Open("/proc/net/dev")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 创建scanner逐行读取文件内容
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
// 跳过前两行 头部信息
|
||||||
|
scanner.Scan()
|
||||||
|
scanner.Scan()
|
||||||
|
|
||||||
|
var stats []InterfaceStats
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
// 按空格分割
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 17 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 提取网卡名
|
||||||
|
ifaceName := strings.TrimSuffix(fields[0], ":")
|
||||||
|
if ifaceName == "lo" {
|
||||||
|
continue //跳过回环接口
|
||||||
|
}
|
||||||
|
|
||||||
|
stat := InterfaceStats{
|
||||||
|
Name: ifaceName,
|
||||||
|
RxBytes: parseUint64(fields[1]),
|
||||||
|
RxPackets: parseUint64(fields[2]),
|
||||||
|
RxErrors: parseUint64(fields[3]),
|
||||||
|
TxBytes: parseUint64(fields[9]),
|
||||||
|
TxPackets: parseUint64(fields[10]),
|
||||||
|
TxErrors: parseUint64(fields[11]),
|
||||||
|
}
|
||||||
|
stats = append(stats, stat)
|
||||||
|
}
|
||||||
|
return stats, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint64(s string) uint64 {
|
||||||
|
var val uint64
|
||||||
|
fmt.Sscanf(s, "%d", &val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateRate(prev, curr []InterfaceStats, duration time.Duration) []TrafficRate {
|
||||||
|
prevMap := make(map[string]InterfaceStats)
|
||||||
|
for _, stat := range prev {
|
||||||
|
prevMap[stat.Name] = stat
|
||||||
|
}
|
||||||
|
var rates []TrafficRate
|
||||||
|
seconds := duration.Seconds()
|
||||||
|
for _, c := range curr {
|
||||||
|
p, exists := prevMap[c.Name]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rxBytesDiff := c.RxBytes - p.RxBytes
|
||||||
|
txBytesDiff := c.TxBytes - p.TxBytes
|
||||||
|
rate := TrafficRate{
|
||||||
|
Name: c.Name,
|
||||||
|
RxBytesRate: float64(rxBytesDiff) / seconds,
|
||||||
|
TxBytesRate: float64(txBytesDiff) / seconds,
|
||||||
|
RxKbps: (float64(rxBytesDiff) * 8) / (seconds * 1024),
|
||||||
|
TxKbps: (float64(txBytesDiff) * 8) / (seconds * 1024),
|
||||||
|
}
|
||||||
|
rates = append(rates, rate)
|
||||||
|
}
|
||||||
|
return rates
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatBytes(bytes float64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%.2f B", bytes)
|
||||||
|
}
|
||||||
|
div, exp := uint64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f %cB", bytes/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatBps 格式化比特率为 1.23 Kbps、4.56 Mbps 等
|
||||||
|
func FormatBps(bps float64) string {
|
||||||
|
const unit = 1000
|
||||||
|
if bps < unit {
|
||||||
|
return fmt.Sprintf("%.2f bps", bps)
|
||||||
|
}
|
||||||
|
|
||||||
|
div, exp := float64(unit), 0
|
||||||
|
for n := bps / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%.2f %cps", bps/div, "KMGTPE"[exp])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user