SysMind:Go 语言驱动的AI系统运维助手

发布于:2025-07-23 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言

作为一名正在运维岗位求职赛道上的应届实习生,最近总在深夜复盘技术面试时的细节 —— 当面试官抛出 “如何用技术手段提升运维效率” 的问题时,我发现自己列举的监控工具、自动化脚本等答案,始终停留在 “使用工具” 的层面。或许正是这种 “知其然不知其所以然” 的焦虑,让我开始认真审视系统运维这个领域。

系统运维远不止部署服务、排查故障这些表层动作。在实际工作中,它更像在数据海洋中搭建灯塔:既要实时捕捉服务器的 CPU 波动、内存占用等基础体征,又要通过历史数据预测潜在风险,甚至在故障发生时能自动定位根因。这种 “感知 - 分析 - 决策” 的闭环能力,恰恰是当前企业对运维工程师的核心诉求。

回想前段时间自己写的系统监控信息陈列平台,那个基于 Gin 框架和 Gopsutil 开发的监控系统

Gitee 上的https://gitee.com/Triyaotu/triyaotu

虽然实现了基础的指标展示,但总觉得离 “智能助手” 还有段距离。它更像个数据陈列柜,而我想要的是能主动对话的 “运维助手”。

于是一个大胆的想法冒了出来:如果从 0 开始重构,把它拆分成真正的微服务架构,让每个模块都具备数据采集、分析推理、指令执行的能力,会不会诞生一个能听懂人话的运维助手?这个念头像颗种子,在连续几周修改简历的间隙里疯狂生长 —— 毕竟对于自学 Go 的个人开发者来说,没有什么比亲手造轮子更能检验技术成色的了。

想了半天,给它一个名字吧:SysMind

程序简介

先来说说这个被改造的 “前辈”——MonitorXY。它是一个可以在 web 端查看系统状态信息的平台,界面清晰地展示着服务器的各项关键数据,像 CPU 使用率的实时曲线、内存占用的具体数值、磁盘 IO 的波动情况等,一目了然。当时开发它的时候,主要就是想用 Gin 框架搭起 web 服务的骨架,再借助 Gopsutil 这个强大的标准库,从系统中提取出各种有用的信息,然后通过前端展示出来,让用户能直观地了解系统的运行状态。

img

要是你对这个 “前辈” 感兴趣,想亲手体验一下,很适合自己学完Go语言作为一个练手的小项目,前端文件已经给出来了直接用就行,随时可以去 Gitee 上看看。Gitee 地址是:MonitorXY: V1.n 版本的 MonitorXY。,说不定你能从中发现一些有趣的东西,或者给我提出一些改进的建议呢。

现在,我把它改造成了一个小型微服务。这个微服务在提取监控信息的原理上,还是沿用了 Gopsutil 标准库,通过它来获取文件系统的各种信息。之所以选择微服务的架构,是因为我觉得这样能让整个系统的各个功能模块更加独立,方便后续的扩展和维护。比如,数据采集模块和数据分析模块可以分开部署,当其中一个模块需要升级或者出现问题时,不会对其他模块造成太大的影响。

整个改造过程我分成了好几个阶段来实现,每个阶段都有它的重点和需要解决的问题。在这个过程中,我不断地思考如何让各个模块之间更好地协作,如何提高数据传输的效率,如何确保系统的稳定性。毕竟我是个人开发者,而且还是自学的 Go,在这个过程中肯定会遇到不少困难和挑战。

经过一番努力,这个系统最终可以通过邮件系统将 AI 分析的结果发送给用户,让用户能够及时了解系统的运行状况。想象一下,当系统监测到异常情况时,它会自动进行分析,找出可能的原因,然后把这些信息整理成清晰易懂的邮件内容,发送到你的邮箱里。你不需要时刻守在电脑前盯着监控界面,只需要查看邮件,就能知道系统出了什么问题,大概是什么原因导致的,这无疑会大大提高运维的效率。

比如,当 CPU 使用率长时间过高时,邮件里会详细说明这段时间内 CPU 的变化趋势,列出可能占用大量 CPU 资源的进程,甚至会给出一些初步的解决建议。这样的结果呈现方式,既直观又实用,让运维工作变得更加轻松便捷。

接下来,就让我们一起看看具体的实现思路吧。要是在格式、思路或者其他方面有什么问题,欢迎大家随时指出来,我一定会认真听取并加以改进的。

最终通过邮件系统就可以达到下面这种程度!
以下为AI分析的结果,并发送至邮箱里进行查看的。)

