做一个监听本地文件的状态的小项目。比如监听日志log文件的修改,如果日志文件中出现error字段,则发送邮件或者短信提醒开发者。

简单拆分一下,主要有监听和“发送消息”(不同形式的消息)这两个功能需要实现。监听打算用go语言的tail包,发送邮件可以用smtp包。文章先分别讲监听、发送消息等前置知识,最后整合做满足效果的Demo。

1 监听文件

参考简书的文章Go实时监听日志文件内容

首先Terminal命令行安装tail包

go get github.com/hpcloud/tail
  • 1 命令行在项目文件下新建foo.log文件
echo > foo.log #新建foo.log空文件
echo wukangzuishuai111 >> foo.log #为foo.log文件,新增一行wukangzuishuai111
  • 2 新建tailTest目录,目录下建main.go
package main

import (
    "fmt"
    "github.com/hpcloud/tail"
    "time"
)

func main() {
    fileName := "./foo.log" // 被监听的文件,即foo日志文件
    config := tail.Config{
        ReOpen:    true,                                 // 重新打开
        Follow:    true,                                 // 是否跟随
        Location:  &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个地方开始读
        MustExist: false,                                // 文件不存在不报错
        Poll:      true,                                 // 监听新行,使用tail -f,这个参数非常重要
    }

    tails, err := tail.TailFile(fileName, config)
    if err != nil {
        fmt.Println("tail file failed, err:", err)
        return
    }

    var line *tail.Line
    var ok bool

    for {
        line, ok = <-tails.Lines
        if !ok {
            fmt.Printf("tail file close reopen, filename:%s\n", tails.Filename)
            time.Sleep(time.Second)
            continue
        }
        // 打印修改新增的内容
        fmt.Println(line.Text)
    }
}
  • 3 运行跑起来,修改foo.log文件,发现新增的信息被打印在控制台

image-20220608201959764

2 发送邮件

参考简书文章go发送邮件

smtp标准库就是golang实现邮件发送的一个标准库,你可以查看src中的net/stmp来查看他的源码。

smtp库不需要我们视线接口,我们只需要构造好参数,通过SendMail()来发送邮件即可。这里采用qq邮箱演示。

新建mailText目录,下建main.go

package main

import (
    "fmt"
    "net/smtp"
)

func main() {
    // 一些参数
    subject := fmt.Sprintf("Subject: %s\r\n", "日志异常提醒")
    send := fmt.Sprintf("From: %s 测试发件邮箱\r\n", "12059XXXXX.com")
    receiver := fmt.Sprintf("To: %s\r\n", "6736XXXXX@qq.com")
    contentType := "Content-Type: text/plain" + "; charset=UTF-8\r\n\r\n"
    // 拼接body
    content := "你好,这里是邮件的内容..."
    msgMail := []byte(subject + send + receiver + contentType + content)
    addr := "smtp.qq.com:25"
    auth := smtp.PlainAuth("", "12059XXXXX@qq.com", "rdzzXXXXXXXX", "smtp.qq.com")
    from := "12059XXXXX@qq.com"
    to := []string{"673XXXXX@qq.com"}
    // 发送邮件
    err := smtp.SendMail(addr, auth, from, to, msgMail)
    fmt.Println(err)
}

可以收到邮件

image-20220608204025410

3 配置文件

参考CSSDN上的文章golang几种常用配置文件使用方法总结(yaml、toml、json、xml、ini)

这里使用github上第三方开源gopkg.in/yaml.v2

使用配置文件存一些常用的配置,避免多处修改。项目必备。

新建yamlTest目录,目录下建conf.yaml文件

host: localhost:3306
user: tigerwolfc
pwd: 654321
dbname: tablename

然后命令行下载第三方库

go get gopkg.in/yaml.v2

yamlTest目录下写main.go

package main

import (
    "fmt"
    "gopkg.in/yaml.v2"
    "io/ioutil"
)

func main() {
    var c conf
    conf := c.getConf()
    fmt.Println(conf)
}

// conf 配置文件结构体
type conf struct {
    Host   string `yaml:"host"`
    User   string `yaml:"user"`
    Pwd    string `yaml:"pwd"`
    Dbname string `yaml:"dbname"`
}

func (c *conf) getConf() *conf {
    yamlFile, err := ioutil.ReadFile("./yamlTest/conf.yaml")
    if err != nil {
        fmt.Println(err.Error())
    }
    err = yaml.Unmarshal(yamlFile, c)
    if err != nil {
        fmt.Println(err.Error())
    }
    return c
}

运行,控制台打印出了配置文件中的数据信息

image-20220609112457374

4 监听多个文件并实时发送消息

先看看项目目录:

image-20220609151741066

1 先看看配置文件conf.yaml ,将文件名以及发送邮件的常用参数放在配置文件中 方便修改和调用:

fileName: ./log/foo1.log,./log/foo2.log
email:
  sendMail: 12059XXXXXX@qq.com
  password: rdzzXXXXXXXXXXXXX
  acceptMail: 673XXXXXX@qq.com
  subject: 日志异常提醒

2 main.go主函数,采用WaitGroup的方式控制检测多个文件的gorouting,所有协程结束之后才退出主函数。直接遍历文件名,为每个文件检测函数GetMessage分配一个gorouting。

package main

