├── .gitignore ├── config_example.yaml ├── notify ├── common.go └── mail.go ├── README.md ├── config └── config.go ├── main.go └── watcher └── watcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | log-alert 3 | *.exe 4 | config.yaml 5 | .idea 6 | *.log 7 | -------------------------------------------------------------------------------- /config_example.yaml: -------------------------------------------------------------------------------- 1 | mode: release 2 | receivers: 3 | - receiver@yufu.fun 4 | notify: 5 | driver: mail 6 | url: user|password|smtp.mxhichina.com|25 7 | files: 8 | - file: ./test-%Y-%m-%d.log 9 | desc: 测试文件 10 | bound: '\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]' 11 | rules: 12 | - rule: path/to.*(499|500) 13 | desc: http请求异常 14 | interval: 60s 15 | duration: 60s 16 | times: 5 17 | - rule: path/to.*200 18 | desc: http请求异常 19 | interval: 60s 20 | duration: 60s 21 | times: 5 -------------------------------------------------------------------------------- /notify/common.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "sync" 5 | "fmt" 6 | ) 7 | 8 | type Notify interface { 9 | Send(receivers []string, desc string, content ...string) 10 | } 11 | 12 | type notifierDriver func(url string, receivers []string) (Notify, error) 13 | 14 | var drivers sync.Map 15 | 16 | func init() { 17 | register("mail", getMailNotify) 18 | } 19 | 20 | func register(scheme string, driver notifierDriver) error { 21 | _, loaded := drivers.LoadOrStore(scheme, driver) 22 | if loaded { 23 | return fmt.Errorf("register: notifier driver scheme '%s' alrealy exists", scheme) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | 30 | func Open(schema, url string, receivers []string) (Notify, error) { 31 | driver, ok := drivers.Load(schema) 32 | if !ok { 33 | return nil, fmt.Errorf("open: notifier driver '%s' not exists", schema) 34 | } 35 | return driver.(notifierDriver)(url, receivers) 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # log-alert 2 | 日志文件监控警报 3 | 4 | ### 用途: 5 | 用于简单的监控服务器上日志文件,根据配置的规则(正则),当日志内容符合该规则,则触发警报 6 | 7 | ### 使用方法 8 | #### 1.部署 9 | - 从release中下载[二级制文件](https://github.com/yufunny/log-alert/releases/download/v0.1.1/log-alert-linux-amd64) 10 | - 上传log-alert-linux-amd64到服务器上 11 | - 执行 chmod 755 log-alert-linux-amd64 修改文件权限 12 | 13 | #### 2.配置 14 | - 复制config_example.yaml 到config.yaml 15 | - 修改config.yaml 16 | - mode: 模式 debug-调试模式 会有详细的日志, release-关闭debug日志 17 | - receivers: 通知接送者的邮箱 18 | - notify: 发送者邮件配置 19 | - driver: 发送通知驱动,mail为邮件,其他通知方式需要自行开发 20 | - url: 通知配置,邮件通知的格式为 发送邮箱|密码|smtp服务器|smtp端口号 21 | - files: 监听文件规则列表 22 | - file: 监听的文件 可以通过 %Y-年 %m-月 %d-日 匹配一些按日期拆分的日志文件 23 | - desc: 文件描述 24 | - bound: 多行日志分割标识,例如laravel/lumen的日志文件会有多行,可以通过配置'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]' 来合并多行日志,对于类似nginx日志只有一行的情况,配置空字符串即可 25 | - rules: 监控规则列表 26 | - file: 要监控的文件 27 | - rule: 正则表达式 28 | - desc: 描述 29 | - duration: 警报次数累计周期时长 30 | - times: 一个duration期间内,达到多少次后触发警报。当duration为0时,则和周期无关,累计达到该值就触发 31 | - interval: 2次警报最低间隔时间 32 | 33 | #### 3.执行 34 | 35 | 可以通过nohup直接执行, nohup ./log-alert-linux-amd64 & 36 | 37 | 也可以搭配pm2或其他进程管理工具使用。 38 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "gopkg.in/yaml.v2" 6 | "io/ioutil" 7 | ) 8 | 9 | type FileConfig struct { 10 | File string `yaml:"file"` 11 | Desc string `yaml:"desc"` 12 | Bound string `yaml:"bound"` 13 | Rules []RuleConfig `yaml:"rules"` 14 | } 15 | 16 | type RuleConfig struct { 17 | Rule string `yaml:"rule"` 18 | Desc string `yaml:"desc"` 19 | Duration string `yaml:"duration"` 20 | Times int `yaml:"times"` 21 | Interval string `yaml:"interval"` 22 | Receiver []string `yaml:"receivers"` 23 | } 24 | 25 | type NotifyConfig struct { 26 | Driver string `yaml:"driver"` 27 | Url string `yaml:"url"` 28 | } 29 | 30 | type SystemConfig struct { 31 | Mode string `yaml:"mode"` 32 | Receiver []string `yaml:"receivers"` 33 | Notify NotifyConfig `yaml:"notify"` 34 | Files []FileConfig `yaml:"files"` 35 | } 36 | 37 | // LoadConfig 加载系统配置 38 | func LoadConfig(file string) (*SystemConfig, error) { 39 | b, e := ioutil.ReadFile(file) 40 | if nil != e { 41 | return nil, errors.New("Config->Read config file[" + file + "] error; " + e.Error()) 42 | } 43 | config := &SystemConfig{} 44 | e = yaml.Unmarshal(b, config) 45 | if nil != e { 46 | return nil, errors.New("Config->Unmarshal config from config file[" + file + "] error; " + e.Error()) 47 | } 48 | return config, nil 49 | } 50 | -------------------------------------------------------------------------------- /notify/mail.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "gopkg.in/gomail.v2" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type MailNotify struct { 12 | Url string 13 | Receivers []string 14 | } 15 | 16 | func getMailNotify (url string, receivers []string) (Notify, error) { 17 | return &MailNotify{ 18 | Url:url, 19 | Receivers:receivers, 20 | }, nil 21 | } 22 | 23 | func (x *MailNotify) Send(receivers []string,desc string, content ...string) { 24 | p := strings.Split(x.Url, "|") 25 | if len(p) != 4 { 26 | logrus.Errorf("mail config error") 27 | return 28 | } 29 | address := p[0] 30 | password := p[1] 31 | smtp := p[2] 32 | port, err:= strconv.Atoi(p[3]) 33 | if err != nil { 34 | logrus.Errorf("mail port config error:%s", err.Error()) 35 | return 36 | } 37 | 38 | m := gomail.NewMessage() 39 | // 发件人 40 | m.SetAddressHeader("From", address, "notice") 41 | // 收件人 42 | 43 | if len(receivers) == 0 { 44 | receivers = x.Receivers 45 | } 46 | 47 | m.SetHeader("To", receivers...) 48 | // 主题 49 | m.SetHeader("Subject", fmt.Sprintf("[%s]日志监控警报", desc)) 50 | body := "日志内容:" 51 | for _, str := range content { 52 | body = body + "\n" + str 53 | } 54 | // 发送的body体 55 | m.SetBody("text/plain", body) 56 | d := gomail.NewDialer(smtp, port, address, password) 57 | if err := d.DialAndSend(m); err != nil { 58 | logrus.Errorf("[mail notify]error:%s", err.Error()) 59 | } else { 60 | logrus.Info("mail send success...") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/robfig/cron" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/urfave/cli" 7 | "github.com/yufunny/log-alert/config" 8 | "github.com/yufunny/log-alert/notify" 9 | "github.com/yufunny/log-alert/watcher" 10 | "os" 11 | ) 12 | 13 | var ( 14 | configPath string 15 | configs *config.SystemConfig 16 | ) 17 | 18 | func main() { 19 | log.SetLevel(log.DebugLevel) 20 | { 21 | app := cli.NewApp() 22 | app.Name = "log-alert" 23 | app.Usage = "alert by log file" 24 | app.Version = "0.1.0" 25 | app.Authors = []cli.Author{ 26 | { 27 | Name: "yufu", 28 | Email: "mxy@yufu.fun", 29 | }, 30 | } 31 | app.Flags = []cli.Flag{ 32 | cli.StringFlag{ 33 | Name: "config, c", 34 | Usage: "config path", 35 | Value: "config.yaml", 36 | Destination: &configPath, 37 | }, 38 | } 39 | app.Action = run 40 | 41 | log.Infof("[MAIN]Run start") 42 | err := app.Run(os.Args) 43 | if nil != err { 44 | log.Errorf("[MAIN]Run error; " + err.Error()) 45 | } 46 | } 47 | } 48 | 49 | func run(_ *cli.Context) { 50 | var err error 51 | configs, err = config.LoadConfig(configPath) 52 | if err != nil { 53 | log.Fatalf("[main]parse config error:%s", err.Error()) 54 | } 55 | if configs.Mode == "release" { 56 | log.SetLevel(log.InfoLevel) 57 | } 58 | 59 | notifier, err := notify.Open(configs.Notify.Driver, configs.Notify.Url, configs.Receiver) 60 | watchers := make([]*watcher.Watcher, 0) 61 | for _, file := range configs.Files { 62 | w := watcher.NewWatcher(file, notifier) 63 | go w.Watch() 64 | watchers = append(watchers, w) 65 | } 66 | 67 | c := cron.New() 68 | spec := "0 0 0 * * ?" 69 | c.AddFunc(spec, func() { 70 | for _, w := range watchers { 71 | go w.Watch() 72 | } 73 | }) 74 | c.Start() 75 | 76 | select {} 77 | } 78 | -------------------------------------------------------------------------------- /watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "github.com/papertrail/go-tail/follower" 5 | "github.com/yufunny/log-alert/notify" 6 | "io" 7 | "regexp" 8 | "time" 9 | "github.com/yufunny/log-alert/config" 10 | "strings" 11 | "strconv" 12 | "fmt" 13 | "os" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Watcher struct { 18 | file string 19 | desc string 20 | rules []*rule 21 | boundRegexp *regexp.Regexp 22 | 23 | notifier notify.Notify 24 | handler * follower.Follower 25 | live uint64 26 | piece []string 27 | clock *time.Ticker 28 | } 29 | 30 | type rule struct { 31 | ruleRegexp *regexp.Regexp 32 | desc string 33 | duration uint64 34 | times int 35 | interval uint64 36 | count int 37 | sent bool 38 | text []string 39 | receivers []string 40 | } 41 | 42 | func NewWatcher(fileConfig config.FileConfig, notifier notify.Notify) *Watcher { 43 | rules := make([]*rule, 0) 44 | for _, rule := range fileConfig.Rules { 45 | parsedRule := parseRule(rule) 46 | rules = append(rules, parsedRule) 47 | } 48 | var boundRegexp *regexp.Regexp 49 | if fileConfig.Bound != "" { 50 | boundRegexp = regexp.MustCompile(fileConfig.Bound) 51 | } else { 52 | boundRegexp = nil 53 | } 54 | watcher := &Watcher{ 55 | file: fileConfig.File, 56 | desc: fileConfig.Desc, 57 | boundRegexp: boundRegexp, 58 | notifier: notifier, 59 | rules: rules, 60 | } 61 | go watcher.tick() 62 | return watcher 63 | } 64 | 65 | func parseFile(raw string) string { 66 | if strings.Index(raw, "%Y") > -1 { 67 | raw = strings.Replace(raw, "%Y", strconv.Itoa(time.Now().Year()), 1) 68 | } 69 | if strings.Index(raw, "%m") > -1 { 70 | month := fmt.Sprintf("%02d", time.Now().Month()) 71 | raw = strings.Replace(raw, "%m", month, 1) 72 | } 73 | if strings.Index(raw, "%d") > -1 { 74 | day := fmt.Sprintf("%02d", time.Now().Day()) 75 | raw = strings.Replace(raw, "%d", day, 1) 76 | } 77 | return raw 78 | } 79 | 80 | func parseRule(ruleConfig config.RuleConfig) *rule { 81 | duration, _ := time.ParseDuration(ruleConfig.Duration) 82 | interval, _ := time.ParseDuration(ruleConfig.Interval) 83 | return &rule{ 84 | ruleRegexp: regexp.MustCompile(ruleConfig.Rule), 85 | desc: ruleConfig.Desc, 86 | duration: uint64(duration.Seconds()), 87 | interval: uint64(interval.Seconds()), 88 | times: ruleConfig.Times, 89 | count: 0, 90 | sent: false, 91 | text: make([]string, 0), 92 | receivers: ruleConfig.Receiver, 93 | } 94 | } 95 | 96 | func (w *Watcher) Watch() { 97 | if w.handler != nil { 98 | w.handler.Close() 99 | } 100 | parsedFile := parseFile(w.file) 101 | for { 102 | _, err := os.Stat(parsedFile) 103 | if err == nil { 104 | break 105 | } 106 | logrus.Infof("文件:%s 不存在", parsedFile) 107 | time.Sleep(time.Minute) 108 | } 109 | w.handler, _ = follower.New(parsedFile, follower.Config{ 110 | Whence: io.SeekEnd, 111 | Offset: 0, 112 | Reopen: true, 113 | }) 114 | 115 | logrus.Infof("start listening: %s", parsedFile) 116 | 117 | for line := range w.handler.Lines() { 118 | piece := w.parsePiece(line.String()) 119 | if len(piece) == 0 { 120 | continue 121 | } 122 | 123 | for _, rule := range w.rules { 124 | if rule.ruleRegexp.Match([]byte(piece[0])) { 125 | if !rule.sent { 126 | rule.text = append(rule.text, piece...) 127 | rule.count++ 128 | if rule.count >= rule.times { 129 | w.notifier.Send(rule.receivers, "[" + w.desc +"]" + rule.desc, rule.text...) 130 | if rule.interval > 0 { 131 | rule.sent = true 132 | } 133 | rule.text = make([]string, 0) 134 | rule.count = 0 135 | } 136 | } 137 | } 138 | } 139 | 140 | } 141 | } 142 | 143 | func (w *Watcher) parsePiece(line string) []string { 144 | if w.boundRegexp == nil { 145 | return []string{line} 146 | } 147 | if ! w.boundRegexp.Match([]byte(line)) { 148 | if len(w.piece) != 0 { 149 | w.piece = append(w.piece, line) 150 | } 151 | return []string{} 152 | } else { 153 | ret := w.piece 154 | w.piece = []string{line} 155 | return ret 156 | } 157 | } 158 | 159 | func (w *Watcher) tick() { 160 | w.clock = time.NewTicker(time.Second) 161 | go func() { 162 | for { 163 | select { 164 | case <-w.clock.C: 165 | w.live++ 166 | if w.live == ^uint64(0) { 167 | w.live = 0 168 | } 169 | for _, rule := range w.rules { 170 | if rule.interval> 0 && w.live % rule.interval == 0 { 171 | logrus.Debugf("internal clear") 172 | rule.sent = false 173 | } 174 | if rule.duration> 0 && w.live % rule.duration == 0 { 175 | logrus.Debugf("duration clear") 176 | rule.count = 0 177 | rule.text = make([]string, 0) 178 | } 179 | } 180 | } 181 | } 182 | }() 183 | } 184 | --------------------------------------------------------------------------------