12 files changed
+403
-61
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -140,6 +141,20 @@ var registerCmd = &cobra.Command{
|
||||
Run: registerAgent,
|
||||
}
|
||||
|
||||
// logCmd 查看日志命令
|
||||
var logCmd = &cobra.Command{
|
||||
Use: "log",
|
||||
Short: "查看日志",
|
||||
Long: `查看 Agent 运行日志,支持实时跟踪`,
|
||||
Run: viewLog,
|
||||
}
|
||||
|
||||
var (
|
||||
logFollow bool
|
||||
logLines int
|
||||
logService bool
|
||||
)
|
||||
|
||||
// infoCmd 信息命令
|
||||
var infoCmd = &cobra.Command{
|
||||
Use: "info",
|
||||
@@ -174,10 +189,16 @@ func init() {
|
||||
registerCmd.Flags().StringVarP(&agentName, "name", "n", "", "探针名称(默认使用主机名)")
|
||||
registerCmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "自动确认配置并继续安装")
|
||||
|
||||
// log 命令参数
|
||||
logCmd.Flags().BoolVarP(&logFollow, "follow", "f", false, "实时跟踪日志输出")
|
||||
logCmd.Flags().IntVarP(&logLines, "lines", "n", 100, "显示最近多少行日志")
|
||||
logCmd.Flags().BoolVarP(&logService, "service", "s", false, "强制查看服务日志(跳过日志文件)")
|
||||
|
||||
// 添加子命令
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(registerCmd) // 注册命令放在前面,方便用户发现
|
||||
rootCmd.AddCommand(infoCmd)
|
||||
rootCmd.AddCommand(logCmd)
|
||||
rootCmd.AddCommand(runCmd)
|
||||
rootCmd.AddCommand(installCmd)
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
@@ -591,3 +612,196 @@ func showInfo(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf(" 主机名: %s\n", hostname)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// viewLog 查看日志
|
||||
func viewLog(cmd *cobra.Command, args []string) {
|
||||
// 加载配置
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 判断是否以服务模式运行
|
||||
serviceRunning := false
|
||||
if runtime.GOOS == "linux" {
|
||||
// 检查 systemd 服务状态
|
||||
if out, err := exec.Command("systemctl", "is-active", "pika-agent").CombinedOutput(); err == nil && strings.TrimSpace(string(out)) == "active" {
|
||||
serviceRunning = true
|
||||
}
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
if out, err := exec.Command("launchctl", "list", "org.pika.pika-agent").CombinedOutput(); err == nil && len(strings.TrimSpace(string(out))) > 0 {
|
||||
serviceRunning = true
|
||||
}
|
||||
} else if runtime.GOOS == "windows" {
|
||||
if out, err := exec.Command("sc", "query", "pika-agent").CombinedOutput(); err == nil && strings.Contains(string(out), "RUNNING") {
|
||||
serviceRunning = true
|
||||
}
|
||||
}
|
||||
|
||||
// 决定日志来源
|
||||
// 1. 如果指定了 --service 或者服务正在运行,查看服务日志
|
||||
// 2. 否则,查看日志文件
|
||||
if logService || serviceRunning {
|
||||
viewServiceLog(cfg)
|
||||
} else {
|
||||
viewLogFile(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// viewServiceLog 查看系统服务日志
|
||||
func viewServiceLog(cfg *config.Config) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
viewLinuxServiceLog(cfg)
|
||||
case "darwin":
|
||||
viewDarwinServiceLog()
|
||||
case "windows":
|
||||
viewWindowsServiceLog()
|
||||
default:
|
||||
log.Printf("⚠️ 当前系统 (%s) 不支持查看服务日志", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// viewLinuxServiceLog Linux 系统服务日志查看(优先 journalctl,空时回退到日志文件)
|
||||
func viewLinuxServiceLog(cfg *config.Config) {
|
||||
args := []string{"journalctl", "-u", "pika-agent", "-n", fmt.Sprintf("%d", logLines)}
|
||||
if logFollow {
|
||||
args = append(args, "-f")
|
||||
}
|
||||
|
||||
// 非跟踪模式:先检查 journal 是否为空
|
||||
if !logFollow {
|
||||
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
|
||||
output := strings.TrimSpace(string(out))
|
||||
if err != nil || output == "" || strings.Contains(output, "-- No entries --") {
|
||||
// journal 为空,回退到日志文件
|
||||
viewLogFile(cfg)
|
||||
return
|
||||
}
|
||||
fmt.Printf("📋 系统服务日志 (最近 %d 行):\n", logLines)
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
fmt.Println(output)
|
||||
return
|
||||
}
|
||||
|
||||
// 跟踪模式:先快速检查 journal 是否有内容,再决定是否回退
|
||||
checkOut, _ := exec.Command("journalctl", "-u", "pika-agent", "-n", "1").CombinedOutput()
|
||||
checkOutput := strings.TrimSpace(string(checkOut))
|
||||
if checkOutput == "" || strings.Contains(checkOutput, "-- No entries --") {
|
||||
// journal 为空,回退到日志文件
|
||||
viewLogFile(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("📋 正在跟踪系统服务日志 (最近 %d 行)...\n", logLines)
|
||||
fmt.Println("按 Ctrl+C 退出")
|
||||
fmt.Println()
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("⚠️ 查看服务日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// viewDarwinServiceLog macOS 系统服务日志查看
|
||||
func viewDarwinServiceLog() {
|
||||
args := []string{"log", "show"}
|
||||
args = append(args, "--predicate", `subsystem == "org.pika.pika-agent"`)
|
||||
args = append(args, "--last", fmt.Sprintf("%d", logLines))
|
||||
if logFollow {
|
||||
args = append(args, "--stream")
|
||||
}
|
||||
|
||||
if logFollow {
|
||||
fmt.Printf("📋 正在跟踪系统服务日志 (最近 %d 行)...\n", logLines)
|
||||
fmt.Println("按 Ctrl+C 退出")
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf("📋 系统服务日志 (最近 %d 行):\n", logLines)
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
}
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("⚠️ 查看服务日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// viewWindowsServiceLog Windows 系统服务日志查看
|
||||
func viewWindowsServiceLog() {
|
||||
psCmd := fmt.Sprintf("Get-WinEvent -FilterHashtable @{LogName='Application';ProviderName='pika-agent'} -MaxEvents %d | Format-List TimeCreated, Message", logLines)
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-Command", psCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if logFollow {
|
||||
fmt.Printf("📋 正在跟踪系统服务日志 (最近 %d 行)...\n", logLines)
|
||||
fmt.Println("按 Ctrl+C 退出")
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf("📋 系统服务日志 (最近 %d 行):\n", logLines)
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("⚠️ 查看服务日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// viewLogFile 查看日志文件
|
||||
func viewLogFile(cfg *config.Config) {
|
||||
logFile := cfg.Agent.LogFile
|
||||
if logFile == "" {
|
||||
// 默认日志路径
|
||||
logFile = getHomeLogPath()
|
||||
}
|
||||
|
||||
// 检查日志文件是否存在
|
||||
if _, err := os.Stat(logFile); os.IsNotExist(err) {
|
||||
log.Printf("⚠️ 日志文件不存在: %s", logFile)
|
||||
fmt.Println()
|
||||
fmt.Println("💡 提示: 如果 Agent 以服务方式运行,请使用以下命令查看日志:")
|
||||
if runtime.GOOS == "linux" {
|
||||
fmt.Println(" journalctl -u pika-agent -n 100 -f")
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
fmt.Println(" log show --predicate 'subsystem == \"org.pika.pika-agent\"' --last 100")
|
||||
} else if runtime.GOOS == "windows" {
|
||||
fmt.Println(" Get-WinEvent -FilterHashtable @{LogName='Application';ProviderName='pika-agent'} -MaxEvents 100")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("或者使用 --service 参数直接查看服务日志:")
|
||||
fmt.Println(" agent log --service")
|
||||
return
|
||||
}
|
||||
|
||||
if logFollow {
|
||||
fmt.Printf("📋 正在跟踪日志: %s (最近 %d 行)...\n", logFile, logLines)
|
||||
fmt.Println("按 Ctrl+C 退出")
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
|
||||
// 使用 tail -f
|
||||
cmd := exec.Command("tail", "-f", "-n", fmt.Sprintf("%d", logLines), logFile)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("⚠️ 跟踪日志失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("📋 日志文件: %s (最近 %d 行)\n", logFile, logLines)
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
|
||||
// 读取文件最后 N 行
|
||||
cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", logLines), logFile)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("⚠️ 读取日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,11 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
if err := json.Unmarshal(data, &diskDataList); err != nil {
|
||||
return err
|
||||
}
|
||||
// 无有效磁盘数据时,不更新缓存(保留上一次有效值)
|
||||
if len(diskDataList) == 0 {
|
||||
metrics := s.convertToMetrics(agentID, metricType, diskDataList, timestamp)
|
||||
return s.vmClient.Write(ctx, metrics)
|
||||
}
|
||||
// 计算汇总数据用于缓存
|
||||
var totalTotal, totalUsed, totalFree uint64
|
||||
for _, diskData := range diskDataList {
|
||||
@@ -191,10 +196,7 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
totalUsed += diskData.Used
|
||||
totalFree += diskData.Free
|
||||
}
|
||||
var usagePercent float64
|
||||
if totalTotal > 0 {
|
||||
usagePercent = float64(totalUsed) / float64(totalTotal) * 100
|
||||
}
|
||||
usagePercent := float64(totalUsed) / float64(totalTotal) * 100
|
||||
latestMetrics.Disk = &metric.DiskSummary{
|
||||
UsagePercent: usagePercent,
|
||||
TotalDisks: len(diskDataList),
|
||||
@@ -210,6 +212,11 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
if err := json.Unmarshal(data, &networkDataList); err != nil {
|
||||
return err
|
||||
}
|
||||
// 无有效网络数据时,不更新缓存(保留上一次有效值)
|
||||
if len(networkDataList) == 0 {
|
||||
metrics := s.convertToMetrics(agentID, metricType, networkDataList, timestamp)
|
||||
return s.vmClient.Write(ctx, metrics)
|
||||
}
|
||||
// 计算汇总数据用于缓存
|
||||
var totalSentRate, totalRecvRate uint64
|
||||
var totalSentTotal, totalRecvTotal uint64
|
||||
@@ -266,6 +273,11 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
if err := json.Unmarshal(data, &gpuDataList); err != nil {
|
||||
return err
|
||||
}
|
||||
// 无 GPU 数据时,不更新缓存
|
||||
if len(gpuDataList) == 0 {
|
||||
metrics := s.convertToMetrics(agentID, metricType, gpuDataList, timestamp)
|
||||
return s.vmClient.Write(ctx, metrics)
|
||||
}
|
||||
// 更新缓存
|
||||
latestMetrics.GPU = gpuDataList
|
||||
metrics := s.convertToMetrics(agentID, metricType, gpuDataList, timestamp)
|
||||
@@ -276,6 +288,11 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
if err := json.Unmarshal(data, &tempDataList); err != nil {
|
||||
return err
|
||||
}
|
||||
// 无温度数据时,不更新缓存
|
||||
if len(tempDataList) == 0 {
|
||||
metrics := s.convertToMetrics(agentID, metricType, tempDataList, timestamp)
|
||||
return s.vmClient.Write(ctx, metrics)
|
||||
}
|
||||
// 更新缓存
|
||||
latestMetrics.Temp = tempDataList
|
||||
metrics := s.convertToMetrics(agentID, metricType, tempDataList, timestamp)
|
||||
|
||||
@@ -57,5 +57,10 @@ func (d *DiskCollector) Collect() ([]protocol.DiskData, error) {
|
||||
diskDataList = append(diskDataList, diskData)
|
||||
}
|
||||
|
||||
// 所有白名单分区均采集失败时,返回空列表表示异常
|
||||
if len(diskDataList) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return diskDataList, nil
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dushixiang/pika/internal/protocol"
|
||||
)
|
||||
|
||||
const nvidiaSmiTimeout = 15 * time.Second
|
||||
|
||||
// gpuStaticInfo GPU 静态信息
|
||||
type gpuStaticInfo struct {
|
||||
Index int
|
||||
@@ -42,7 +46,9 @@ func (g *GPUCollector) initStatic() {
|
||||
}
|
||||
|
||||
// 查询静态信息: index, name, uuid, memory.total
|
||||
cmd := exec.Command("nvidia-smi",
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nvidiaSmiTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "nvidia-smi",
|
||||
"--query-gpu=index,name,uuid,memory.total",
|
||||
"--format=csv,noheader,nounits")
|
||||
|
||||
@@ -105,7 +111,9 @@ func (g *GPUCollector) Collect() ([]*protocol.GPUData, error) {
|
||||
func (g *GPUCollector) collectDynamic() ([]*protocol.GPUData, error) {
|
||||
// 使用 nvidia-smi 查询动态数据
|
||||
// 输出格式: index, temperature.gpu, utilization.gpu, memory.used, memory.free, power.draw, fan.speed
|
||||
cmd := exec.Command("nvidia-smi",
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nvidiaSmiTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "nvidia-smi",
|
||||
"--query-gpu=index,temperature.gpu,utilization.gpu,memory.used,memory.free,power.draw,fan.speed",
|
||||
"--format=csv,noheader,nounits")
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ func (m *Manager) CollectAndSendMemory(conn WebSocketWriter) error {
|
||||
// CollectAndSendDisk 采集并发送磁盘指标
|
||||
func (m *Manager) CollectAndSendDisk(conn WebSocketWriter) error {
|
||||
diskDataList, err := m.diskCollector.Collect()
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil || len(diskDataList) == 0 {
|
||||
// 采集失败或无有效数据,不发送空列表
|
||||
return nil
|
||||
}
|
||||
return m.sendMetrics(conn, protocol.MetricTypeDisk, diskDataList)
|
||||
}
|
||||
@@ -85,8 +86,9 @@ func (m *Manager) CollectAndSendDiskIO(conn WebSocketWriter) error {
|
||||
// CollectAndSendNetwork 采集并发送网络指标
|
||||
func (m *Manager) CollectAndSendNetwork(conn WebSocketWriter) error {
|
||||
networkDataList, err := m.networkCollector.Collect()
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil || len(networkDataList) == 0 {
|
||||
// 采集失败或无有效数据,不发送空列表
|
||||
return nil
|
||||
}
|
||||
return m.sendMetrics(conn, protocol.MetricTypeNetwork, networkDataList)
|
||||
}
|
||||
|
||||
+121
-39
@@ -28,9 +28,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
agentPingInterval = 10 * time.Second
|
||||
agentPongWait = 30 * time.Second
|
||||
agentWriteWait = 5 * time.Second
|
||||
agentPingInterval = 10 * time.Second
|
||||
agentPongWait = 30 * time.Second
|
||||
agentWriteWait = 5 * time.Second
|
||||
agentCollectTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// safeConn 线程安全的 WebSocket 连接包装器
|
||||
@@ -63,6 +64,13 @@ func (sc *safeConn) Close() error {
|
||||
return sc.conn.Close()
|
||||
}
|
||||
|
||||
// WriteControl 线程安全地写入控制消息
|
||||
func (sc *safeConn) WriteControl(messageType int, data []byte, deadline time.Time) error {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
return sc.conn.WriteControl(messageType, data, deadline)
|
||||
}
|
||||
|
||||
// Agent 探针服务
|
||||
type Agent struct {
|
||||
cfg *config.Config
|
||||
@@ -222,7 +230,7 @@ func (a *Agent) runOnce(ctx context.Context, onRegistered func()) error {
|
||||
|
||||
// 主动发送 Ping,快速检测断线
|
||||
wg.Go(func() {
|
||||
if err := a.pingLoop(ctx, rawConn, done); err != nil {
|
||||
if err := a.pingLoop(ctx, conn, done); err != nil {
|
||||
select {
|
||||
case errChan <- fmt.Errorf("ping失败: %w", err):
|
||||
default:
|
||||
@@ -314,7 +322,7 @@ func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) pingLoop(ctx context.Context, conn *websocket.Conn, done chan struct{}) error {
|
||||
func (a *Agent) pingLoop(ctx context.Context, conn *safeConn, done chan struct{}) error {
|
||||
ticker := time.NewTicker(agentPingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -529,6 +537,42 @@ func (a *Agent) collectAndSendAllMetrics(manager *collector.Manager) error {
|
||||
return fmt.Errorf("采集器未初始化")
|
||||
}
|
||||
|
||||
type result struct {
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
done <- result{err: a.doCollectAndSend(manager)}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
return r.err
|
||||
case <-time.After(agentCollectTimeout):
|
||||
return fmt.Errorf("指标采集超时 (超过 %v)", agentCollectTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// collectTimer 采集耗时计时器,记录每个采集器的耗时
|
||||
type collectTimer struct {
|
||||
name string
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func newCollectTimer(name string) *collectTimer {
|
||||
return &collectTimer{name: name, start: time.Now()}
|
||||
}
|
||||
|
||||
func (t *collectTimer) done() {
|
||||
d := time.Since(t.start)
|
||||
if d > 500*time.Millisecond {
|
||||
slog.Info("采集耗时", "collector", t.name, "duration", d)
|
||||
}
|
||||
}
|
||||
|
||||
// doCollectAndSend 执行实际的采集和发送
|
||||
func (a *Agent) doCollectAndSend(manager *collector.Manager) error {
|
||||
conn := a.getActiveConn()
|
||||
if conn != nil {
|
||||
if sent, err := a.outboundBuffer.Flush(conn); err != nil {
|
||||
@@ -543,56 +587,92 @@ func (a *Agent) collectAndSendAllMetrics(manager *collector.Manager) error {
|
||||
var hasError bool
|
||||
|
||||
// CPU 动态指标
|
||||
if err := manager.CollectAndSendCPU(bw); err != nil {
|
||||
slog.Warn("采集CPU指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("cpu")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendCPU(bw); err != nil {
|
||||
slog.Warn("采集CPU指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 内存动态指标
|
||||
if err := manager.CollectAndSendMemory(bw); err != nil {
|
||||
slog.Warn("采集内存指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("memory")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendMemory(bw); err != nil {
|
||||
slog.Warn("采集内存指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 磁盘指标
|
||||
if err := manager.CollectAndSendDisk(bw); err != nil {
|
||||
slog.Warn("采集磁盘指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("disk")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendDisk(bw); err != nil {
|
||||
slog.Warn("采集磁盘指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 磁盘 IO 指标
|
||||
if err := manager.CollectAndSendDiskIO(bw); err != nil {
|
||||
slog.Warn("采集磁盘IO指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("disk_io")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendDiskIO(bw); err != nil {
|
||||
slog.Warn("采集磁盘IO指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 网络指标
|
||||
if err := manager.CollectAndSendNetwork(bw); err != nil {
|
||||
slog.Warn("采集网络指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("network")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendNetwork(bw); err != nil {
|
||||
slog.Warn("采集网络指标失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 网络连接统计
|
||||
if err := manager.CollectAndSendNetworkConnection(bw); err != nil {
|
||||
slog.Warn("采集网络连接统计失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("network_connection")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendNetworkConnection(bw); err != nil {
|
||||
slog.Warn("采集网络连接统计失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// 主机信息(包含 Load)
|
||||
if err := manager.CollectAndSendHost(bw); err != nil {
|
||||
slog.Warn("采集主机信息失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("host")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendHost(bw); err != nil {
|
||||
slog.Warn("采集主机信息失败", "error", err)
|
||||
hasError = true
|
||||
}
|
||||
}()
|
||||
|
||||
// GPU 信息(可选)
|
||||
if err := manager.CollectAndSendGPU(bw); err != nil {
|
||||
slog.Info("采集GPU信息失败", "error", err)
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("gpu")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendGPU(bw); err != nil {
|
||||
slog.Info("采集GPU信息失败", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 温度信息(可选)
|
||||
if err := manager.CollectAndSendTemperature(bw); err != nil {
|
||||
slog.Info("采集温度信息失败", "error", err)
|
||||
}
|
||||
func() {
|
||||
t := newCollectTimer("temperature")
|
||||
defer t.done()
|
||||
if err := manager.CollectAndSendTemperature(bw); err != nil {
|
||||
slog.Info("采集温度信息失败", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送打包后的指标
|
||||
if len(bw.items) > 0 {
|
||||
@@ -613,6 +693,8 @@ func (a *Agent) collectAndSendAllMetrics(manager *collector.Manager) error {
|
||||
slog.Info("当前连接不可用,消息已写入缓存")
|
||||
} else if writer.sendErr != nil {
|
||||
slog.Warn("发送消息失败,已写入缓存", "error", writer.sendErr)
|
||||
// 连接写失败视为连接异常,返回错误触发重连
|
||||
return fmt.Errorf("连接发送失败: %w", writer.sendErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ func (w *outboundWriter) WriteJSON(v interface{}) error {
|
||||
}
|
||||
w.buffered = true
|
||||
w.sendErr = err
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/dushixiang/pika/pkg/agent"
|
||||
@@ -203,7 +204,23 @@ func NewServiceManager(cfg *config.Config) (*ServiceManager, error) {
|
||||
|
||||
// Install 安装服务
|
||||
func (m *ServiceManager) Install() error {
|
||||
return m.service.Install()
|
||||
// 先尝试直接安装
|
||||
err := m.service.Install()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只有当是"服务已存在"的错误时才清理重装
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
slog.Info("服务已存在,先清理旧服务再重新安装...")
|
||||
_ = m.service.Stop()
|
||||
_ = m.service.Uninstall()
|
||||
// 再次尝试安装
|
||||
return m.service.Install()
|
||||
}
|
||||
|
||||
// 其他错误直接返回
|
||||
return err
|
||||
}
|
||||
|
||||
// Uninstall 卸载服务
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface PrivateRouteProps {
|
||||
@@ -13,9 +12,9 @@ const PrivateRoute = ({ children }: PrivateRouteProps) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
|
||||
// 如果没有登录信息,重定向到登录页
|
||||
// 如果没有登录信息,返回空占位
|
||||
if (!token || !userInfo) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已登录,渲染子组件
|
||||
|
||||
@@ -90,7 +90,6 @@ const AdminLayout = () => {
|
||||
const userInfoStr = localStorage.getItem('userInfo');
|
||||
|
||||
if (!token || !userInfoStr) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ const sendRequest = async <T>(url: string, config: RequestConfig = {}): Promise<
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
window.location.href = '/login';
|
||||
throw new HttpError('未认证或认证已过期', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
|
||||
@@ -17,16 +17,16 @@ const CyberCard = ({className, children, animation, hover}: Props) => {
|
||||
)}>
|
||||
{/* 装饰性边框 - 仅在暗色模式下显示 */}
|
||||
<div
|
||||
className="hidden dark:block absolute top-0 left-0 w-2 h-2 border-t-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors"></div>
|
||||
className="hidden dark:block absolute top-0 left-0 w-2 h-2 border-t-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors duration-300"></div>
|
||||
<div
|
||||
className="hidden dark:block absolute top-0 right-0 w-2 h-2 border-t-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors"></div>
|
||||
className="hidden dark:block absolute top-0 right-0 w-2 h-2 border-t-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors duration-300"></div>
|
||||
<div
|
||||
className="hidden dark:block absolute bottom-0 left-0 w-2 h-2 border-b-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors"></div>
|
||||
className="hidden dark:block absolute bottom-0 left-0 w-2 h-2 border-b-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors duration-300"></div>
|
||||
<div
|
||||
className="hidden dark:block absolute bottom-0 right-0 w-2 h-2 border-b-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors"></div>
|
||||
className="hidden dark:block absolute bottom-0 right-0 w-2 h-2 border-b-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors duration-300"></div>
|
||||
{animation &&
|
||||
<div
|
||||
className="hidden dark:block absolute inset-0 bg-gradient-to-b from-transparent via-cyan-500/5 to-transparent -translate-y-full group-hover:translate-y-full transition-transform duration-1000 ease-in-out pointer-events-none"/>
|
||||
className="hidden dark:block absolute inset-0 bg-gradient-to-b from-transparent via-cyan-500/5 to-transparent opacity-0 group-hover:opacity-100 -translate-y-full group-hover:translate-y-full transition-[transform,opacity] duration-1000 ease-in-out pointer-events-none will-change-transform"/>
|
||||
}
|
||||
|
||||
<div className={cn("relative z-10 p-4", className)}>
|
||||
|
||||
Reference in new issue
Block a user