在这里插入图片描述

一. 信息输出

微服务改造的第一步,就卡在了最基础的信息输出环节。

最初为前端展示设计的代码,采用结构体直接返回数据的方式 —— 就像这段获取 CPU 信息的GetCpuInfo函数,它会把逻辑核心数、进程负载等数据打包成CPUInfo结构体返回。这种方式在前后端交互时很高效,但当我决定将这些数据作为 AI 分析的原始素材时,就暴露出明显的缺陷:结构体数据无法直接写入文件形成可读的原始记录,更谈不上让 AI 理解数据间的关联关系:

package datainfo

/**
 * @Description
 * @Author 是垚不是土
 * @Date 2025/6/8 13:02
 **/
import (
	"fmt"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/process"
	"sort"
	"time"
)

func GetCpuInfo() (*CPUInfo, error) {
	logicalCores, err := cpu.Counts(true)
	if err != nil {
		return nil, err
	}
	physicalCores, err := cpu.Counts(false)
	if err != nil {
		return nil, err
	}

	//获取CPU使用率
	cpuPrecent, err := cpu.Percent(time.Second, false)
	if err != nil {
		return nil, err
	}

	//获取进程信息
	processes, err := process.Processes()
	if err != nil {
		return nil, err
	}

	//进程排序
	sort.Slice(processes, func(i, j int) bool {
		cpuI, _ := processes[i].CPUPercent()
		cpuJ, _ := processes[j].CPUPercent()
		return cpuI > cpuJ
	})

	//获取前五个进程
	topProcesses := make([]ProcessInfo, 0, 5)
	for i := 0; i < 5 && i < len(processes); i++ {
		p := processes[i]
		name, _ := p.Name()
		cpuPrec, _ := p.CPUPercent()
		memPrec, _ := p.MemoryPercent()

		topProcesses = append(topProcesses, ProcessInfo{
			PID:        p.Pid,
			Name:       name,
			CPUPercent: cpuPrec,
			MemPercent: memPrec,
		})
	}
	return &CPUInfo{
		LogicalCores:  logicalCores,
		PhysicalCores: physicalCores,
		UsagePrecent:  cpuPrecent[0],
		TopProcesses:  topProcesses,
	}, nil
}

于是我做的第一个关键改造,就是为每类监控信息设计标准化的输出格式。

以 CPU 信息为例,新增的FormatCPUInfo函数承担了 “数据翻译” 的角色:它先调用GetCpuInfo获取原始数据,再通过格式化字符串将零散的字段重组为结构化文本。

func FormatCPUInfo() string {
	cpuInfo, err := GetCpuInfo()
	if err != nil {
		return fmt.Sprintf("CPU信息采集失败: %v", err)
	}

	// 结构化输出
	result := "========== CPU状态 ==========\n"
	result += fmt.Sprintf("逻辑核心: %d\n", cpuInfo.LogicalCores)
	result += fmt.Sprintf("物理核心: %d\n", cpuInfo.PhysicalCores)
	result += fmt.Sprintf("使用率: %.2f%%\n", cpuInfo.UsagePrecent)
	result += "高负载进程:\n"
	for i, p := range cpuInfo.TopProcesses {
		result += fmt.Sprintf("  %d. PID=%d, 进程名=%s, CPU=%.2f%%, 内存=%.2f%%\n",
			i+1, p.PID, p.Name, p.CPUPercent, p.MemPercent)
	}
	return result
}

这种格式化输出有三个明显优势:

首先是可读性提升

通过========== CPU状态 ==========

这样的分隔符和清晰的字段命名,让原本藏在结构体里的数字变成了人类可直接阅读的报告。比如 “逻辑核心: 8”、“使用率: 23.56%” 这类表述,既保留了数据精度,又降低了信息获取成本。

其次是存储标准化。所有格式化后的信息都会按统一规则写入原始文件,这为后续 AI 模块的文本解析铺平了道路。想象一下,如果 CPU 信息用表格排版,内存信息用 JSON 字符串,AI 在处理时就需要不断切换解析逻辑;而现在统一的文本格式,相当于给所有数据贴上了相同规格的标签。

最后是扩展性增强。当需要新增监控维度时,只需在格式化函数中添加对应的输出行,无需调整文件存储逻辑。比如后续要加入 CPU 温度监测,只需在FormatCPUInfo里增加一行温度数据的格式化代码即可。

我们一起看看这样之后输出的文件长什么样子:

