日志收集的agent开发
流程总结:
- ini.MapTo() 加载配置文件
- kafka.Init()
2.1 sarama.NewSyncProducer()
初始化kafka生产者client,并连接kafka
2.2 msgChan = make(chan *sarama.ProducerMessage, chanSize)
初始化存放日志数据的channel 将读日志和发送日志改为异步执行(channel)
2.3 go sendMsg()
后台goroutine 从msgChan中接收日志数据发送到kafka - etcd.Init()
3.1 clientv3.New()
初始化一个连接etcd的client - etcd.GetConf()
从etcd中拉取要收集日志的配置项。(我要去哪个目录下取什么数据,发送到哪个topic下面)
即去etcd中根据给定的key获取配置文件 配置文件是一个切片 切片中的每一项是日志文件的路径和topic - go etcd.WatchConf()
启动一个后台的goroutine 监听etcd中的日志收集项是否发生变化 - tailfile.Init()。
之前通过GetConf()拿到配置文件,根据配置项读取日志。有一个配置项给一个tailobj。
6.1 ttMgr = &tailTaskMgr{}
创建一个全局的tailTask管理者。因为如果有配置项变更,需要去管理,如新建、关闭等。
6.2 遍历配置项,newTailTask(conf.Path, conf.Topic)
遍历传过来的配置 每有一个配置项就启动一个tailTask 启动一个后台的goroutine去执行日志收集
6.3 go ttMgr.watch()
启动后台的goroutine去等新的配置来 从一个无缓冲的channel中接收新配置。新配置来的话有三种情况:原来存在的,不管;原来没有,创建;原来有现在没有,停掉。
配置文件版logagent
ini配置文件解析
cfg , err := ini.Load("./conf/config.ini")
if err != nil {
logrus.Error("load config failed,err:%v", err)
return
}
kafkaAddr := cfg.Section("kafka").Key("address").String()
fmt.Println(kafkaAddr)
初始化kafka
// Init 是初始化全局的kafka Client
func Init(address []string, chanSize int64)(err error){
// 1. 生产者配置
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // ACK
config.Producer.Partitioner = sarama.NewRandomPartitioner // 分区
config.Producer.Return.Successes = true // 确认
// 2. 连接kafka
client, err = sarama.NewSyncProducer(address, config)
if err != nil {
logrus.Error("kafka:producer closed, err:", err)
return
}
// 初始化MsgChan
MsgChan = make(chan *sarama.ProducerMessage, chanSize)
// 起一个后台的goroutine从msgchan中读数据(main函数中的通道)
go sendMsg()
return
}
DATA := <-CHAN ,和向channel传入数据相反,在数据输送箭头的右侧的是channel,形象地展现了数据从‘隧道’流出到变量里。
go sendMsg()
利用select取channel,然后去除msg的值发送给kafka(SendMessage)
初始化tail
func Init(filename string)(err error){
config := tail.Config{
ReOpen: true,
Follow: true,
Location: &tail.SeekInfo{Offset: 0, Whence: 2},
MustExist: false,
Poll: true,
}
// 打开文件开始读取数据
TailObj, err = tail.TailFile(filename, config)
if err != nil {
logrus.Error("tailfile: create tailObj for path:%s failed, err:%v\n", filename, err)
return
}
return
}
收集日志发送到kafka
func run ()(err error){
// logfile --> TailObj --> log --> Client --> kafka
for {
// 循环读数据
line, ok := <-tailfile.TailObj.Lines // chan tail.Line
if !ok {
logrus.Warn("tail file close reopen, filename:%s\n", tailfile.TailObj.Filename)
time.Sleep(time.Second) // 读取出错等一秒
continue
}
// 利用通道将同步的代码改为异步的
// 把读出来的一行日志包装秤kafka里面的msg类型
msg := &sarama.ProducerMessage{}
msg.Topic = "web_log"
msg.Value = sarama.StringEncoder(line.Text)
// 丢到通道中
kafka.MsgChan <- msg
}
}
logfile --> TailObj --> log --> Client --> kafka
从日志logfile通过tailobj读取出一行,包装成kafka认识的一个msg对象,扔到通道中。
为什么要扔到通道?因为如果直接调用发送消息的函数,相当于编程for循环里取一行日志文件,包装成函数,然后再调用函数,相当于函数调用函数,是同步的过程,容易阻塞。
main函数:
小结
介绍etcd
类似于zookeeper, etcd\consul
etcd搭建
详见腾讯文档:https://docs.qq.com/doc/DTndrQXdXYUxUU09O?opendocxfrom=admin
Go操作etcd
注意 put是client/V3版本的命令!!!
如果使用etcdctl.exe
来操作etcd的话,记得要设置环境变量:
SET ETCDCTL_API=3
Mac&Linux:
export ETCDCTL_API=3
put和set
func main(){
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout:time.Second*5,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v", err)
return
}
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err = cli.Put(ctx, "s4", "真好")
if err != nil {
fmt.Printf("put to etcd failed, err:%v", err)
return
}
cancel()
// get
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
gr, err := cli.Get(ctx, "s4")
if err != nil {
fmt.Printf("get from etcd failed, err:%v", err)
return
}
for _, ev := range gr.Kvs{
fmt.Printf("key:%s value:%s\n", ev.Key, ev.Value)
}
cancel()
}
watch
监控etcd中key的变化(创建\更改\删除)
func main(){
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout:time.Second*5,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v", err)
return
}
defer cli.Close()
// watch
watchCh := cli.Watch(context.Background(), "s4")
for wresp := range watchCh{
for _, evt := range wresp.Events{
fmt.Printf("type:%s key:%s value:%s\n", evt.Type, evt.Kv.Key, evt.Kv.Value)
}
}
}
kafka消费
package main
import (
"fmt"
"github.com/Shopify/sarama"
"sync"
)
// kafka consumer(消费者)
func main(){
// 创建新的消费者
consumer, err:= sarama.NewConsumer([]string{"127.0.0.1:9092"}, nil)
if err != nil {
fmt.Printf("fail to start consumer, err:%v\n", err)
return
}
// 拿到指定topic下面的所有分区列表
partitionList, err := consumer.Partitions("web_log") // 根据topic取到所有的分区
if err != nil {
fmt.Printf("fail to get list of partition:err%v\n", err)
return
}
fmt.Println(partitionList)
var wg sync.WaitGroup
for partition := range partitionList{ // 遍历所有的分区
// 针对每个分区创建一个对应的分区消费者
pc, err := consumer.ConsumePartition("web_log", int32(partition),sarama.OffsetNewest)
if err != nil {
fmt.Printf("failed to start consumer for partition %d,err:%v\n",
partition, err)
return
}
defer pc.AsyncClose()
// 异步从每个分区消费信息
wg.Add(1)
go func(sarama.PartitionConsumer){
for msg:=range pc.Messages(){
fmt.Printf("Partition:%d Offset:%d Key:%s Value:%s",
msg.Partition, msg.Offset, msg.Key, msg.Value)
}
}(pc)
}
wg.Wait()
}
总结
之前版本的logagent还存在以下问题:
- 只能读取一个日志文件,不支持多个日志文件。
- 无法管理日志的topic
思路:
用etcd存储要收集的日志项,使用json格式数据:
[
{
"path": "d:\logs\s4.log",
"topic": "s4_log",
},
{
"path": "e:\logs\web.log",
"topic": "web_log",
},
]
lagagent使用etcd管理收集项
Init
func Init(address []string)(err error){
client, err = clientv3.New(clientv3.Config{
Endpoints: address,
DialTimeout:time.Second*5,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v", err)
return
}
return
}
GetConf
// 拉取日志收集配置项的函数
func GetConf(key string)(collectEntryList []collectEntry, err error){
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
resp, err := client.Get(ctx, key)
if err != nil {
logrus.Errorf("get conf from etcd by key:%s failed ,err:%v",key, err)
return
}
if len(resp.Kvs) == 0 {
logrus.Warningf("get len:0 conf from etcd by key:%s",key)
return
}
ret := resp.Kvs[0]
// ret.Value // json格式字符串
fmt.Println(ret.Value)
err = json.Unmarshal(ret.Value, &collectEntryList)
if err != nil {
logrus.Errorf("json unmarshal failed, err:%v", err)
return
}
return
}
为每个单独的配置项启动tailTask
管理日志收集项
程序启动之后,拉去了最新的配置之后,就应该派一个小弟去监控etcd中 collect_log_conf
这个key的变化
logagent流程梳理
暂留的问题
如果logagent停了需要记录上一次的位置,参考filebeat。
不同服务器日志存放的位置可能不同,需要注意。它们的配置通过服务器ip来区分。
config.ini中的collect_key里放一个占位符,动态分配ip。
每台服务器上的logagent的收集项可能都不一致,我们需要让logagent去etcd中根据IP获取自己的配置
如何获取本机的IP
net.InterfaceAddrs() 用net库里的包
//方法一
func GetLocalIP() (ip string, err error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return
}
for _, addr := range addrs {
ipAddr, ok := addr.(*net.IPNet) // 类型断言,addr是接口类型,如果是括号里的类型则返回true,否则false
if !ok {
continue
}
//三步过滤掉。
if ipAddr.IP.IsLoopback() {
continue
}
if !ipAddr.IP.IsGlobalUnicast() {
continue
}
fmt.Println(ipAddr)
return ipAddr.IP.String(), nil
}
return
}
//方法二
// Get preferred outbound ip of this machine
func GetOutboundIP() string {
//udp拨号,但不会真建立请求。tcp也可
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
fmt.Println(localAddr.String())
return localAddr.IP.String()
}
logagent中集成根据ip拉取配置
etcd中配置的key要注意使用IP
今日内容
gopsutil包
influxDB时序数据库
安装
Windows
下载链接:https://dl.influxdata.com/influxdb/releases/influxdb-1.7.7_windows_amd64.zip
Mac
下载链接:https://dl.influxdata.com/influxdb/releases/influxdb-1.7.7_darwin_amd64.tar.gz
或者:
brew update
brew install influxdb
基本命令
官方文档:https://docs.influxdata.com/influxdb/v1.7/introduction/getting-started
grafana
展示数据的工具,监控数据可视化
- 搜索引擎找官网
下载
下载地址:https://grafana.com/grafana/download
安装
解压
把conf/sample.ini
复制一份然后重命名为conf/custom.ini
在解压目录下,执行
bin\grafana-server.exe
默认在本机的:3000
端口启动
默认账号密码都是:admin
grafana安装插件
浏览插件仓库:https://grafana.com/grafana/plugins
选择自己要安装的插件,然后按照提示安装即可.
例如,要安装饼图插件:https://grafana.com/grafana/plugins/grafana-piechart-panel
在grafana目录下面,执行以下命令:
grafana-cli.exe plugins install grafana-piechart-panel
然后重启grafana即可.