time-exporter 开发
1 前言
1.1 功能设计
监控需求:
- 监控时间服务是否存在
- 监控时间服务配置文件是否修改
- 监控时间服务的时区或者时间是否正确
开发思路:
- 通过监控 PID 来判断程序是否存在
- 通过监控文件来实现文件是否有被修改
- 通过校验时间来监控时间是否有区别
目录结构:
[17:26:45 root@go ntp]#tree
.
├── conf
│ └── config.go
├── controller
│ ├── monitoring_file.go
│ └── ntpUp.go
├── etc
│ └── config.yaml
├── go.mod
├── go.sum
├── log
│ └── time_exporter.log
├── logs
│ └── log.go
├── main.go
└── timeService_exporter
2 开发流程
2.1 编写 controller 模块
2.1.1获取 PID 实现程序存活监控
在 Go 语言中获取当前进程的进程 ID 可以使用标准库中的 os
包。具体操作如下:
package main
import (
"fmt"
"os"
)
func main() {
pid := os.Getpid()
fmt.Println(pid)
}
执行:
[10:23:10 root@go demo4]#go run mian.go
1376862
以上代码中,os.Getpid()
函数返回当前进程的进程 ID,我们将其赋值给变量 pid
,然后使用 fmt.Println()
函数输出进程 ID。
在 Golang 中获取进程名对应的 PID 可以使用 os/exec
包中的 pgrep
命令来实现。示例代码如下:
package main
import (
"fmt"
"os/exec"
"strconv"
"strings"
)
func main() {
processName := "myprocess"
cmd := exec.Command("pgrep", "-f", processName)
output, err := cmd.Output()
if err != nil {
fmt.Println("Error: ", err)
return
}
pidStr := strings.TrimSpace(string(output))
pid, err := strconv.Atoi(pidStr)
if err != nil {
fmt.Println("Error: ", err)
return
}
fmt.Printf("PID of process %s is %d\n", processName, pid)
}
在上述代码中,我们使用 pgrep -f
命令来查找进程名中包含指定字符串的进程,并返回对应的 PID。然后使用 strconv.Atoi
函数将 PID 字符串转换为整数类型。最后输出进程名和对应的 PID。
将其嵌入只 Prometheus 监控代码块中
package controller
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"os/exec"
)
// Determine whether the ntp server is alive
func NtpUp() {
// 这里需要监控的进程是 systemd-timesyncd
proecssName := "systemd-timesyncd"
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "proecss:systemd_timesyncd",
Namespace: "Proecss",
Help: "Proecss UP info 0 表示存活 1 表示死亡",
ConstLabels: prometheus.Labels{"Proecss_Name": "Systemd_timesyncd"},
}, func() float64 {
// 通过 pgrep -f 获取到对应的监控进程
cmd := exec.Command("pgrep", "-f", proecssName)
// 获取 PID
output, err := cmd.Output()
if err != nil {
fmt.Println("Error:", err)
}
// 基于获取到的 pid 判断是否为空,如果 pid 为空就表示该进程不存在不为空就是存活
if string(output) != "" {
return 0
} else {
return 1
}
}))
}
但是我们会发如果后期需要对别的进程监控那么这里的设计就不够人性化,所以需要通过 viper
包实现对配置文件的调用
2.1.2 监控文件是否修改
如果发现 time service config 文件有修改或者访问也需要告警,所以这里需要使用到 fsnotify 库
如果golang程序想监听文件系统中某些文件的变化, 那么最普遍的做法是使用fsnotify库. 起初是由Chris Howey(github account: howeyc)开发的库, 后来受到广大开发者的喜爱, 遂单独建立仓库. 至今为止, 其仓库已收到了5.9k star, 这足以证明其受欢迎程度. 想了解更多关于fsnotify的历史, 可以查看官网.
fsnotify 库使用范例如下:
func H(confName string) {
// New 一个 watcher struct,其实 watcher 变量为一个 chan
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
// 关闭管道
defer watcher.Close()
done := make(chan bool)
// 开启携程实时监控
go func() {
for {
select {
// 判断监控事件直接赋值给 event
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
// 判断文件是否有写入操作
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// 添加需要监控的文件路径
err = watcher.Add("/etc/systemd/timesyncd.conf")
if err != nil {
log.Fatal(err)
}
<-done
}
- 导入 fsnotify 和 prometheus 库
import (
"github.com/fsnotify/fsnotify"
"github.com/prometheus/client_golang/prometheus"
)
- 创建一个 FileWatcher 结构体来存储文件监控相关的信息
type FileWatcher struct {
watcher *fsnotify.Watcher
filesMonitored prometheus.Gauge
alertsTriggered prometheus.Counter
}
- 实现 NewFileWatcher 函数,该函数用来创建一个新的 FileWatcher 实例
func NewFileWatcher(path string, filesMonitored prometheus.Gauge, alertsTriggered prometheus.Counter) (*FileWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
err = watcher.Add(path)
if err != nil {
return nil, err
}
return &FileWatcher{watcher, filesMonitored, alertsTriggered}, nil
}
- 实现 Start 和 Stop 函数,用于启动和停止文件监控
func (fw *FileWatcher) Start() {
go func() {
for {
select {
case event, ok := <-fw.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
log.Printf("file %s modified\n", event.Name)
fw.alertsTriggered.Inc()
}
case err, ok := <-fw.watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
fw.filesMonitored.Set(1)
}
func (fw *FileWatcher) Stop() {
fw.watcher.Close()
fw.filesMonitored.Set(0)
}
- 最后,在 main 函数中初始化 FileWatcher 并启动监控
func main() {
// 初始化 Prometheus 相关指标
filesMonitored := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "files_monitored",
Help: "Number of files being monitored",
})
alertsTriggered := prometheus.NewCounter(prometheus.CounterOpts{
Name: "alerts_triggered",
Help: "Number of alerts triggered",
})
prometheus.MustRegister(filesMonitored)
prometheus.MustRegister(alertsTriggered)
// 初始化 FileWatcher
fw, err := NewFileWatcher("/path/to/directory", filesMonitored, alertsTriggered)
if err != nil {
log.Fatal(err)
}
// 启动监控
fw.Start()
defer fw.Stop()
// 启动 HTTP 服务器,暴露 Prometheus 指标
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}
以上代码仅供参考,具体实现方式可能因应用场景而异。
2.2 编写 conf 模块,提供配置文件功能
2.2.1 viper 嵌入至程序中
viper 的基础使用这里不再过多赘述,直接看我写过的相关文档
http://39.105.137.222:8089/?p=2265#header-id-7
package conf
import "github.com/spf13/viper"
// 需要监控的 service,用来获取对应的 PID 和 配置文件路径
type Service struct {
ServiceName string `mapstructure:"name"`
ServiceConfigPath string `mapstructure:"config_path"`
}
// 日志配置
type Log struct {
FileName string `mapstructure:"filename"`
Max_age string `mapstructure:"max_age"`
}
// 指定 exporter 监控端口
type Web struct {
Addr string `mapstructure:"addr"`
}
type Options struct {
Service Service `mapstructuer:"service"`
Web Web `mapstructuer:"web"`
Log Log `mapstructuer:"log"`
}
// 解析配置文件并返回解析后的 options 结构体
func ParseConfig(path string) (*Options, error) {
conf := viper.New()
conf.SetDefault("web.addr", ":19090")
conf.SetConfigFile(path)
if err := conf.ReadInConfig(); err != nil {
return nil, err
}
options := &Options{}
if err := conf.Unmarshal(options); err != nil {
return nil, err
}
return options, nil
}
编写配置文件 yaml
[16:41:07 root@go etc]#cat config.yaml
# 数据库配置
service:
name: "systemd-timesyncd"
config_path: ""
# web 配置
web:
# 默认地址为 19090
addr:
# log 配置
# 默认日志路径当前路径下 log/time_exporter.log
# 日志级别共有 panic fatal error warn info debug trace 7 个级别
log:
filename: log/time_exporter.log
max_age: 10
2.3 编写 logs 模块
这里的 logs 模块采用的是 logrus
,教程的话可以查看我的这篇文章
http://39.105.137.222:8089/?p=2265#header-id-7
package logs
import (
"cicc/ntp/conf"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
func Ex_logs(log *conf.Log) {
logr := lumberjack.Logger{
Filename: log.FileName,
MaxAge: log.Max_age,
}
defer logr.Close()
logrus.SetOutput(&logr)
logrus.SetLevel(logrus.DebugLevel)
logrus.SetReportCaller(true)
}
// 自定义日志
func WithFields(metrics, sample interface{}) {
logrus.WithFields(
logrus.Fields{
"metrics": metrics,
"sample": sample,
}).Error("Time_Service err")
}
2.4 main 程序编写
package main
import (
"cicc/ntp/conf"
"cicc/ntp/controller"
"cicc/ntp/logs"
"flag"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
"log"
"net/http"
)
func main() {
// Linux Service systemd 启动
var config string
flag.StringVar(&config, "config", "/apps/time/config.yaml", "config")
flag.Parse()
options, err := conf.ParseConfig(config)
if err != nil {
logrus.Fatal(err)
}
logs.Ex_logs(&options.Log)
// 监控数量
filesMonitored := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "file_monitored",
Help: "Number of files being monitored",
})
// 触发告警数量
alertsTriggered := prometheus.NewCounter(prometheus.CounterOpts{
Name: "file_monitored",
Namespace: "Time_Service",
Help: "file_monitored:文件监控触发告警正常情况下是 0 如果不等于 0 那就触发告警",
ConstLabels: prometheus.Labels{"FileName": options.Service.ServiceConfigPath},
})
// 注册
prometheus.MustRegister(filesMonitored)
prometheus.MustRegister(alertsTriggered)
// New一个 FileWatcher 结构体
fw, err := controller.NewFileWatcher(options.Service.ServiceConfigPath, filesMonitored, alertsTriggered)
if err != nil {
log.Fatal(err)
logs.WithFields("NewFileWatcher_err:", err)
}
fw.Start()
defer fw.Stop()
// 监控 service 是否存活
controller.Service_Up(options.Service.ServiceName)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(options.Web.Addr, nil)
}
3 功能演示
功能演示的前提需要自己部署并配置 Prometheus、alertmanager、并且 alertmanager 已经接对告警接收媒介,这里我用企业微信来做告警接收媒介
1 配置 config.yaml 文件
[15:07:47 root@go ntp]#vim /apps/time/config.yaml
# 数据库配置
service:
name: "systemd-timesyncd"
config_path: "/etc/systemd/timesyncd.conf"
# web 配置
web:
# 默认地址为 19090
addr:
# log 配置
# 默认日志路径当前路径下 log/time_exporter.log
# 日志级别共有 panic fatal error warn info debug trace 7 个级别
log:
filename: log/time_exporter.log
max_age: 10
2 编写 service 文件,通过 systemd 启动
[15:08:42 root@go ntp]#vim /etc/systemd/system/timeService-exporter.service
[Unit]
Description=Systemd Test
After=network.target
[Service]
User=nobody
# Execute `systemctl daemon-reload` after ExecStart= is changed.
# 指定配置文件
ExecStart=/usr/local/bin/timeService_exporter -config "/apps/time/config.yaml"
[Install]
WantedBy=multi-user.target
3 加载并启动文件
[15:08:42 root@go ntp]# go build -o timeService_exporter
[15:08:42 root@go ntp]# cp timeService_exporter /usr/local/bin/
[15:08:42 root@go ntp]# systemctl daemon-reload
[15:08:42 root@go ntp]# systemctl enable --now timeService-exporter.service
[15:08:42 root@go ntp]# systemctl status timeService-exporter.service
● timeService-exporter.service - Systemd Test
Loaded: loaded (/etc/systemd/system/timeService-exporter.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-04-13 15:11:06 CST; 6s ago
Main PID: 1798087 (timeService_exp)
Tasks: 6 (limit: 19067)
Memory: 2.0M
CGroup: /system.slice/timeService-exporter.service
└─1798087 /usr/local/bin/timeService_exporter -config /apps/time/config.yaml
Apr 13 15:11:06 go systemd[1]: Started Systemd Test.
4 浏览器对应指标
http://10.0.0.135:19090/metrics
5 Prometheus 查看已对接
6 alertmanager 已配置
7 停掉服务并修改配置文件
[17:36:39 root@go ntp]#systemctl stop systemd-timesyncd.service
[17:37:19 root@go ntp]#echo "sdad" >> /etc/systemd/timesyncd.conf
8 alerts 已经变红
9 企微接收告警