========== CPU状态 ==========
逻辑核心: 4
物理核心: 4
使用率: 20.83%
高负载进程:
  1. PID=43998, 进程名=remote-dev-server, CPU=70.16%, 内存=12.02%
  2. PID=47485, 进程名=___go_build_main_go__1_, CPU=29.63%, 内存=0.06%
  3. PID=43121, 进程名=sshd, CPU=0.56%, 内存=0.04%
  4. PID=728, 进程名=vmtoolsd, CPU=0.21%, 内存=0.03%
  5. PID=42920, 进程名=sshd, CPU=0.16%, 内存=0.04%

这种改造看似只是给数据加了层 “包装”,实则是完成了从 “机器间通信格式” 到 “人机协同格式” 的转变。这一步走扎实了,后续的 AI 分析模块才能站在规范的数据基础上开展工作。

二. 持久化存储

如果说信息输出的格式化是给数据穿上了统一的 “外衣”,那持久化存储就是为这些数据建造了坚固的 “仓库”。

对于系统运维而言,实时的监控数据稍纵即逝,只有将其妥善保存,才能形成可供追溯的系统日志,也才能为 AI 分析提供足够丰富的历史样本。这个看似简单的 “存文件” 动作,实则是连接实时采集与深度分析的关键枢纽。

请看这一部分的代码:

func CreateTimeStampedFile() (*os.File, error) {
	logDir := "logs"
	filename := time.Now().Format("2006_0102_15:04") + ".log"
	path := filepath.Join(logDir, filename)

	// 确保目录存在
	if err := os.MkdirAll(logDir, 0755); err != nil {
		return nil, err
	}
	return os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
}

func CreateFile() {
	file, err := CreateTimeStampedFile()
	if err != nil {
		fmt.Println("日志文件创建失败:", err)
		return
	}
	defer file.Close()

	//调用各模块的输出函数,写入文件
	_, _ = file.WriteString(datainfo.FormatHostInfo() + "\n")
	_, _ = file.WriteString(datainfo.FormatCPUInfo() + "\n")
	_, _ = file.WriteString(datainfo.FormatMemInfo() + "\n")
	_, _ = file.WriteString(datainfo.FormatDISKInfo() + "\n")
	_, _ = file.WriteString(datainfo.FormatNetInfo() + "\n")
	// 提示用户
}

从代码实现来看,整个持久化过程分为三个精密咬合的环节:

  • 目录自动管理:在CreateTimeStampedFile函数中,通过os.MkdirAll(logDir, 0755)实现了 “有则复用,无则创建” 的智能逻辑。这里的 0755 权限设置很讲究 —— 既保证了服务进程对日志目录的读写权限,又限制了其他用户的访问,在安全性和可用性之间找到了平衡。这种设计让程序在新环境部署时无需人工预建目录,极大降低了运维成本。

  • 文件名的时间编码:time.Now().Format(“2006_0102_15:04”) + ".log"生成的文件名,采用 “年月日_时分” 的格式,比如 “2025_0722_14:30.log”。这种命名方式有两个妙用:一是天然实现了日志文件的时间排序,后续 AI 在分析时能快速定位某个时间段的数据;二是通过时间粒度控制文件大小,避免单个日志文件过大导致的解析效率下降。

  • 数据写入的模块化设计:CreateFile函数中,通过依次调用datainfo包下的FormatHostInfo、FormatCPUInfo等格式化函数,将主机、CPU、内存等不同维度的信息按统一格式写入文件。这种 “模块化写入” 的方式,既保证了日志内容的完整性,又让后续新增监控维度时(比如添加数据库监控),只需在写入列表中增加对应函数即可,无需修改核心存储逻辑。

特别值得注意的是文件打开模式os.O_CREATE|os.O_WRONLY|os.O_APPEND的组合:

  • O_CREATE确保文件不存在时自动创建

  • O_WRONLY保证写入操作的安全性

  • O_APPEND则实现了追加写入 —— 这意味着同一时间段内的监控数据会累积到同一文件中,形成连续的系统状态记录。

这种设计让日志文件既保持了时间连续性,又通过定时创建新文件避免了无限膨胀。

这些细节共同构成了可靠的存储层:当 AI 模块需要分析系统趋势时,能通过文件名快速定位目标时段的日志;当运维人员排查历史故障时,可直接翻阅对应时间的完整记录。这样的持久化存储,让每一次状态变化都有迹可循。

我们一起来看一下运行之后的效果:

在这里插入图片描述

