实现日志收集功能

This commit is contained in:
Alex Yang
2025-12-05 00:03:44 +08:00
parent 057a2ea9ee
commit 52155cb77c
13 changed files with 7811 additions and 139 deletions

View File

@@ -1,9 +1,8 @@
{
"server_url": "http://10.35.10.12:8080/api",
"id": "agent-fnos1",
"name": "fnos1",
"device_id": "agent-fnos1",
"token": "eea3ffc9b3bb6b2a9f2e5bf228a2c7db",
"id": "agent-1764858612504948034-3525156",
"name": "yunc",
"token": "84f0657f9075f63e3964aeb7e3a9e59b",
"interval": "10s",
"debug": true,
"api_port": 8081

7121
agent/agent.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import (
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -23,13 +24,12 @@ import (
// Config Agent配置
type Config struct {
ServerURL string `json:"server_url"`
ID string `json:"id"` // Agent唯一标识自动生成
Name string `json:"name"` // Agent显示名称
DeviceID string `json:"device_id"` // 向后兼容,保留
Token string `json:"token"` // 设备认证令牌
Interval string `json:"interval"` // 采集间隔
Debug bool `json:"debug"` // 调试模式
APIPort int `json:"api_port"` // API端口
ID string `json:"id"` // Agent唯一标识自动生成
Name string `json:"name"` // Agent显示名称
Token string `json:"token"` // 设备认证令牌
Interval string `json:"interval"` // 采集间隔
Debug bool `json:"debug"` // 调试模式
APIPort int `json:"api_port"` // API端口
}
// NetworkInterfaceMetrics 网卡监控指标
@@ -92,10 +92,9 @@ type Metrics struct {
RxRate uint64 `json:"rx_rate"` // 所有网卡实时接收速率总和 (bytes/s)
TxRate uint64 `json:"tx_rate"` // 所有网卡实时发送速率总和 (bytes/s)
// 设备信息字段
DeviceID string `json:"device_id"` // 设备ID
AgentID string `json:"agent_id"` // Agent唯一标识
Name string `json:"name"` // 设备名称
IP string `json:"ip"` // 设备IP地址
AgentID string `json:"agent_id"` // Agent唯一标识
Name string `json:"name"` // 设备名称
IP string `json:"ip"` // 设备IP地址
}
// 全局配置
@@ -183,10 +182,9 @@ func initConfig() {
// 默认配置
config = Config{
ServerURL: "http://localhost:8080/api",
ID: "", // 自动生成
Name: "", // 自动生成或从配置读取
DeviceID: "default", // 向后兼容,保留
Token: "", // 设备认证令牌,从配置或环境变量读取
ID: "", // 自动生成
Name: "", // 自动生成或从配置读取
Token: "", // 设备认证令牌,从配置或环境变量读取
Interval: "10s",
Debug: false, // 默认非调试模式
APIPort: 8081, // 默认API端口8081
@@ -217,9 +215,9 @@ func initConfig() {
// 打印配置信息
if config.Debug {
log.Printf("Agent ID: %s, Name: %s, DeviceID: %s, Debug: %v, API Port: %d", config.ID, config.Name, config.DeviceID, config.Debug, config.APIPort)
log.Printf("Agent ID: %s, Name: %s, Debug: %v, API Port: %d", config.ID, config.Name, config.Debug, config.APIPort)
} else {
log.Printf("Agent ID: %s, Name: %s, DeviceID: %s", config.ID, config.Name, config.DeviceID)
log.Printf("Agent ID: %s, Name: %s", config.ID, config.Name)
}
}
@@ -237,10 +235,6 @@ func loadFromEnv() {
config.Name = name
}
if deviceID := os.Getenv("AGENT_DEVICE_ID"); deviceID != "" {
config.DeviceID = deviceID
}
if token := os.Getenv("AGENT_TOKEN"); token != "" {
config.Token = token
}
@@ -356,10 +350,6 @@ func readConfigFile() {
config.Name = fileConfig.Name
}
if fileConfig.DeviceID != "" {
config.DeviceID = fileConfig.DeviceID
}
if fileConfig.Token != "" {
config.Token = fileConfig.Token
}
@@ -746,6 +736,116 @@ func collectDiskDetails() ([]DiskDetailMetrics, error) {
return diskDetails, nil
}
// 采集系统日志
func collectLogs() ([]LogEntry, error) {
// 日志文件路径
logFile := "/var/log/messages"
log.Printf("Attempting to collect logs from %s", logFile)
// 检查文件是否存在和权限
fileInfo, err := os.Stat(logFile)
if err != nil {
log.Printf("Failed to stat log file %s: %v", logFile, err)
return nil, fmt.Errorf("failed to stat log file %s: %w", logFile, err)
}
log.Printf("Log file %s exists, size: %d bytes", logFile, fileInfo.Size())
// 打开日志文件
file, err := os.Open(logFile)
if err != nil {
log.Printf("Failed to open log file %s: %v", logFile, err)
return nil, fmt.Errorf("failed to open log file %s: %w", logFile, err)
}
defer file.Close()
// 读取文件末尾内容
const maxReadSize = 1024 * 1024 // 最多读取1MB
readSize := maxReadSize
if fileInfo.Size() < int64(maxReadSize) {
readSize = int(fileInfo.Size())
}
log.Printf("Reading %d bytes from log file", readSize)
buf := make([]byte, readSize)
bytesRead, err := file.ReadAt(buf, fileInfo.Size()-int64(readSize))
if err != nil {
log.Printf("Failed to read log file: %v", err)
return nil, fmt.Errorf("failed to read log file: %w", err)
}
log.Printf("Successfully read %d bytes from log file", bytesRead)
// 分割日志行
lines := bytes.Split(buf[:bytesRead], []byte("\n"))
log.Printf("Found %d lines in log file", len(lines))
// 创建日志条目切片
logs := make([]LogEntry, 0, 50) // 最多保存50条日志
// 从后往前解析日志行
for i := len(lines) - 1; i >= 0 && len(logs) < 50; i-- {
line := bytes.TrimSpace(lines[i])
if len(line) == 0 {
continue
}
// 打印前5行日志行用于调试
if i >= len(lines)-5 {
log.Printf("Processing log line %d: %s", i, string(line))
}
// 使用字符串处理,更方便处理空格
lineStr := string(line)
// 使用strings.Fields分割日志行自动处理连续空格
fields := strings.Fields(lineStr)
log.Printf("Line %d fields: %v, length: %d", i, fields, len(fields))
if len(fields) < 6 {
log.Printf("Skipping line %d: not enough fields (%d) after splitting", i, len(fields))
continue
}
// 构建时间字符串:月份 日期 时间
timeStr := fmt.Sprintf("%s %s %s", fields[0], fields[1], fields[2])
log.Printf("Line %d timeStr: '%s'", i, timeStr)
// 解析时间
t, err := time.Parse("Jan 2 15:04:05", timeStr)
if err != nil {
log.Printf("Skipping line %d: failed to parse time '%s': %v", i, timeStr, err)
continue
}
// 设置当前年份
year, _, _ := time.Now().Date()
t = time.Date(year, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.Local)
// 寻找第一个冒号用于分割source和message
colonIndex := strings.Index(lineStr, ": ")
if colonIndex == -1 {
log.Printf("Skipping line %d: no colon found to split source and message", i)
continue
}
// 解析source和message
source := lineStr[:colonIndex]
message := lineStr[colonIndex+2:]
// 创建日志条目
logEntry := LogEntry{
Sequence: len(logs) + 1,
Source: source,
Time: t,
Message: message,
}
// 添加到日志切片(注意顺序,后面的日志先解析,所以需要插入到前面)
logs = append([]LogEntry{logEntry}, logs...)
}
log.Printf("Successfully collected %d logs", len(logs))
return logs, nil
}
func collectMetrics() (*Metrics, error) {
metrics := &Metrics{}
@@ -753,11 +853,6 @@ func collectMetrics() (*Metrics, error) {
metrics.Network = make(map[string]NetworkInterfaceMetrics)
// 设置设备信息
deviceID := config.DeviceID
if deviceID == "" {
deviceID = config.ID
}
metrics.DeviceID = deviceID
metrics.AgentID = config.ID
metrics.Name = config.Name
// 尝试获取本机IP地址
@@ -825,6 +920,15 @@ func collectMetrics() (*Metrics, error) {
metrics.RxRate = rxRate
metrics.TxRate = txRate
// 采集系统日志
logs, err := collectLogs()
if err != nil {
// 日志采集失败时使用空切片
log.Printf("Failed to collect logs: %v, using empty slice", err)
logs = make([]LogEntry, 0)
}
metrics.Logs = logs
return metrics, nil
}
@@ -849,12 +953,6 @@ func sendMetrics(metricsList []*Metrics) error {
// 设置请求头
req.Header.Set("Content-Type", "application/json")
// 使用DeviceID作为设备唯一标识与设备管理中的ID匹配
deviceID := config.DeviceID
if deviceID == "" {
deviceID = config.ID
}
req.Header.Set("X-Device-ID", deviceID)
// 设置Agent名称
req.Header.Set("X-Agent-Name", config.Name)
// 设置设备认证令牌
@@ -928,16 +1026,15 @@ func startHTTPServer() {
disk, _ := collectDisk()
status := map[string]interface{}{
"status": "running",
"agent_id": config.ID,
"name": config.Name,
"device_id": config.DeviceID,
"debug": config.Debug,
"interval": config.Interval,
"cpu": cpu,
"cpu_hz": cpuHz,
"memory": memory,
"disk": disk,
"status": "running",
"agent_id": config.ID,
"name": config.Name,
"debug": config.Debug,
"interval": config.Interval,
"cpu": cpu,
"cpu_hz": cpuHz,
"memory": memory,
"disk": disk,
}
w.Header().Set("Content-Type", "application/json")

Binary file not shown.

Binary file not shown.

View File

@@ -8,6 +8,7 @@ import (
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
@@ -37,12 +38,13 @@ func RegisterRoutes(r *gin.Engine) {
// 监控数据路由
metrics := api.Group("/metrics")
{
metrics.GET("/cpu", GetCPUMetrics)
metrics.GET("/memory", GetMemoryMetrics)
metrics.GET("/disk", GetDiskMetrics)
metrics.GET("/network", GetNetworkMetrics)
metrics.GET("/cpu", GetCPUMetrics) // 添加CPU信息查询端点
metrics.GET("/memory", GetMemoryMetrics) // 添加内存信息查询端点
metrics.GET("/disk", GetDiskMetrics) // 添加磁盘信息查询端点
metrics.GET("/network", GetNetworkMetrics) // 添加网络信息查询端点
metrics.GET("/processes", GetProcessMetrics) // 添加进程信息查询端点
metrics.GET("/disk_details", GetDiskDetails) // 添加磁盘详细信息查询端点
metrics.GET("/logs", GetLogs) // 添加系统日志查询端点
// 添加POST端点接收Agent发送的指标数据
metrics.POST("/", HandleMetricsPost)
}
@@ -98,6 +100,13 @@ type DiskDetailMetrics struct {
Description string `json:"description"` // 设备描述
}
// LogMetrics 系统日志指标
type LogMetrics struct {
Source string `json:"source"` // 日志来源
Time string `json:"time"` // 日志时间
Message string `json:"message"` // 日志内容
}
// NetworkInterfaceMetrics 网卡监控指标
type NetworkInterfaceMetrics struct {
BytesSent uint64 `json:"bytes_sent"` // 发送速率 (bytes/s)
@@ -106,6 +115,14 @@ type NetworkInterfaceMetrics struct {
RxBytes uint64 `json:"rx_bytes"` // 累计接收字节数
}
// LogEntry 系统日志条目
type LogEntry struct {
Sequence int `json:"sequence"` // 日志序号
Source string `json:"source"` // 来源
Time time.Time `json:"time"` // 发生时间
Message string `json:"message"` // 内容
}
// MetricsRequest 指标请求结构
type MetricsRequest struct {
CPU float64 `json:"cpu"`
@@ -115,6 +132,7 @@ type MetricsRequest struct {
DiskDetails []DiskDetailMetrics `json:"disk_details"` // 磁盘详细信息
Network map[string]NetworkInterfaceMetrics `json:"network"`
Processes []ProcessMetrics `json:"processes"` // 进程信息
Logs []LogEntry `json:"logs"` // 系统日志
RxTotal uint64 `json:"rx_total"` // 所有网卡累计接收字节数总和
TxTotal uint64 `json:"tx_total"` // 所有网卡累计发送字节数总和
RxRate uint64 `json:"rx_rate"` // 所有网卡实时接收速率总和 (bytes/s)
@@ -303,6 +321,14 @@ func HandleMetricsPost(c *gin.Context) {
}
}
// 写入日志数据
for _, logEntry := range req.Logs {
if err := globalStorage.WriteLogMetric(writeCtx, deviceID, logEntry.Sequence, logEntry.Source, logEntry.Time, logEntry.Message, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write log for device %s: %v", deviceID, err)
}
}
// 广播指标更新消息,只广播最后一个指标
if i == len(metricsList)-1 {
// 准备广播的磁盘使用率数据(兼容旧格式)
@@ -559,8 +585,8 @@ func GetNetworkMetrics(c *gin.Context) {
receivedPoints, err2 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_received", startTime, endTime)
// 查询发送和接收的累积总流量指标
txBytesPoints, err3 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_total_tx_bytes", startTime, endTime)
rxBytesPoints, err4 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_total_rx_bytes", startTime, endTime)
txBytesPoints, err3 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_tx_bytes", startTime, endTime)
rxBytesPoints, err4 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_rx_bytes", startTime, endTime)
// 处理错误
if err1 != nil {
@@ -918,8 +944,12 @@ func GetAllDevices(c *gin.Context) {
func GetProcessMetrics(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-1h")
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
sortBy := c.DefaultQuery("sort_by", "cpu")
sortOrder := c.DefaultQuery("sort_order", "desc")
// 查询数据
processes, err := globalStorage.QueryProcessMetrics(context.Background(), deviceID, startTime, endTime)
@@ -927,13 +957,23 @@ func GetProcessMetrics(c *gin.Context) {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query process metrics: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": []map[string]interface{}{},
"data": []ProcessMetrics{},
"page": page,
"limit": limit,
"total": 0,
"sort_by": sortBy,
"sort_order": sortOrder,
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": processes,
"data": processes,
"page": page,
"limit": limit,
"total": len(processes),
"sort_by": sortBy,
"sort_order": sortOrder,
})
}
@@ -941,7 +981,7 @@ func GetProcessMetrics(c *gin.Context) {
func GetDiskDetails(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-1h")
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
// 查询数据
@@ -950,7 +990,7 @@ func GetDiskDetails(c *gin.Context) {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query disk details: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": []map[string]interface{}{},
"data": []DiskDetailMetrics{},
})
return
}
@@ -959,3 +999,62 @@ func GetDiskDetails(c *gin.Context) {
"data": diskDetails,
})
}
// GetLogs 获取系统日志
func GetLogs(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
// 查询数据
logData, err := globalStorage.QueryLogMetrics(context.Background(), deviceID, startTime, endTime)
if err != nil {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query logs: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": []LogMetrics{},
})
return
}
// 转换为前端需要的格式
logs := make([]LogMetrics, 0, len(logData))
for _, log := range logData {
// 将time.Time转换为字符串
timeValue, ok := log["time"].(time.Time)
var timeStr string
if ok {
timeStr = timeValue.Format(time.RFC3339)
} else {
// 如果不是time.Time类型尝试转换
if timeStrVal, ok := log["time"].(string); ok {
timeStr = timeStrVal
} else {
timeStr = ""
}
}
// 获取其他字段
source := ""
if sourceVal, ok := log["source"].(string); ok {
source = sourceVal
}
message := ""
if messageVal, ok := log["message"].(string); ok {
message = messageVal
}
// 添加到结果列表
logs = append(logs, LogMetrics{
Source: source,
Time: timeStr,
Message: message,
})
}
c.JSON(http.StatusOK, gin.H{
"data": logs,
})
}

View File

@@ -228,6 +228,29 @@ func (s *Storage) WriteDiskDetailMetric(ctx context.Context, deviceID, diskDevic
return s.writeData(ctx, "disk_details", allTags, fields, deviceID, "disk_detail")
}
// WriteLogMetric 写入日志指标
func (s *Storage) WriteLogMetric(ctx context.Context, deviceID string, sequence int, source string, time time.Time, message string, tags map[string]string) error {
// 创建标签映射,合并原有标签和新标签
allTags := make(map[string]string)
// 复制原有标签
for k, v := range tags {
allTags[k] = v
}
// 添加设备ID标签
allTags["device_id"] = deviceID
// 添加日志来源标签
allTags["source"] = source
// 创建字段映射
fields := map[string]interface{}{
"sequence": sequence,
"message": message,
}
// 使用新的writeData方法
return s.writeData(ctx, "logs", allTags, fields, deviceID, "log")
}
// QueryMetrics 查询监控指标
func (s *Storage) QueryMetrics(ctx context.Context, deviceID, metricType, startTime, endTime string) ([]MetricPoint, error) {
queryAPI := s.client.QueryAPI(s.org)
@@ -526,6 +549,67 @@ func (s *Storage) QueryProcessMetrics(ctx context.Context, deviceID string, star
return processes, nil
}
// QueryLogMetrics 查询日志指标
func (s *Storage) QueryLogMetrics(ctx context.Context, deviceID string, startTime, endTime string) ([]map[string]interface{}, error) {
queryAPI := s.client.QueryAPI(s.org)
// 构建查询语句
query := `from(bucket: "` + s.bucket + `")
|> range(start: ` + startTime + `, stop: ` + endTime + `)
|> filter(fn: (r) => r["_measurement"] == "logs")`
// 如果指定了设备ID添加设备ID过滤
if deviceID != "" {
query += `
|> filter(fn: (r) => r["device_id"] == "` + deviceID + `")`
}
// 按时间倒序排列,获取最新的日志
query += `
|> sort(columns: ["_time"], desc: true)
|> limit(n: 100)` // 限制返回100条最新日志
// 执行查询
queryResult, err := queryAPI.Query(ctx, query)
if err != nil {
return nil, err
}
defer queryResult.Close()
// 存储日志数据
logs := make([]map[string]interface{}, 0)
// 处理查询结果
for queryResult.Next() {
if queryResult.TableChanged() {
// 表结构变化,跳过
continue
}
// 获取记录
record := queryResult.Record()
// 构建日志数据
logData := map[string]interface{}{
"time": record.Time(),
"device_id": record.ValueByKey("device_id"),
"source": record.ValueByKey("source"),
"sequence": record.ValueByKey("sequence"),
"message": record.ValueByKey("message"),
"agent_name": record.ValueByKey("agent_name"),
}
// 添加到日志列表
logs = append(logs, logData)
}
if queryResult.Err() != nil {
return nil, queryResult.Err()
}
return logs, nil
}
// QueryDiskDetails 查询磁盘详细信息
func (s *Storage) QueryDiskDetails(ctx context.Context, deviceID string, startTime, endTime string) ([]map[string]interface{}, error) {
queryAPI := s.client.QueryAPI(s.org)

View File

@@ -159,7 +159,7 @@ func main() {
// 启动服务器
addr := fmt.Sprintf(":%d", cfg.Server.Port)
log.Printf("Server starting on %s", addr)
log.Printf("Static files served from /root/monitor/frontend")
log.Printf("Static files served from ./static")
if err := r.Run(addr); err != nil {
log.Fatalf("Failed to start server: %v", err)
}

Binary file not shown.

View File

@@ -360,7 +360,7 @@
</div>
</div>
</div>
<br>
<!-- 进程信息展示 -->
<div id="processInfoContainer" class="mt-8">
<h3 class="text-lg font-semibold text-gray-900 mb-4">进程信息</h3>
@@ -478,8 +478,8 @@
</div>
</div>
<!-- 系统日志 图表 -->
<div id="logChartContainer" class="chart-container h-80 hidden">
<!-- 系统日志 信息 -->
<div id="logInfoContainer" class="mt-8 hidden">
<h3 class="text-lg font-semibold text-gray-900 mb-4">系统日志</h3>
<div class="overflow-x-auto">
<table id="logTable" class="min-w-full bg-white rounded-lg overflow-hidden shadow-md">
@@ -496,6 +496,8 @@
</tbody>
</table>
</div>
<!-- 日志信息分页容器 -->
<div id="logPaginationContainer" class="mt-4"></div>
</div>
<!-- 缩放控件已移动到各个图表容器内 -->

View File

@@ -7,6 +7,7 @@ let state = {
customStartTime: '',
customEndTime: '',
currentInterval: '3m', // 固定10分钟区间
currentDeviceID: '', // 当前选中的服务器ID
historyMetrics: {}, // 存储历史指标数据
autoRefreshEnabled: false, // 自动刷新开关状态,默认关闭
lastMetricsUpdate: null, // 记录上次指标更新时间
@@ -101,12 +102,22 @@ function updateAutoRefreshInterval() {
// 初始化自定义时间范围
function initCustomTimeRange() {
const now = new Date();
// 默认显示过去24小时
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// 默认显示过去1小时
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000);
// 直接使用ISO字符串包含完整的时区信息
state.customStartTime = twentyFourHoursAgo.toISOString();
state.customStartTime = oneHourAgo.toISOString();
state.customEndTime = now.toISOString();
// 更新日期选择器输入框的值
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 将ISO字符串转换为datetime-local格式YYYY-MM-DDTHH:MM
startTimeInput.value = oneHourAgo.toISOString().slice(0, 16);
endTimeInput.value = now.toISOString().slice(0, 16);
}
}
// 页面切换
@@ -125,9 +136,13 @@ function switchPage() {
if (hash === '#servers') {
showContent('serversContent');
loadAllServers();
// 清除当前设备ID避免在服务器列表页显示特定服务器的数据
state.currentDeviceID = '';
} else if (hash === '#devices') {
showContent('devicesContent');
loadDeviceManagementList();
// 清除当前设备ID
state.currentDeviceID = '';
} else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
showContent('serverMonitorContent');
@@ -137,6 +152,9 @@ function switchPage() {
deviceId = hash.split('/')[1];
}
// 直接设置当前设备ID确保loadMetrics能使用正确的设备ID
state.currentDeviceID = deviceId;
// 加载服务器信息
if (deviceId) {
loadServerInfo(deviceId);
@@ -146,6 +164,8 @@ function switchPage() {
} else {
showContent('homeContent');
loadHomeData();
// 清除当前设备ID
state.currentDeviceID = '';
}
}
@@ -733,7 +753,7 @@ function initDetailedCharts() {
},
zoom: {
wheel: {
enabled: true,
enabled: true
},
pinch: {
enabled: true
@@ -802,7 +822,7 @@ function initDetailedCharts() {
},
zoom: {
wheel: {
enabled: true,
enabled: true
},
pinch: {
enabled: true
@@ -872,7 +892,7 @@ function initDetailedCharts() {
},
zoom: {
wheel: {
enabled: true,
enabled: true
},
pinch: {
enabled: true
@@ -950,11 +970,10 @@ function initDetailedCharts() {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const rawValue = context.raw.y;
// 第二行显示实际值
return [
`${label}: ${value} MB`,
`实际值: ${rawValue} MB`
`实际值: ${value} MB`
];
}
}
@@ -1043,11 +1062,10 @@ function initDetailedCharts() {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const rawValue = context.raw.y;
// 第二行显示实际值
return [
`${label}: ${value} MB/s`,
`实际值: ${rawValue} MB/s`
`实际值: ${value} MB/s`
];
}
}
@@ -1196,7 +1214,18 @@ async function fetchMetric(metricType, aggregation = 'average') {
}
const data = await response.json();
return data.data;
// 确保返回的数据是数组格式
if (metricType === 'disk') {
// 磁盘数据可能是对象格式,需要特殊处理
return data.data || {};
} else if (metricType === 'network') {
// 网络数据可能是对象格式,需要特殊处理
return data.data || {};
} else {
// 其他数据应该是数组格式
return Array.isArray(data.data) ? data.data : [];
}
}
// 格式化时间,统一格式避免图表误解
@@ -1259,6 +1288,9 @@ async function loadMetrics() {
statusIndicator.className = 'w-2 h-2 bg-red-500 rounded-full animate-pulse';
lastRefreshTime.textContent = `上次刷新: 失败`;
}
// 即使发生错误,也要尝试初始化图表,避免页面空白
initDetailedCharts();
}
}
@@ -1450,6 +1482,9 @@ function handleWebSocketMessage(message) {
// 加载服务器信息
async function loadServerInfo(deviceId) {
try {
// 将设备ID存储到全局状态
state.currentDeviceID = deviceId;
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!response.ok) {
throw new Error('Failed to fetch server info');
@@ -1464,6 +1499,9 @@ async function loadServerInfo(deviceId) {
}
} catch (error) {
console.error('Failed to load server info:', error);
// 即使请求失败也要将设备ID存储到全局状态
state.currentDeviceID = deviceId;
// 使用模拟数据
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
@@ -1475,6 +1513,12 @@ async function loadServerInfo(deviceId) {
// 处理指标更新
function handleMetricsUpdate(message) {
const { device_id, metrics } = message;
// 只处理当前选中设备的WebSocket消息
if (state.currentDeviceID && device_id !== state.currentDeviceID) {
return;
}
// 格式化数据确保updateStatusCards函数能正确处理
const formattedMetrics = {
cpu: metrics.cpu,
@@ -1855,12 +1899,18 @@ function updateHistoryMetrics(metrics) {
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkData) {
// 确保数据格式正确
const safeCpuData = Array.isArray(cpuData) ? cpuData : [];
const safeMemoryData = Array.isArray(memoryData) ? memoryData : [];
const safeDiskData = typeof diskData === 'object' && diskData !== null ? diskData : {};
const safeNetworkData = typeof networkData === 'object' && networkData !== null ? networkData : {};
// 保存待处理的图表更新数据
state.pendingChartUpdate = {
cpuData,
memoryData,
diskData,
networkData
cpuData: safeCpuData,
memoryData: safeMemoryData,
diskData: safeDiskData,
networkData: safeNetworkData
};
// 使用节流机制更新图表
@@ -2619,18 +2669,7 @@ function bindEvents() {
});
}
// 初始化自定义时间输入框
const now = new Date();
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 默认显示过去24小时
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// 显示本地时间格式YYYY-MM-DDTHH:MM
startTimeInput.value = twentyFourHoursAgo.toISOString().slice(0, 16);
endTimeInput.value = now.toISOString().slice(0, 16);
}
// 缩放控件事件处理
const zoomOutBtn = document.getElementById('zoomOutBtn');
@@ -2722,27 +2761,49 @@ async function loadNetworkInterfaces() {
// 更新当前时间范围显示
const updateTimeRangeDisplay = () => {
let displayText = '';
switch(state.currentTimeRange) {
case '30m':
displayText = '过去30分钟';
break;
case '1h':
displayText = '过去1小时';
break;
case '2h':
displayText = '过去2小时';
break;
case '6h':
displayText = '过去6小时';
break;
case '12h':
displayText = '过去12小时';
break;
case '24h':
displayText = '过去24小时';
break;
default:
displayText = '自定义时间范围';
// 计算实际的开始时间和结束时间
let startTime, endTime;
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间范围
startTime = new Date(state.customStartTime);
endTime = new Date(state.customEndTime);
displayText = `${formatTime(state.customStartTime)}${formatTime(state.customEndTime)}`;
} else {
// 使用预设时间范围
const now = new Date();
endTime = now;
// 根据预设时间范围计算开始时间
switch(state.currentTimeRange) {
case '30m':
startTime = new Date(now.getTime() - 30 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '1h':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '2h':
startTime = new Date(now.getTime() - 2 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '6h':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '12h':
startTime = new Date(now.getTime() - 12 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '24h':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
default:
displayText = '自定义时间范围';
}
}
// 更新所有图表的时间范围显示
@@ -2897,11 +2958,15 @@ function initChartTabs() {
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
console.log(`Shown chart container: ${tabId}ChartContainer`);
} else {
console.error(`Chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息磁盘详细信息
// 显示/隐藏进程信息磁盘详细信息和系统日志
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
@@ -2910,6 +2975,9 @@ function initChartTabs() {
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
@@ -2926,6 +2994,13 @@ function initChartTabs() {
// 加载磁盘详细信息
loadDiskDetails();
}
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
// 加载系统日志
loadSystemLogs();
}
}
// 显示/隐藏网卡选择下拉框
@@ -2941,15 +3016,64 @@ function initChartTabs() {
});
});
// 初始状态:根据当前选中的选项卡显示/隐藏网卡选择下拉框
// 初始状态:根据当前选中的选项卡显示/隐藏网卡选择下拉框和加载数据
const activeTab = document.querySelector('.chart-tab.active');
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (activeTab && interfaceContainer) {
if (activeTab) {
const tabId = activeTab.dataset.tab;
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
// 显示当前选中的图表容器
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息和系统日志
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
loadSystemLogs();
}
// 显示/隐藏网卡选择下拉框
if (interfaceContainer) {
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
}
}
@@ -2974,7 +3098,18 @@ let processPagination = {
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allProcesses: []
allProcesses: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 系统日志分页状态
let logPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allLogs: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 加载进程信息
@@ -2991,7 +3126,16 @@ async function loadProcessInfo(page = 1) {
params.append('device_id', state.currentDeviceID);
}
// 如果是第一次加载,获取全部数据
// 检查设备ID是否变化
if (processPagination.lastDeviceID !== state.currentDeviceID) {
// 设备ID变化清空旧数据
processPagination.allProcesses = [];
processPagination.totalItems = 0;
processPagination.totalPages = 0;
processPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID变化获取全部数据
if (processPagination.allProcesses.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/processes?${params.toString()}`);
@@ -3003,6 +3147,8 @@ async function loadProcessInfo(page = 1) {
processPagination.allProcesses = data.data || [];
processPagination.totalItems = processPagination.allProcesses.length;
processPagination.totalPages = Math.ceil(processPagination.totalItems / processPagination.itemsPerPage);
// 更新上次请求数据的设备ID
processPagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
@@ -3072,7 +3218,8 @@ let diskPagination = {
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allDisks: []
allDisks: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 创建分页控件
@@ -3221,7 +3368,16 @@ async function loadDiskDetails(page = 1) {
params.append('device_id', state.currentDeviceID);
}
// 如果是第一次加载,获取全部数据
// 检查设备ID是否变化
if (diskPagination.lastDeviceID !== state.currentDeviceID) {
// 设备ID变化清空旧数据
diskPagination.allDisks = [];
diskPagination.totalItems = 0;
diskPagination.totalPages = 0;
diskPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID变化获取全部数据
if (diskPagination.allDisks.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/disk_details?${params.toString()}`);
@@ -3233,6 +3389,8 @@ async function loadDiskDetails(page = 1) {
diskPagination.allDisks = data.data || [];
diskPagination.totalItems = diskPagination.allDisks.length;
diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage);
// 更新上次请求数据的设备ID
diskPagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
@@ -3319,9 +3477,15 @@ async function loadDiskDetails(page = 1) {
}
// 加载系统日志
async function loadSystemLogs() {
async function loadSystemLogs(page = 1) {
console.log('loadSystemLogs function called');
const logTableBody = document.getElementById('logTableBody');
if (!logTableBody) return;
const logPaginationContainer = document.getElementById('logPaginationContainer');
if (!logTableBody) {
console.error('logTableBody element not found');
return;
}
try {
// 构建查询参数
@@ -3330,46 +3494,86 @@ async function loadSystemLogs() {
params.append('device_id', state.currentDeviceID);
}
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/logs?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch system logs');
// 检查设备ID是否变化
if (logPagination.lastDeviceID !== state.currentDeviceID) {
// 设备ID变化清空旧数据
logPagination.allLogs = [];
logPagination.totalItems = 0;
logPagination.totalPages = 0;
logPagination.currentPage = 1;
}
const data = await response.json();
const logs = data.data;
// 如果是第一次加载或设备ID变化获取全部数据
if (logPagination.allLogs.length === 0) {
console.log('Fetching logs from:', `${API_BASE_URL}/metrics/logs?${params.toString()}`);
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/logs?${params.toString()}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
logPagination.allLogs = data.data || [];
logPagination.totalItems = logPagination.allLogs.length;
logPagination.totalPages = Math.ceil(logPagination.totalItems / logPagination.itemsPerPage);
// 更新上次请求数据的设备ID
logPagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
logPagination.currentPage = page;
// 清空表格
logTableBody.innerHTML = '';
if (logs.length === 0) {
if (logPagination.totalItems === 0) {
// 没有日志数据,显示提示
logTableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500">暂无系统日志</td>
</tr>
`;
// 隐藏分页控件
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (logPagination.currentPage - 1) * logPagination.itemsPerPage;
const endIndex = Math.min(startIndex + logPagination.itemsPerPage, logPagination.totalItems);
const paginatedLogs = logPagination.allLogs.slice(startIndex, endIndex);
// 填充表格数据
logs.forEach((log, index) => {
paginatedLogs.forEach((log, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${index + 1}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${startIndex + index + 1}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${log.source || 'System'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${log.time || new Date().toLocaleString()}</td>
<td class="px-6 py-4 whitespace-normal text-sm text-gray-500">${log.message || 'No message'}</td>
`;
logTableBody.appendChild(row);
});
// 创建分页控件
if (logPaginationContainer) {
createPaginationControls(logPaginationContainer, logPagination, loadSystemLogs);
}
} catch (error) {
console.error('Error loading system logs:', error);
logTableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载系统日志失败</td>
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载系统日志失败: ${error.message}</td>
</tr>
`;
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
}
}
@@ -3379,11 +3583,56 @@ function handleHashChange() {
if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
// 延迟一下确保DOM已经渲染完成
setTimeout(() => {
loadMetrics();
// 加载进程信息如果当前是CPU选项卡
// 加载当前选项卡对应的数据
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab && activeTab.dataset.tab === 'cpu') {
loadProcessInfo();
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示/隐藏附加信息容器
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
// 加载系统日志
loadSystemLogs();
} else {
// 加载其他监控数据
loadMetrics();
// 根据选项卡加载附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
}
}
} else {
// 如果没有找到激活的选项卡默认加载metrics
loadMetrics();
}
}, 300);
}

10
test_agent.json Normal file
View File

@@ -0,0 +1,10 @@
{
"server_url": "http://localhost:8080/api",
"id": "test-agent",
"name": "Test Agent",
"device_id": "test-device",
"token": "bb30bfaee01bf7b541bbefe422f72645",
"interval": "5s",
"debug": true,
"api_port": 8081
}

11
test_metrics.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# 测试发送单个指标对象
echo "测试发送单个指标对象:"
curl -v -X POST -H "Content-Type: application/json" -H "X-Device-ID: test-device" -H "X-Agent-Name: test-agent" -H "X-Device-Token: test-token" -d '{"cpu": 50.5, "memory": 30.2, "disk": {":/": 45.6}, "network": {"eth0": {"bytes_sent": 1000, "bytes_received": 2000}}}' http://localhost:8080/api/metrics/
echo "\n---\n"
# 测试发送指标数组
echo "测试发送指标数组:"
curl -v -X POST -H "Content-Type: application/json" -H "X-Device-ID: test-device" -H "X-Agent-Name: test-agent" -H "X-Device-Token: test-token" -d '[{"cpu": 50.5, "memory": 30.2, "disk": {":/": 45.6}, "network": {"eth0": {"bytes_sent": 1000, "bytes_received": 2000}}}, {"cpu": 60.5, "memory": 40.2, "disk": {":/": 55.6}, "network": {"eth0": {"bytes_sent": 2000, "bytes_received": 3000}}}]' http://localhost:8080/api/metrics/