import (
    "fmt"
    tail "github.com/hpcloud/tail"
    "goLogMessage/controller"
    "goLogMessage/entity"
    "strings"
    "sync"
)

func main() {
    // 获取配置文件结构体
    var c entity.Conf
    conf := c.GetConf()
    fileList := strings.Split(conf.FileName, ",")
    //fmt.Println(conf)
    config := tail.Config{
        ReOpen:    true,                                 // 重新打开
        Follow:    true,                                 // 是否跟随
        Location:  &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个地方开始读
        MustExist: false,                                // 文件不存在不报错
        Poll:      true,                                 // 监听新行,使用tail -f,这个参数非常重要
    }
    // waitGroup等待所有协程退出
    var wg sync.WaitGroup
    wg.Add(len(fileList))
    for _, s := range fileList {
        tailFile, err := tail.TailFile(s, config)
        if err != nil {
            fmt.Printf("tail file failed,file: %v err: %v/n", s, err)
            return
        }
        go controller.GetMessage(tailFile, wg)
    }
    wg.Wait()
}

3 controller层下的getMessage.go,for循环一直遍历执行,监听文件的变化。controller层采用工厂模式的形式调用service层的发送消息的方法:发送邮件消息或者D-Chat消息。

package controller

import (
    "fmt"
    "github.com/hpcloud/tail"
    "goLogMessage/entity"
    "goLogMessage/service"
    "strings"
    "sync"
    "time"
)

func GetMessage(tailFile *tail.Tail, wg sync.WaitGroup) {
    var line *tail.Line
    var ok bool
    // for循环一直遍历执行,监听文件的变化
    for {
        line, ok = <-tailFile.Lines
        if !ok {
            fmt.Printf("tail file close reopen, filename:%s\n", tailFile.Filename)
            time.Sleep(time.Second)
            continue
        }
        fmt.Println(line.Text)
        text := line.Text
        if strings.Contains(text, "error") {
            var message entity.Message
            message.FileName = tailFile.Filename
            message.Body = text
            message.GetTime = time.Now()
            service.NewSendMessage("mail").Send(message)
            service.NewSendMessage("note").Send(message)
        }
    }
    wg.Done()
}

4 service层发送消息的逻辑sendMessage.go,里面有一个发送消息的接口,分别有发送邮件和发送D-chat消息两种实现。

package service

import (
    "fmt"
    "goLogMessage/entity"
    "net/smtp"
)

func NewSendMessage(str string) SendMessage {
    switch str {
    case "mail":
        return SendMail{}
    case "note":
        return SendDChat{}
    }
    return SendMail{}

}

// SendMessage 发送消息的接口
type SendMessage interface {
    Send(entity.Message) bool
}

// SendMail 邮件实现接口
type SendMail struct{}

// Send sendMail函数实现接口中的send方法
func (fMail SendMail) Send(msg entity.Message) bool {
    var c = entity.Conf{}
    emailConf := c.GetConf().Email

    fmt.Println(msg)
    // 发送邮件配置
    subject := fmt.Sprintf("Subject: %s\r\n", emailConf.Subject)
    send := fmt.Sprintf("From: %s 测试发件邮箱\r\n", emailConf.SenMail)
    receiver := fmt.Sprintf("To: %s\r\n", emailConf.AcceptMail)
    contentType := "Content-Type: text/plain" + "; charset=UTF-8\r\n\r\n"
    // 拼接body
    content := "文件:" + msg.FileName + ",存在error日志行为:" + msg.Body + ",出现时间:" + msg.GetTime.String()

    msgMail := []byte(subject + send + receiver + contentType + content)
    addr := "smtp.qq.com:25"
    auth := smtp.PlainAuth("", emailConf.SenMail, emailConf.Password, "smtp.qq.com")
    from := emailConf.SenMail
    to := []string{emailConf.AcceptMail}
    // 发送邮件
    err := smtp.SendMail(addr, auth, from, to, msgMail)
    if err != nil {
        fmt.Println(err)
        return false
    }
    return true
}

// SendDChat DChat实现接口
type SendDChat struct{}

func (fChat SendDChat) Send(msg entity.Message) bool {
    fmt.Println(msg)
    return true
}

5 entity包下面两个简单的实体类,分别表示配置和消息

  • 配置和获取配置
package entity

import (
    "fmt"
    "gopkg.in/yaml.v2"
    "io/ioutil"
)

type Conf struct {
    FileName string `yaml:"fileName"`

    Email struct {
        SenMail    string `yaml:"sendMail"`
        Password   string `yaml:"password"`
        AcceptMail string `yaml:"acceptMail"`
        Subject    string `yaml:"subject"`
    }
}

func (c *Conf) GetConf() *Conf {
    yamlFile, err := ioutil.ReadFile("conf.yaml")
    if err != nil {
        fmt.Println(err.Error())
    }
    err = yaml.Unmarshal(yamlFile, c)
    if err != nil {
        fmt.Println(err.Error())
    }
    return c
}
  • 表示消息的实体类
package entity

import "time"

type Message struct {
    FileName string
    Body     string
    GetTime  time.Time
}

6 最log包下新建两个日志文件foo1、foo2,启动项目,修改编辑日志文件,当日志中出现error字段时,会发送消息给指定的用户

image-20220609153720625

全文代码地址点击这里

c947a24bebca9a8178ec98d0204ecac4