从第一张图片可以清晰地看到,程序运行后,在项目根目录下自动生成了一个名为logs的文件夹,打开这个文件夹,里面躺着一个以时间戳命名的.log文件 —— 这正是CreateTimeStampedFile函数工作的直接成果。文件名严格遵循了 “年月日_时分” 的格式,完美呼应了代码中对文件名的设计逻辑。

(-AI.md文件你一会儿就知道他是干嘛的了,嘿嘿)

在这里插入图片描述

再看第二张图片里的日志文件内容:

文件中清晰地划分出不同的系统信息模块,每个模块下的内容都条理分明。以 CPU 状态为例,不仅列出了逻辑核心数、物理核心数、使用率等关键数据,还按顺序排列出高负载进程的 PID、进程名、CPU 及内存占用情况,和我们在FormatCPUInfo函数中定义的输出格式完全一致。

这种内容呈现方式,使得即便是非技术人员,也能快速从中获取关键信息。而对于后续的 AI 分析模块来说,这样规整的文本内容,就如同精心整理好的原材料,能大大降低其解析数据的难度,为高效分析打下坚实基础。同时,这些记录也成为了系统运行的 “快照”,当需要回溯某个时间点的系统状态时,只需找到对应的日志文件,就能清晰地了解当时的系统全貌。

三. 引入AI分析

有了标准化的日志文件后,就该让 AI 登场了。这一步的核心是让机器读懂系统日志,并用专业运维视角给出分析报告。我选择接入 DeepSeek API 实现这一功能(换成 OpenAI 只需修改接口地址和请求格式)。

// DeepSeekRequest 官方请求结构
type DeepSeekRequest struct {
	Model    string `json:"model"`
	Messages []struct {
		Role    string `json:"role"`
		Content string `json:"content"`
	} `json:"messages"`
	Temperature float32 `json:"temperature,omitempty"`
	MaxTokens   int     `json:"max_tokens,omitempty"`
}

DeepSeekRequest结构体是与 AI 沟通的「语言转换器」。严格遵循 API 要求的 JSON 格式,通过Model字段指定使用的模型版本,Messages数组则模拟人类对话 —— 先由系统角色设定 AI 的专业身份,再由用户角色传递日志内容。这种设计既符合大模型的交互规范,又能通过角色分离确保提示词的稳定性。

// 日志指纹生成
func GenerateLogFingerprint(content string) string {
	hasher := md5.New()
	hasher.Write([]byte(content))
	return hex.EncodeToString(hasher.Sum(nil))[:8]
}

const (
	ApiKey    = "Token"
	ApiUrl    = "https://api.deepseek.com/v1/chat/completions"
	ModelName = "deepseek-chat"
	LogDir    = "logs"
)

在与 AI 交互前,有两个基础工作必须做:

日志指纹的生成机制暗藏巧思。GenerateLogFingerprint函数通过 MD5 哈希算法对日志内容进行摘要计算,并截取前 8 位作为唯一标识。这个设计有两个实际价值:一是快速判断两份日志是否存在重复内容,减少无效分析;二是为后续建立 “日志 - 分析结果” 的关联索引埋下伏笔。当系统积累足够多的日志后,可通过指纹比对快速定位相似故障的历史解决方案。

常量定义的模块化则体现了配置分离的思想。ApiKey、ApiUrl等核心参数被集中定义为常量,既方便后续修改(比如更换 API 版本时只需改ApiUrl),又能通过环境变量注入的方式避免密钥硬编码 —— 在实际部署时,只需将ApiKey改为从环境变量读取,就能显著提升安全性。(当然,后续为了安全性可以使用环境变量)

AI 分析的核心逻辑集中在AnalyzeLogWithAI函数中,这个函数实现了从 “喂数据” 到 “拿结果” 的完整闭环:

