diff --git a/eBPF_Visualization/eBPF_prometheus/checker/check.go b/eBPF_Visualization/eBPF_prometheus/checker/check.go index 0562708d4..a1f67104a 100644 --- a/eBPF_Visualization/eBPF_prometheus/checker/check.go +++ b/eBPF_Visualization/eBPF_prometheus/checker/check.go @@ -32,25 +32,37 @@ const ( MaxFileSize int64 = 100 * 1024 * 1024 ) +// 定义了一个名为 CollectCheck 的函数,用于检查和获取要收集的文件路径及其他参数 func CollectCheck(ctx *cli.Context) (string, error) { //if err := CheckArgs(ctx, 1, ConstExactArgs); err != nil { // return "", err //} + // 从命令行上下文中获取第一个参数,即文件路径 file := ctx.Args().Get(0) + + // 检查输入字符串是否有效 if !IsInputStringValid(file) { return "", fmt.Errorf("input:%s is invalid", file) } + // 检查文件是否存在 exist, err := PathExist(file) if err != nil { return "", err } + // 如果文件不存在,返回相应的错误信息 if !exist { return "", fmt.Errorf("file %s is not exist", file) } + + // 获取完整的命令行参数,并将它们连接成一个字符串 + // fullcommand 是一个包含参数的字符串切片 fullcommand := ctx.Args().Slice() + // 将字符串切片中的元素连接成一个字符串,fullcommand 是一个包含命令行参数的字符串切片," " 是连接各个参数时使用的分隔符 full := strings.Join(fullcommand, " ") + + // 返回完整的命令行参数作为结果,以及 nil 表示没有错误 return full, nil } @@ -78,15 +90,22 @@ func PathExist(path string) (bool, error) { return false, err } +// 定义了一个名为 CheckNormalError 的函数,用于检查并处理普通的错误 func CheckNormalError(err error) { + // 如果 err 不为 nil,表示发生了错误 if err != nil { + // 使用 log.Fatalln 打印错误信息并终止程序 log.Fatalln(err) } } +// 该函数接收一个字符串参数 content,用于判断是否符合 "proc" 命令的输出格式 func IsProcOutput(content string) bool { + // 定义了一个包含正则表达式的字符串,用于匹配 "proc" 命令的输出格式。该正则表达式包含了多个条件,用 | 分隔 pattern := `flag:\d+\s+pid:\d+\s+comm:\S+\s+offcpu_id|oncpu_time:\d+\s+offcpu_time|oncpu_time:\d+\s+oncpu_id|offcpu_id:\d+\s+oncpu_time|offcpu_time:\d+\s+time:[\d.]+` + // 将字符串正则表达式编译成一个正则表达式对象 re re := regexp.MustCompile(pattern) + // 检查传入的 content 是否与正则表达式匹配,如果匹配则返回 true,否则返回 false return re.MatchString(content) } diff --git a/eBPF_Visualization/eBPF_prometheus/collector/collect_output.go b/eBPF_Visualization/eBPF_prometheus/collector/collect_output.go index 2151650e7..48db5107a 100644 --- a/eBPF_Visualization/eBPF_prometheus/collector/collect_output.go +++ b/eBPF_Visualization/eBPF_prometheus/collector/collect_output.go @@ -37,14 +37,23 @@ import ( const firstline = int(1) +// 定义了一个结构体类型 Aservice type Aservice struct { - Name string - Desc string + // 服务的名称 + Name string + // 服务的描述 + Desc string + // NewInst 是一个函数类型的字段,用于创建服务实例 + // 当一个函数返回 interface{} 类型时,它实际上是在表示该函数可以返回任何类型的值。 + // 在使用这样的设计时,调用方可能需要使用类型断言来将 interface{} 类型的服务实例转换为具体的类型,以便进行后续的操作。 NewInst func(ctx *cli.Context, opts ...interface{}) (interface{}, error) } +// 定义了一个名为 GlobalServices 的变量,它是一个结构体类型的值 var GlobalServices = struct { + // 使用 sync.RWMutex 类型的嵌入字段,提供读写锁功能 sync.RWMutex + // services 是一个 map,键是字符串类型,值是指向 Aservice 结构体的指针 services map[string]*Aservice }{} @@ -61,49 +70,77 @@ func AddAService(svc *Aservice) error { return nil } +// 定义一个名为 RunServices 的函数,接受一个回调函数作为参数 func RunServices(fn func(nm string, svc *Aservice) error) error { + // 对全局服务列表进行加锁 GlobalServices.Lock() + // 在函数结束时解锁,确保解锁操作一定会执行 defer GlobalServices.Unlock() + // 遍历全局服务列表中的服务 + // name 是服务的名称,service 是服务实例 for name, service := range GlobalServices.services { + // 调用传入的回调函数,并传递服务名称和服务实例作为参数 if err := fn(name, service); err != nil { + // 如果回调函数返回错误,立即返回该错误 return err } } + // 如果遍历完所有服务都没有发生错误,返回 nil 表示成功 return nil } +// 定义一个名为 collectCommand 的 cli.Command 类型变量 var collectCommand = cli.Command{ - Name: "collect", + // 设置命令的名称 + Name: "collect", + // 设置命令的别名(可以使用 "c" 作为别名) Aliases: []string{"c"}, - Usage: "collect system data by eBPF", - Action: simpleCollect, + // 设置命令的用途描述 + Usage: "collect system data by eBPF", + // 设置命令执行时调用的处理函数(Action) + Action: simpleCollect, } +// init 函数在包被导入时自动执行 func init() { + // 初始化全局服务列表的 services 字段,使用 make 创建一个空的 map GlobalServices.services = make(map[string]*Aservice) + + // 创建并配置一个名为 "collectData" 的服务实例 svc := Aservice{ Name: "collectData", Desc: "collect eBPF data", - NewInst: newCollectCmd, + NewInst: newCollectCmd, // 指定该服务的 NewInst 方法为 newCollectCmd } + + // 将服务实例添加到全局服务列表 if err := AddAService(&svc); err != nil { log.Fatalf("Failed to load ... error:%s\n", err) return } + + // 创建并配置一个名为 "procCollectData" 的服务实例 procSvc := Aservice{ Name: "procCollectData", Desc: "collect process eBPF data", - NewInst: newProcCmd, + NewInst: newProcCmd, // 指定该服务的 NewInst 方法为 newProcCmd } + + // 将服务实例添加到全局服务列表 if err := AddAService(&procSvc); err != nil { log.Fatalf("Failed to load ... error:%s\n", err) return } + + // 创建并配置一个名为 "tmuxCollectData" 的服务实例 tmuxSvc := Aservice{ Name: "tmuxCollectData", Desc: "collect data from lock_image", - NewInst: newTmuxCmd} + NewInst: newTmuxCmd, // 指定该服务的 NewInst 方法为 newTmuxCmd + } + + // 将服务实例添加到全局服务列表 if err := AddAService(&tmuxSvc); err != nil { log.Fatalf("Failed to load ... error:%s\n", err) return @@ -122,89 +159,148 @@ func newTmuxCmd(ctx *cli.Context, opts ...interface{}) (interface{}, error) { return tmux_command, nil } +// 定义了一个名为 BPF_name 的结构体 type BPF_name struct { + // 结构体包含一个字段 Name,表示 BPF 名称 Name string } + +// 定义了一个名为 simpleCollect 的函数,用于执行简单的收集操作 func simpleCollect(ctx *cli.Context) error { + // 调用 CollectCheck 函数,检查并获取要收集的文件路径及其他参数信息 filePath, err := checker.CollectCheck(ctx) if err != nil { + // 如果出现错误,直接返回错误 return err } + + // 使用 strings.Fields 将文件路径拆分成字段,并取第一个字段作为路径 path := strings.Fields(filePath)[0] + + // 使用 strings.Split 将路径按 "/" 分割成切片 pathlist := strings.Split(path, "/") + + // 创建 BPF_name 结构体实例,并将其 Name 字段初始化为处理过的文件名,去除了文件名中的 ".py" 后缀,表示收集操作的名称 n := BPF_name{Name: strings.ReplaceAll(pathlist[len(pathlist)-1], ".py", "")} + + // 调用 BPF_name 结构体的 Run 方法执行收集操作 return n.Run(filePath) } +// 定义了一个名为 CheckFileType 的函数,用于检查文件类型并返回相应的命令字符串 func CheckFileType(filePath string) (specificcommand string) { + // 创建一个字符串切片,用于构建命令 cmdSlice := make([]string, 0) + // 将 "sudo" 添加到命令切片 cmdSlice = append(cmdSlice, "sudo") + // stdbuf -oL 表示将标准输出设置为行缓冲模式。这样可以使得输出更及时地显示在终端上,而不会等到缓冲区满或遇到换行符才刷新 + // 将 "stdbuf" 添加到命令切片 cmdSlice = append(cmdSlice, "stdbuf") + // 将 "-oL" 添加到命令切片,这是为了调整输出缓冲方式 cmdSlice = append(cmdSlice, "-oL") + // 将文件路径转换为小写 lowercaseFilename := strings.ToLower(filePath) + // 如果文件路径以 ".py" 结尾 if strings.HasSuffix(lowercaseFilename, ".py") { + // 打印日志,表示尝试运行一个 Python 程序 log.Println("Try to run a python program.") + // 将 "python3" 添加到命令切片 cmdSlice = append(cmdSlice, "python3") + // 将 "-u" 添加到命令切片,表示无缓冲输出 cmdSlice = append(cmdSlice, "-u") + // 将文件路径添加到命令切片 cmdSlice = append(cmdSlice, filePath) + // 使用空格连接命令切片,形成完整的命令字符串 cmdStr := strings.Join(cmdSlice, " ") + // 返回构建好的命令字符串 return cmdStr } else { + // 如果不是以 ".py" 结尾 + // 打印日志,表示尝试运行一个 eBPF 程序 + log.Println("Try to run a eBPF program.") + // 将 "-u" 添加到命令切片,表示无缓冲输出 + cmdSlice = append(cmdSlice, "-u") + // 将文件路径添加到命令切片 cmdSlice = append(cmdSlice, filePath) + // 使用空格连接命令切片,形成完整的命令字符串 cmdStr := strings.Join(cmdSlice, " ") + // 返回构建好的命令字符串 return cmdStr } } +// 定义了一个名为 Run 的方法,属于 BPF_name 结构体 func (b *BPF_name) Run(filePath string) error { + // 检查文件类型,获取相应的命令字符串 cmdStr := CheckFileType(filePath) + // 创建一个执行外部命令的 Command 对象 + // -c 表示后面的参数是一个命令字符串,而不是一个可执行文件 cmd := exec.Command("sh", "-c", cmdStr) + // 设置 SysProcAttr,用于设置新创建的进程的属性 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + // 获取命令的标准输出管道 stdout, err := cmd.StdoutPipe() log.Println("full command is :", cmdStr) if err != nil { log.Println("get stdout failed:", err) } + // 启动一个 goroutine 监听系统信号 go listenSystemSignals(cmd) //go getStdout(stdout) + // 创建一个用于传递 map 数据的通道 mapchan := make(chan []map[string]interface{}, 2) + // 启动一个 goroutine 用于重定向命令的标准输出 go redirectStdout(stdout, mapchan) + // 创建一个指向 prom_core.MyMetrics 类型的指针 metricsobj, + // 并使用结构体字段初始化 BPFName 和 Sqlinited。 metricsobj := &prom_core.MyMetrics{BPFName: b.Name, Sqlinited: false} + // 创建一个指向 dao.Sqlobj 类型的指针 sqlobj, + // 并使用结构体字段初始化 Tablename。 sqlobj := &dao.Sqlobj{Tablename: b.Name} metricsobj.Sqlobj = sqlobj + // 启动 MyMetrics 实例的服务 go metricsobj.StartService() - // process chan from redirect Stdout + + // 启动一个 goroutine 处理从重定向标准输出通道收到的数据 go func() { for { select { + // 当从通道中接收到数据时 case <-mapchan: + // 从通道中获取 map 切片,将其赋值给 MyMetrics 实例的 Maplist 字段 metricsobj.Maplist = <-mapchan log.Println(metricsobj.Maplist) + // 更新 MyMetrics 实例的数据 metricsobj.UpdateData() + // 如果 SQL 已初始化,则更新 SQL 数据;否则,初始化 SQL if metricsobj.Sqlinited { metricsobj.UpdataSql() } else { metricsobj.Initsql() } + // 从通道中接收第二次数据 <-mapchan default: } } }() + // 启动命令 err = cmd.Start() if err != nil { log.Printf("cmd.Start() analysis service failed: %v", err) os.Exit(-1) } + // 等待命令执行完毕 err = cmd.Wait() if err != nil { log.Printf("cmd.Run() analysis failed with: %v", err) @@ -214,73 +310,124 @@ func (b *BPF_name) Run(filePath string) error { return nil } +// 定义了一个名为 listenSystemSignals 的函数,用于监听系统信号 func listenSystemSignals(cmd *exec.Cmd) { + // 创建一个用于接收系统信号的通道 signalChan := make(chan os.Signal, 1) + + // 向 signalChan 注册接收的系统信号类型,包括 Interrupt、Kill 和 SIGTERM signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) + + // 无限循环,等待接收系统信号 for { select { + // 当从 signalChan 中接收到信号时 case <-signalChan: + // 向指定进程组发送 SIGKILL 信号,结束进程 _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + + // 退出当前程序,返回状态码 1 os.Exit(1) } } } +// 定义了一个名为 redirectStdout 的函数,用于读取 stdout,并将其解析成 map 数据发送到指定通道 func redirectStdout(stdout io.ReadCloser, mapchan chan []map[string]interface{}) { + // 用于存储解析后的 map 数据的切片 var maps []map[string]interface{} + // 互斥锁,用于保护 maps 切片的并发写入 var mu sync.Mutex + // 创建一个 bufio.Scanner 用于逐行读取 stdout 的内容 scanner := bufio.NewScanner(stdout) + // 存储标题的字符串切片 var titles []string + // 行号计数器 var line_number = 1 + // 命令索引,用于区分不同的命令 var commandindex = 0 + // 逐行扫描 stdout for scanner.Scan() { + // 获取一行的文本内容 line := scanner.Text() + // 处理第一行,提取标题信息 if line_number == firstline { // log.Printf("Title:%s\n", line) + // 将字符串 line 按照空白字符进行分割,并返回一个切片 parms,其中包含了被空白字符分割的各个子字符串 parms := strings.Fields(line) + // 遍历切片 parms 中的每个元素,并将元素的值赋给变量 value。在这里,使用了下划线 _ 表示我们对元素的索引不感兴趣,只关注元素的值 for _, value := range parms { + // 根据标题字段的值是否为 "COMM" 确定命令索引,如果不等于,则将 commandindex 的值增加 1 if strings.ToUpper(value) != "COMM" { commandindex = commandindex + 1 } + + // 创建一个新的空 map,其键是字符串类型,值是空接口类型 interface{}。这种设置允许 map 中的值可以是任何类型 one_map := make(map[string]interface{}) + // 向 one_map 中添加一个键值对,其中键是字符串 value,值是 nil。这里看起来是为了构建一个只包含一个键值对的 map one_map[value] = nil + // 将新创建的 one_map 添加到切片 maps 中。这样,maps 就成为一个包含了多个这样的 map 的切片 maps = append(maps, one_map) + + // 将标题字段添加到 titles 切片中 titles = append(titles, value) } } else { - // log.Printf("Content:%s\n", line) + // 使用 strings.Fields 函数将字符串 line 按照空白字符分割成多个字段,返回一个切片 parms。这个切片包含了一行文本中的各个字段 parms := strings.Fields(line) + // 声明了一个新的字符串切片 special_parms,用于存储处理后的字段。这个切片将用于存储由原始字段组成的新的字段切片,以保证字段数量与标题数量一致 var special_parms []string + + // 检查字段数量是否与标题数量一致(不是理解) if len(parms) != len(titles) { // log.Printf("title number: %d, content number:%d", len(titles), len(parms)) + // 声明一个字符串变量 COMM,用于存储合并后的字段值 var COMM string + // 遍历一行文本中的字段 for i, value := range parms { + // 检查字段是否在命令字段之前或者之后 if i < commandindex-1 && i >= len(parms)-commandindex { + // 将特殊处理的字段值添加到 special_parms 切片中 special_parms = append(special_parms, value) + // 如果当前字段是命令字段 } else if i == commandindex-1 { + // 将当前字段的值赋给 COMM COMM = value + // 如果当前字段在命令字段之前 } else if i < len(parms)-commandindex { + // 将当前字段的值追加到 COMM,用空格分隔 COMM = COMM + " " + value + // 将合并后的字段值添加到 special_parms 切片中 special_parms = append(special_parms, COMM) } } + + // 创建新的 map,并将数据发送到通道 newMap := make(map[string]interface{}) mu.Lock() + // 遍历特殊处理后的字段值 for i, value := range special_parms { + // 将字段值与标题对应,构建新的 map newMap[titles[i]] = value } mu.Unlock() + // 将新创建的 map 发送到通道 mapchan mapchan <- []map[string]interface{}{newMap} + // 如果字段数量与标题数量一致 } else { + // 创建新的 map,并将数据发送到通道 newMap := make(map[string]interface{}) mu.Lock() for i, value := range parms { newMap[titles[i]] = value } mu.Unlock() + // 将新创建的 map 发送到通道 mapchan mapchan <- []map[string]interface{}{newMap} } } + + // 行号递增 line_number += 1 } } diff --git a/eBPF_Visualization/eBPF_prometheus/collector/collect_proc_image.go b/eBPF_Visualization/eBPF_prometheus/collector/collect_proc_image.go index ab93699bb..a4b5c7a39 100644 --- a/eBPF_Visualization/eBPF_prometheus/collector/collect_proc_image.go +++ b/eBPF_Visualization/eBPF_prometheus/collector/collect_proc_image.go @@ -35,7 +35,7 @@ import ( "time" ) -// Pro_Setting 定义了设置项,通过读取同目录下的proc_setting.yaml实现对基本信息的设置。 +// Pro_Setting 定义了设置项,通过读取同目录下的tmux_proc_setting.yaml实现对基本信息的设置。 type Proc_Setting struct { Name string `yaml:"proc_name"` Path string `yaml:"proc_path"` @@ -43,17 +43,23 @@ type Proc_Setting struct { Max_Records int `yaml:"proc_max_records"` } +// 定义了一个名为 proc_imageCommand 的 CLI 命令,用于执行特定的数据收集任务 var proc_imageCommand = cli.Command{ Name: "proc_image", + // 设置命令的别名,即用户可以使用 "pro" 作为缩写形式来调用相同的命令 Aliases: []string{"pro"}, Usage: "Special collect data out from proc_image", + // 设置命令执行时调用的函数为 procCollect Action: procCollect, } -// Get_Setting 函数用于获取设置的信息 +// Get_Setting 函数用于获取设置的信息,返回一个错误、命令字符串和最大记录数 func Get_Setting(which string) (error, string, int) { + // 获取当前工作目录的绝对路径 currentDir, _ := os.Getwd() + // 读取配置文件 tmux_proc_setting.yaml 的内容 content, err := os.ReadFile(currentDir + "/collector/tmux_proc_setting.yaml") + // 如果读取配置文件时发生错误,输出错误信息并返回错误 if err != nil { log.Fatalf("Error reading file: %v", err) return err, "", 0 @@ -61,14 +67,19 @@ func Get_Setting(which string) (error, string, int) { command := "" maxrecords := 0 if which == "proc" { + // 声明一个 Proc_Setting 类型的变量 setting,用于存储从配置文件解析得到的设置 var setting Proc_Setting + // 使用 YAML 解码器将配置文件内容解析到 setting 变量中 err = yaml.Unmarshal(content, &setting) + // 如果解码时发生错误,输出错误信息并返回错误 if err != nil { log.Fatalf("Error unmarshaling YAML :%v", err) return err, "", 0 } + // 构造命令字符串,包括路径和进程 ID command = setting.Path + " -p " + setting.Pid + // 获取最大记录数 maxrecords = setting.Max_Records } else if which == "tmux" { var setting Tmux_Setting @@ -87,45 +98,63 @@ func Get_Setting(which string) (error, string, int) { } func procCollect(ctx *cli.Context) error { + // 调用 Get_Setting 函数获取 "proc" 类型的设置信息,其中 _ 表示占位符,因为 Get_Setting 返回三个值,但当前只关心 command _, command, _ := Get_Setting("proc") return ProcRun(command) } // ProcRun 是收集器的主函数,通过goroutin的方式实现数据收集,重定向,与prom_core包实现通信。 +// 该函数接收一个字符串参数 command,表示要执行的命令 func ProcRun(command string) error { + // 检查文件类型,并将检查后的命令字符串存储在 cmdStr 变量中 cmdStr := CheckFileType(command) + // 创建一个表示将要执行的命令的对象,并将其赋值给 cmd 变量 cmd := exec.Command("sh", "-c", cmdStr) + // 设置进程组 ID cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + // 获取命令的标准输出管道 stdout, err := cmd.StdoutPipe() + // 输出完整的执行命令,方便调试 log.Println("full command is :", cmdStr) if err != nil { log.Println("get stdout failed:", err) } + // 启动一个 goroutine 监听系统信号 go listenSystemSignals(cmd) + // 创建一个带有缓冲区的通道,用于在 goroutine 之间传递 map 类型的数据 mapchan := make(chan map[string]interface{}, 2) loc, _ := time.LoadLocation("Asia/Shanghai") + // 获取当前时间,并将其转换为浮点数格式 currenttime := float64(time.Now().In(loc).UnixNano()) / 1e9 + // 将命令字符串按 "/" 分割成字符串切片 pathlist := strings.Split(command, "/") + // 使用正则表达式判断命令中是否包含 "proc" is_proc, _ := regexp.MatchString(`proc`, pathlist[len(pathlist)-1]) + // 使用正则表达式判断命令中是否包含 "lifecycle" is_lifecycle, _ := regexp.MatchString(`lifecycle`, pathlist[len(pathlist)-1]) + // 使用正则表达式判断命令中是否包含 "lock" is_lock, _ := regexp.MatchString(`lock`, pathlist[len(pathlist)-1]) if is_proc || is_lifecycle { log.Println("This is lifecycle") _, _, maxrecords := Get_Setting("proc") + // 启动 goroutine,将命令的标准输出传递给 mapchan go redirectProc(stdout, mapchan) + // 创建 prom_core.ProcMetrics 类型的变量 procdata,用于存储从 "proc" 类型的命令中收集到的数据 procdata := prom_core.ProcMetrics{Max_records: maxrecords, NowTime: currenttime} + // 创建 dao.Sqlobj 类型的变量 sqlobj,用于与数据库交互,设置表名为 "proc_image_data" sqlobj := &dao.Sqlobj{Tablename: "proc_image_data"} procdata.Sqlobj = sqlobj - // process chan from redirectProc Stdout + // 启动 goroutine,初始化并运行 procdata 中的数据处理服务 go procdata.BootProcService() + // 启动匿名 goroutine,用于处理从 mapchan 接收到的数据并进行相应的操作,如更新数据库 go func() { for { select { @@ -169,12 +198,14 @@ func ProcRun(command string) error { }() } + // 启动命令,如果启动失败,则输出错误信息并退出程序 err = cmd.Start() if err != nil { log.Printf("cmd.Start() analysis service failed: %v", err) os.Exit(-1) } + // 等待命令执行完成,如果执行失败,则输出错误信息并退出程序 err = cmd.Wait() if err != nil { log.Printf("cmd.Run() analysis failed with: %v", err) @@ -185,21 +216,35 @@ func ProcRun(command string) error { } // redirectProc 实现数据重定向 +// stdout 表示命令的标准输出流,mapchan 表示用于传递数据的通道 func redirectProc(stdout io.ReadCloser, mapchan chan map[string]interface{}) { + // 声明一个 map 类型的变量 onemap,用于存储从命令的标准输出中解析出的数据 var onemap map[string]interface{} + // 使用 bufio.NewScanner 创建一个扫描器,用于逐行扫描命令的标准输出 scanner := bufio.NewScanner(stdout) + // 循环读取命令的标准输出的每一行 for scanner.Scan() { + // 获取当前行的文本内容 line := scanner.Text() + // 去除行中的不必要空格 line = checker.CutunexceptedSpace(line) + // 判断当前行是否是 "proc" 命令的输出 if checker.IsProcOutput(line) { + // 创建一个新的 map 对象,用于存储该行数据 onemap = make(map[string]interface{}) + // 将当前行按空格分割成字符串切片,每个元素表示一组键值对 parms := strings.Fields(line) + // 循环处理每个键值对 for _, value := range parms { + // 将键值对按冒号分割成键和值 parts := strings.Split(value, ":") + // 将键值对存储到 onemap 中 onemap[parts[0]] = parts[1] } // log.Println(onemap) + // 将存储了当前行数据的 onemap 发送到 mapchan 通道中,以便后续处理 mapchan <- onemap + // 如果不是 "proc" 命令的输出,则继续下一轮循环 } else { continue } diff --git a/eBPF_Visualization/eBPF_prometheus/dao/data_to_sqlite.go b/eBPF_Visualization/eBPF_prometheus/dao/data_to_sqlite.go index ceb01e817..88429ba23 100644 --- a/eBPF_Visualization/eBPF_prometheus/dao/data_to_sqlite.go +++ b/eBPF_Visualization/eBPF_prometheus/dao/data_to_sqlite.go @@ -27,9 +27,13 @@ import ( "strconv" ) +// 定义一个名为 Sqlobj 的结构体类型,用于封装数据库相关的信息 type Sqlobj struct { + // Tablename 字段存储数据库表的名称。 Tablename string + // db 字段是一个指向 gorm.DB 类型的指针,用于处理与数据库交互的对象。 db *gorm.DB + // Data 字段是一个 map,存储与数据库相关的信息。 Data map[string]interface{} } diff --git a/eBPF_Visualization/eBPF_prometheus/main.go b/eBPF_Visualization/eBPF_prometheus/main.go index 57fcfdc4c..011a4fc81 100644 --- a/eBPF_Visualization/eBPF_prometheus/main.go +++ b/eBPF_Visualization/eBPF_prometheus/main.go @@ -16,8 +16,10 @@ // // 主函数 +// 声明这个文件属于 main 包,是一个可执行的程序 package main +// 导入所需的包,包括自定义的 checker 和 collector 包,以及一些标准库和第三方库 import ( "ebpf_prometheus/checker" "ebpf_prometheus/collector" @@ -28,8 +30,11 @@ import ( "sort" ) +// 主函数的开始 func main() { + // 创建一个新的 CLI 应用 app := cli.NewApp() + // 配置应用的名称和使用说明文本 app.Name = "data-visual" app.Usage = ` use this cli-tool to collect output data and Convert output data to standard prometheus data. @@ -38,29 +43,40 @@ func main() { sudo data-visual collect ./vfsstat.py sudo data-visual proc_image ` + // 运行 collector 包中的 RunServices 函数,该函数接受一个匿名函数作为回调 + // 该匿名函数将每个服务的实例转换为 cli.Command 接口,并将其添加到应用的命令列表中 err := collector.RunServices(func(nm string, svc *collector.Aservice) error { + // 通过服务注册的 NewInst 函数创建服务实例 ins, err := svc.NewInst(nil) if err != nil { return err } + // 将 ins 转换为 cli.Command 接口 cmd, ok := ins.(cli.Command) if !ok { fmt.Printf("service %s doesn't implement cli.Command\n", nm) return fmt.Errorf("service %s doesn't implement cli.Command\n", nm) } + // 将成功转换的命令实例 cmd 添加到应用的命令列表中 app.Commands = append(app.Commands, &cmd) return nil }) + // 对应用的命令列表按照名称排序 sort.Sort(cli.CommandsByName(app.Commands)) + // 设置应用的 Before 钩子,该钩子将在执行命令之前运行 + // Before 钩子函数用于在执行应用程序的命令之前执行一些特定的任务 app.Before = doBeforeJob + // 运行 CLI 应用,处理命令行参数,并在执行期间处理错误 err = app.Run(os.Args) if err != nil { log.Fatal(err) } } +// doBeforeJob 函数是应用的 Before 钩子函数,用于在执行命令之前执行一些操作,这里检查并处理错误 func doBeforeJob(ctx *cli.Context) (err error) { + // 调用 checker 包中的 CheckNormalError 函数检查错误 checker.CheckNormalError(err) return nil } diff --git a/eBPF_Visualization/eBPF_prometheus/prom_core/processer.go b/eBPF_Visualization/eBPF_prometheus/prom_core/processer.go index 57770c8cc..45f576b12 100644 --- a/eBPF_Visualization/eBPF_prometheus/prom_core/processer.go +++ b/eBPF_Visualization/eBPF_prometheus/prom_core/processer.go @@ -30,12 +30,19 @@ import ( "sync" ) +// 定义一个名为 MyMetrics 的结构体类型。 type MyMetrics struct { + // BPFName 字段存储与此度量相关的 BPF 的名称。 BPFName string + // mu 字段是一个互斥锁,用于在多协程之间同步对结构体字段的访问。 mu sync.Mutex + // Maps 字段是一个 map,存储与此度量相关的信息。 Maps map[string]interface{} + // Maplist 字段是一个切片,存储与此度量相关的信息的列表。 Maplist []map[string]interface{} + // Sqlobj 字段是一个指向 dao.Sqlobj 类型的指针,用于处理与数据库相关的信息。 Sqlobj *dao.Sqlobj + // Sqlinited 字段表示与此度量相关的数据库是否已初始化。 Sqlinited bool } diff --git a/eBPF_Visualization/eBPF_prometheus/prom_core/prometheus.yaml b/eBPF_Visualization/eBPF_prometheus/prom_core/prometheus.yaml index 3b82f6504..dcfdc9f13 100644 --- a/eBPF_Visualization/eBPF_prometheus/prom_core/prometheus.yaml +++ b/eBPF_Visualization/eBPF_prometheus/prom_core/prometheus.yaml @@ -1,8 +1,15 @@ +#全局配置部分,应用于所有的 job 配置 global: + # 设置全局的抓取间隔,即 Prometheus 将每隔 50 毫秒收集一次指标 scrape_interval: 50ms +# 抓取配置部分,用于指定要抓取的目标和相应的设置 scrape_configs: + # 定义一个 job,命名为 'bpf_collector',这个 job 用于配置 Prometheus 如何抓取数据 - job_name: 'bpf_collector' - metrics_path: '/metrics' #采集路径 + # 指定从目标获取指标的路径,即采集路径 + metrics_path: '/metrics' + # 配置静态目标的部分,其中的目标地址是静态的,不会动态变化 static_configs: - - targets: ['127.0.0.1:8090'] #需要采集的地址 + #需要采集的地址 + - targets: ['127.0.0.1:8090'] diff --git a/eBPF_Visualization/eBPF_prometheus/runimages.sh b/eBPF_Visualization/eBPF_prometheus/runimages.sh index 2338e247c..eb20b34e8 100755 --- a/eBPF_Visualization/eBPF_prometheus/runimages.sh +++ b/eBPF_Visualization/eBPF_prometheus/runimages.sh @@ -4,28 +4,35 @@ prometheus_iamge="prom/prometheus" grafana_iamge="grafana/grafana-enterprise" -prometheus_info=$(docker ps -a -q --filter "ancestor=$prometheus_iamge") -grafana_info=$(docker ps -a -q --filter "ancestor=$grafana_iamge") +# 使用 docker ps 命令列出所有容器的 ID,过滤出指定镜像的容器 +prometheus_info=$(sudo docker ps -a -q --filter "ancestor=$prometheus_iamge") +grafana_info=$(sudo docker ps -a -q --filter "ancestor=$grafana_iamge") +# 检查 Prometheus 容器是否存在 if [ -n "$prometheus_info" ]; then # 如果容器存在,获取容器的 ID + # 即获取存储在 $prometheus_info 变量中的容器 ID 列表的第一个容器 ID,并将其存储到 container_id 变量中 container_id=$(echo "$prometheus_info" | head -n 1) echo "prometheus 容器存在,id为$container_id。启动容器..." sudo docker start $container_id else - echo "容器不存在,开始创建容器,并启动服务..." + echo "容器不存在,开始创建容器,并启动服务" + # 启动一个新的 Prometheus 容器,映射主机的端口 9090 到容器的端口 9090, + # 同时将主机上的 Prometheus 配置文件挂载到容器内,以便配置 Prometheus 服务 sudo docker run \ -p 9090:9090 \ - -v ./prom_core/promehteus.yaml:/etc/prometheus/prometheus.yml \ - prom/prometheus + -v ./prom_core/prometheus.yaml:/etc/prometheus/prometheus.yml \ + --name=prometheus prom/prometheus & fi if [ -n "$grafana_info" ]; then # 如果容器存在,获取容器的 ID + # 即获取存储在 $grafana_info 变量中的容器 ID 列表的第一个容器 ID,并将其存储到 container_id 变量中 container_id=$(echo "$grafana_info" | head -n 1) echo "grafana 容器存在,id为$container_id。启动容器..." sudo docker start $container_id else - echo "grafana容器不存在,开始创建容器,并启动服务..." - sudo docker run -d -p 3000:3000 --name=grafana grafana/grafana-enterprise + echo "grafana容器不存在,开始创建容器,并启动服务" + # 启动一个新的 Grafana Enterprise 容器,映射主机的端口 3000 到容器的端口 3000,并指定容器名称为 "grafana" + sudo docker run -d -p 3000:3000 --name=grafana grafana/grafana-enterprise & fi \ No newline at end of file