func AnalyzeLogWithAI(logContent string) (string, error) {
	logFingerprint := GenerateLogFingerprint(logContent)

	// AI深度分析提示词
	systemPrompt := `作为15年经验的Linux系统运维工程师和架构师,请执行深度根因分析:
### 分析维度
1. 性能瓶颈:分析高负载进程的线程级资源争用(特别是remote-dev-server)
2. 安全隐患:关联SSH会话与登录日志溯源异常IP
3. 存储规划:基于历史增长速率预测资源耗尽时间(特别是/boot分区)
4. 配置缺陷:验证网络接口合规性(如未配置IP却收包的ens34)
5. 资源闲置:提出内存/CPU闲置资源的转化方案

### 输出规范,以下是你回答的参考,而不是必须执行相关命令,只是给你个回答的参考样式

**结构化报告格式(Markdown)**
#### 1. 核心问题诊断
| 风险等级 | 问题类型    | 根因定位                | 关联证据         | 紧急程度 |
|----------|-------------|-------------------------|------------------|----------|
| 高危     | 进程失控    | remote-dev-server内存泄漏 | CPU 25.49%       | ⚠️⚠️     |

#### 2. 可执行方案

bash

检查OOM日志(需root权限)

grep -i "oom" /var/log/kern.log | grep {{PID}}

#### 3. 数学预测模型
math

/boot分区耗尽时间 = (当前可用空间) / (日均增长量) ≈ 剩余天数

#### 4. 监控增强建议
- 部署Prometheus exporter采集steal值
- 当/boot >85%时触发告警规则
`

	// 构造官方标准请求体
	requestBody := DeepSeekRequest{
		Model: ModelName,
		Messages: []struct {
			Role    string `json:"role"`
			Content string `json:"content"`
		}{
			{
				Role:    "system",
				Content: systemPrompt,
			},
			{
				Role: "user",
				Content: fmt.Sprintf(
					"日志指纹:%s\n内核版本:%s\n关键进程:%s\n日志内容:\n%s",
					logFingerprint,
					"3.10.0-1160.el7", // 从实际系统获取
					"remote-dev-server,sshd",
					logContent,
				),
			},
		},
		Temperature: 0.2,
		MaxTokens:   8000,
	}

	jsonBody, _ := json.Marshal(requestBody)

	// 发送带完整头的请求
	req, _ := http.NewRequest("POST", ApiUrl, bytes.NewBuffer(jsonBody))
	req.Header.Set("Authorization", "Bearer "+ApiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("网络请求失败: %w", err)
	}
	defer resp.Body.Close()

	// 增强错误解析(捕获API详细错误)[1](@ref)
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		var apiError struct {
			Error struct {
				Message string `json:"message"`
			} `json:"error"`
		}
		if err := json.Unmarshal(body, &apiError); err == nil && apiError.Error.Message != "" {
			return "", fmt.Errorf("API错误 %d: %s", resp.StatusCode, apiError.Error.Message)
		}
		return "", fmt.Errorf("API错误 %d: %s", resp.StatusCode, string(body))
	}

	// 解析响应体
	var response struct {
		Choices []struct {
			Message struct {
				Content string `json:"content"`
			} `json:"message"`
		} `json:"choices"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
		return "", fmt.Errorf("响应解析失败: %w", err)
	}
	if len(response.Choices) == 0 {
		return "", fmt.Errorf("API返回空分析结果")
	}
	analysis := response.Choices[0].Message.Content

	// 写入AI分析文件
	timestamp := time.Now().Format("20060102_15:04")
	aiFilename := fmt.Sprintf("%s-AI.md", timestamp) // 改为Markdown格式
	aiPath := filepath.Join(LogDir, aiFilename)

	if err := os.WriteFile(aiPath, []byte(analysis), 0644); err != nil {
		return "", fmt.Errorf("文件写入失败: %w", err)
	}

	return aiPath, nil
}

提示词工程的设计

系统提示词首先为 AI 设定 “15 年经验的 Linux 系统运维工程师” 的角色,然后通过五个分析维度建立思考框架 —— 从性能瓶颈到资源闲置,覆盖了运维工作的核心场景。

特别值得注意的是对remote-dev-server进程、/boot分区等具体对象的点名关注,这相当于给 AI 划定了分析重点,避免其泛泛而谈。

输出规范的强制约束

通过提供 Markdown 表格、Bash 命令示例、数学公式模板等具体样例,让 AI 的输出天然具备可操作性。

比如 “核心问题诊断” 表格中,风险等级与紧急程度的对应关系,直接为后续的告警分级提供了数据基础。

请求体的构造

Temperature参数设为 0.2(取值范围 0-1),确保 AI 输出更偏向确定性结论而非创造性发挥;

MaxTokens设为 8000 则保证了复杂场景下的分析深度。

在Messages数组中,通过 “system” 角色传递分析规则,“user” 角色传递具体数据,这种分离式设计完全符合大语言模型的交互最佳实践。

func CreateFileWithAnalysis(mailConfig *MailConfig) {
	// 创建原始日志文件
	file, err := CreateTimeStampedFile()
	if err != nil {
		fmt.Println("日志文件创建失败:", err)
		return
	}
	defer file.Close()

	// 收集系统信息
	var contentBuffer bytes.Buffer
	contentBuffer.WriteString(datainfo.FormatHostInfo() + "\n\n")
	contentBuffer.WriteString(datainfo.FormatCPUInfo() + "\n\n")
	contentBuffer.WriteString(datainfo.FormatMemInfo() + "\n\n")
	contentBuffer.WriteString(datainfo.FormatDISKInfo() + "\n\n")
	contentBuffer.WriteString(datainfo.FormatNetInfo() + "\n")

	// 写入原始日志
	if _, err := file.Write(contentBuffer.Bytes()); err != nil {
		fmt.Println("写入原始日志失败:", err)
		return
	}
	fmt.Println("原始系统信息已收集:", file.Name())

	// 调用AI分析(使用完整日志内容)
	aiPath, err := AnalyzeLogWithAI(contentBuffer.String())
	if err != nil {
		fmt.Println("AI分析失败:", err)
		return
	}

	// 验证AI文件内容
	if fi, _ := os.Stat(aiPath); fi.Size() == 0 {
		fmt.Println("警告:AI文件为空,请检查API响应")
	} else {
		fmt.Printf("AI深度分析报告已生成: %s (%.2fKB)\n", aiPath, float64(fi.Size())/1024)
		if fi.Size() == 0 {
			fmt.Println("警告: AI文件为空,请检查API响应")
		} else {
			fileSizeKB := float64(fi.Size()) / 1024
			fmt.Printf("AI深度分析报告已生成: %s (%.2fKB)\n", aiPath, fileSizeKB)

			// 当传入有效配置时才发送邮件
			if mailConfig != nil {
				// 检查收件人是否设置
				recipient := DefaultRecipient
				if mailConfig.Recipient != "" {
					recipient = mailConfig.Recipient
				}

				// 发送邮件
				if err := SendAnalysisEmail(aiPath, *mailConfig); err != nil {
					fmt.Printf("邮件发送失败: %v\n", err)
				} else {
					fmt.Printf("分析报告已发送至: %s\n", recipient)
				}
			}
		}
	}
}

当 AI 返回分析结果后,系统并没有简单地直接使用,而是通过三重校验确保可靠性:

首先是HTTP 响应的状态校验。当 API 返回非 200 状态码时,代码会尝试解析错误信息结构体,将error.message字段提取出来,避免将原始 JSON 错误直接抛给用户。这种错误处理,能大幅降低问题排查的难度。

其次是分析结果的持久化。AI 生成的报告被以时间戳-AI.md的命名格式存入 logs 目录,Markdown 格式既保证了可读性,又为后续邮件发送时的格式渲染提供了便利。文件权限 0644 的设置,则延续了整个系统在安全与可用性之间的平衡策略。

最后是文件完整性检查。在CreateFileWithAnalysis函数中,通过os.Stat检查 AI 生成文件的大小,若为空则立即抛出警告。这种防御性编程,能有效规避 API 返回空内容导致的下游流程异常。

全流程的串联逻辑

CreateFileWithAnalysis函数作为调度中枢,串联起数据采集、AI 分析与结果分发三大环节:

  • 它先调用CreateTimeStampedFile生成原始日志文件,再通过bytes.Buffer集中收集各类系统信息 —— 这种缓冲式写入既减少了 IO 操作次数,又能一次性获取完整的分析素材。当原始日志落盘后,立即调用AnalyzeLogWithAI启动分析,形成 “采集 - 分析” 的无缝衔接。

这一步的完成,让系统真正实现了从 “被动监控” 到 “主动诊断” 的跨越。

四. 引入邮件系统

从上面的代码中可以看到,在CreateFileWithAnalysis函数中有这样的字段:

func CreateFileWithAnalysis(mailConfig *MailConfig) {
    //////
			// 当传入有效配置时才发送邮件
			if mailConfig != nil {
				// 检查收件人是否设置
				recipient := DefaultRecipient
				if mailConfig.Recipient != "" {
					recipient = mailConfig.Recipient
				}

				// 发送邮件
				if err := SendAnalysisEmail(aiPath, *mailConfig); err != nil {
					fmt.Printf("邮件发送失败: %v\n", err)
				} else {
					fmt.Printf("分析报告已发送至: %s\n", recipient)
				}
			}
		}
	}
}

而这个字段就是判断是否发送邮件,而判断完成后如何发送邮件呢?

// 邮件配置常量(硬编码配置)
const (
	SenderName       = "系统监控中心"            // 发件人名称(固定)
	DefaultRecipient = "admin@example.com" // 默认收件人
)

// 邮件配置结构体
type MailConfig struct {
	SMTPServer   string // SMTP服务器地址
	SMTPPort     int    // SMTP端口
	SMTPUsername string // 邮箱账号
	SMTPPassword string // 邮箱密码/授权码
	Recipient    string // 收件人(可选覆盖默认值)
}

结构体与常量的配合形成了灵活的配置方案。

MailConfig结构体包含 SMTP 服务器地址、端口等动态参数,而SenderName、DefaultRecipient等常量则固化了系统名称、默认收件人等基础信息。这种设计让用户只需填写必要的邮箱认证信息,就能快速启用邮件功能,降低了使用门槛。

在 main 函数中,我们可以看到具体的配置实例:

func main() {
	///

	//邮件提醒引擎
	var mailCfg *utils.MailConfig
	mailCfg = &utils.MailConfig{
		SMTPServer:   "smtp.qq.com",       // QQ邮箱服务器
		SMTPPort:     587,                 // QQ邮箱TLS端口
		SMTPUsername: "example@qq.com", // 发件邮箱
		SMTPPassword: "exampleToken",  // 邮箱授权码(非密码)
		Recipient:    "example@qq.com",    // 自定义收件人(可选)
	}
	///
    ///
}

配置完整性校验是发送前的第一道防线。在SendAnalysisEmail函数开头,通过检查SMTPServer、SMTPUsername等核心字段是否为空,避免因配置缺失导致的发送失败。

// 验证配置完整性
	if config.SMTPServer == "" || config.SMTPUsername == "" || config.SMTPPassword == "" {
		return fmt.Errorf("邮件配置不完整:缺少SMTP服务器、用户名或密码")
	}

一封专业的运维邮件,需要在内容和格式上兼顾实用性与可读性:

<p>运维通知:系统诊断报告已生成</p>
<p>➤ 报告指纹: <code>2025_Analysis_Report</code></p>
<p>➤ 生成时间: 2025-07-22 14:30:45</p>
<p>➤ 文件大小: 128.45 KB</p>
<hr>
<p>请查看附件获取详细分析,如有高危问题,请需立即处理</p>

动态正文的信息分层:HTML 格式的邮件正文中,通过➤符号突出显示报告指纹、生成时间、文件大小等关键信息,既符合运维人员的阅读习惯,又能让收件人快速抓取核心内容。其中报告指纹直接关联 AI 分析文件的命名,方便后续在系统中定位原始报告。

	// 验证附件存在性
	if _, err := os.Stat(attachmentPath); os.IsNotExist(err) {
		return fmt.Errorf("附件不存在: %s", attachmentPath)
	}

	// 添加附件
	mail.Attach(attachmentPath)

附件处理的严谨性体现在两个层面:发送前通过os.Stat检查附件是否存在,避免发送空邮件;发送时使用mail.Attach方法将 AI 分析报告(Markdown 格式)作为附件携带,既保留了报告的原始格式,又方便收件人下载后本地查看。

	// 设置发件人(格式:发件人名称 <邮箱地址>)
	from := mail.FormatAddress(config.SMTPUsername, SenderName)
	mail.SetHeader("From", from)

	// 设置收件人(优先使用配置的收件人,否则使用默认值)
	recipient := DefaultRecipient
	if config.Recipient != "" {
		recipient = config.Recipient
	}
	mail.SetHeader("To", recipient)

发件人格式的规范化则借助mail.FormatAddress实现。将发件人名称与邮箱地址组合成"系统监控中心 <xxx@example.com>"的标准格式,既让收件人快速识别邮件来源,又符合邮件协议规范,降低了被标记为垃圾邮件的概率。

邮件能否成功送达,取决于底层发送逻辑:

SMTP 连接的封装实现由gomail.NewDialer完成。这个 Dialer 不仅封装了 TCP 连接的建立过程,还内置了 TLS 加密支持 —— 当连接 465 端口(SSL 加密端口)时会自动启用加密,确保账号密码在传输过程中不被泄露。这种开箱即用的安全特性,省去了手动配置加密的繁琐步骤。

dialer := gomail.NewDialer(
    config.SMTPServer,
    config.SMTPPort,
    config.SMTPUsername,
    config.SMTPPassword,
)

// 特殊端口处理
if config.SMTPPort == 465 {
    dialer.SSL = true // 启用隐式SSL
}

错误信息的精准传递增强了问题排查效率。当发送失败时,通过%w将原始错误包装进新的错误信息,比如"SMTP发送失败: 535 Authentication failed",既保留了底层错误详情,又增加了业务上下文,让运维人员能快速判断是账号错误还是服务器故障。

	if err := dialer.DialAndSend(mail); err != nil {
		return fmt.Errorf("SMTP发送失败: %w", err)
	}
	return nil

附件大小的实时计算:getFileSizeKB函数将字节数转换为 KB 单位并保留两位小数,在邮件正文中展示为 “2.45 KB” 这样直观的表述,让收件人对报告体量有初步预期。

// 获取文件大小(KB)
func getFileSizeKB(path string) float64 {
	fileInfo, err := os.Stat(path)
	if err != nil {
		return 0
	}
	return float64(fileInfo.Size()) / 1024
}

从配置校验到邮件发送,整个邮件系统就像一位尽职的信使,既确保了 AI 分析结果能及时送达,又通过规范的格式和完整的信息,降低了运维人员的处理成本。当高危问题出现时,这封带着 Markdown 报告的邮件,可能就是避免系统崩溃的关键提醒。

至此,整个代码逻辑和实现讲解就结束了,下面是全流程的一个演示。

五. 全流程演示

首先在main函数当作填写你的信息:

例如:

//邮件提醒引擎
	var mailCfg *utils.MailConfig
	mailCfg = &utils.MailConfig{
		SMTPServer:   "smtp.qq.com",       // QQ邮箱服务器
		SMTPPort:     587,                 // QQ邮箱TLS端口
		SMTPUsername: "exammple@qq.com", // 发件邮箱
		SMTPPassword: "token....",  // 邮箱授权码(非密码)
		Recipient:    "1234567890@qq.com",    // 自定义收件人(可选)
	}

这里需要注意,SMTPPassword填写的是邮箱的授权码,而不是登录密码。以 QQ 邮箱为例,授权码需要在邮箱设置的 “账户” 选项中开启 POP3/SMTP 服务后获取。

接着,在utils/deepseek_ai.go中填入 DeepSeek 的 API 密钥,这是 AI 分析功能的通行证:


const (
	ApiKey    = "APIKeys"  //在这里填写
	ApiUrl    = "https://api.deepseek.com/v1/chat/completions"
	ModelName = "deepseek-chat"
	LogDir    = "logs"
)

完成配置后,执行 main 文件,终端会输出一系列执行日志,展示系统的运行状态:

从日志中可以看到,系统先收集原始的系统信息并保存到日志文件,接着调用 AI 进行分析生成报告,最后将报告通过邮件发送出去。

执行完成后,查看 logs 目录,会发现生成了两个文件:

在这里插入图片描述

[root@hzy triyaotu]# ls logs/
2025_0722_13:48.log  20250722_13:49-AI.md

稍等片刻,收件邮箱会收到一封来自 “系统监控中心” 的邮件。邮件中清晰展示了报告的基本信息,包括报告指纹、生成时间和文件大小等。

邮件下方有一个可下载的.md文件,下载后用 Markdown 编辑器打开,就能看到 AI 生成的详细分析报告。报告按照我们预设的格式,从核心问题诊断、可执行方案、数学预测模型到监控增强建议,全面且有条理地呈现了系统的运行状态和潜在问题。

在这里插入图片描述

打开.md文档进行查看:

在这里插入图片描述

这就是AI帮助我们分析的结果了。

总结

回顾整个开发过程,从最初的系统监控 Web 端改造,到一步步拆分为信息输出、持久化存储、AI 分析和邮件系统等模块,我们完成了一个从 “数据采集” 到 “智能决策” 再到 “结果推送” 的完整闭环。

作为一名自学 Go 的个人开发者,这个项目不仅是对 Go 语言知识的实践,更是对系统运维理念的深化。通过 Gopsutil 获取系统信息,借助 DeepSeek 的 AI 能力进行分析,再通过邮件系统及时推送结果,整个流程将技术工具与运维需求紧密结合,实现了运维效率的提升。

当然,这个系统还有很多可以优化的地方,比如可以增加定时任务实现周期性监控,或者拓展更多的 AI 分析维度。但目前而言,它已经具备了一个基础系统运维 AI 助手的核心功能。

如果你也对这个项目感兴趣,不妨按照上面的流程尝试一下,或许能在实践中发现更多有趣的功能点和改进方向。欢迎大家提出宝贵的意见,让我们一起完善这个系统。

后续,我依旧想拿这个思路去坐坐其他应用的监控,可以试试看嘛。

源码可共享,私信或邮箱:triyaotu@163.com

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到