工具包目录结构:

.
├── conf
│   ├── logAgent.ini
│   └── logAgentConfig.go
├── etcd
│   └── etcd.go
├── kafka
│   └── kafka.go
├── main.go
└── taillog
    ├── taillog.go
    └── taillog_mgr.go

logAgent.ini

 1 [kafka]
2 address=127.0.0.1:9092
3 chan_max_size=100000
4
5 [etcd]
6 address=127.0.0.1:2379
7 timeout=5
8 collect_log_key=xxx
9
10 [taillog]
11 filename="./my.log"

logAgentConf.go

 1 /**
2 * @Author: Mr.Cheng
3 * @Description:
4 * @File: logAgentConfig
5 * @Version: 1.0.0
6 * @Date: 2021/12/9 下午8:44
7 */
8
9 package logAgentConfig
10
11 type AppConf struct {
12 KafkaConf `ini:"kafka"`
13 EtcdConf `ini:"etcd"`
14 // TaillogConf `ini:"taillog"`
15 }
16
17 type KafkaConf struct {
18 Address string `ini:"address"`
19 Size int `ini:"chan_max_size"`
20 }
21
22 type EtcdConf struct {
23 Address string `ini:"address"`
24 Timeout int `ini:"timeout"`
25 Key string `ini:"collect_log_key"`
26 }
27
28 // ----- unused ↓️----
29
30 type TaillogConf struct {
31 FileName string `ini:"filename"`
32 }

etcd.go

 1 /**
2 * @Author: Mr.Cheng
3 * @Description:
4 * @File: etcd
5 * @Version: 1.0.0
6 * @Date: 2021/12/9 下午9:12
7 */
8 package etcd
9
10 import (
11 "context"
12 "encoding/json"
13 "fmt"
14 "go.etcd.io/etcd/clientv3"
15 "time"
16 )
17
18 var (
19 client *clientv3.Client
20 )
21
22 type LogEntry struct {
23 Path string `json:"path"` // 日志存放的路径
24 Topic string `json:"topic"` // 日志要发往kafka的topic
25 }
26
27 func Init(address string, interval int) (err error) {
28 client, err = clientv3.New(clientv3.Config{
29 Endpoints: []string{address},
30 DialTimeout: time.Duration(interval) * time.Second,
31 })
32 if err != nil {
33 fmt.Printf("connect to etcd failed, err:%v\n", err)
34 return err
35 }
36 return
37 }
38
39 // 从Etcd中根据Key获取配置项
40 func GetConf(key string) (LogEntryConf []*LogEntry, err error) {
41 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
42 resp, err := client.Get(ctx, key)
43 cancel()
44 if err != nil {
45 fmt.Printf("get from etcd failed, err:%v\n", err)
46 return nil, err
47 }
48 for _, ev := range resp.Kvs {
49 //fmt.Printf("%s:%s\n", ev.Key, ev.Value)
50 err = json.Unmarshal(ev.Value, &LogEntryConf)
51 if err != nil {
52 fmt.Printf("unmarshal etcd value failed, err:%v\n", err)
53 return nil, err
54 }
55 }
56 return LogEntryConf, nil
57 }
58
59 // etcd watch
60 func WatchConf(key string, newConfChan chan<- []*LogEntry) {
61 ch := client.Watch(context.Background(), key)
62 for wresp := range ch {
63 for _, evt := range wresp.Events {
64 fmt.Printf("Type:%v key:%v value:%v\n", evt.Type, string(evt.Kv.Key), string(evt.Kv.Value))
65 var newConf []*LogEntry
66 // 如果是删除操作,json.Unmarshal会报错,需手动添加一个空的newConf
67 if evt.Type != clientv3.EventTypeDelete {
68 err := json.Unmarshal(evt.Kv.Value, &newConf)
69 if err != nil {
70 fmt.Printf("unmarshal new conf failed, err:%v\n", err)
71 continue
72 }
73 }
74 newConfChan <- newConf
75 }
76 }
77 }

kafka.go

 1 /**
2 * @Author: Mr.Cheng
3 * @Description:往kafka写入日志
4 * @File: kafka
5 * @Version: 1.0.0
6 * @Date: 2021/12/9 下午2:19
7 */
8
9 package kafka
10
11 import (
12 "fmt"
13 "github.com/Shopify/sarama"
14 "time"
15 )
16
17 type logData struct {
18 Topic string
19 Data string
20 }
21
22 var (
23 client sarama.SyncProducer // 全局连接kafka的生产者
24 logDataChan chan *logData
25 )
26
27 // 初始化连接
28 func Init(address []string, size int) (err error) {
29 config := sarama.NewConfig()
30 config.Producer.RequiredAcks = sarama.WaitForAll // 发送模式(需leader和follow都确认)
31 config.Producer.Partitioner = sarama.NewRandomPartitioner // 选择分区的方式(轮询)
32 config.Producer.Return.Successes = true // 成功交付的消息将在success channel中返回
33
34 // 连接kafka
35 client, err = sarama.NewSyncProducer(address, config)
36 if err != nil {
37 fmt.Printf("client kafka failed, err:%v\n", err)
38 return err
39 }
40
41 // 初始化logDataChan
42 logDataChan = make(chan *logData, size)
43
44 // 从logDataChan中取数据发往kafaka
45 go sendToKafka()
46 return nil
47 }
48
49 func SendToChan(Topic, Data string) {
50 data := &logData{
51 Topic: Topic,
52 Data: Data,
53 }
54 select {
55 case logDataChan <- data:
56 default:
57 time.Sleep(time.Millisecond * 100)
58 }
59 }
60
61 func sendToKafka() {
62 // 循环从通道logDataChan取值并发送给kafka
63 for {
64 select {
65 case data := <-logDataChan:
66 msg := &sarama.ProducerMessage{}
67 msg.Topic = data.Topic
68 msg.Value = sarama.StringEncoder(data.Data)
69 pid, offset, err := client.SendMessage(msg)
70 if err != nil {
71 fmt.Printf("send msg failed, err:%v\n", err)
72 }
73 fmt.Printf("send msg success, pid:%v offect:%v\n", pid, offset)
74 default:
75 time.Sleep(time.Millisecond * 50)
76 }
77 }
78 }

taillog.go

 1 /**
2 * @Author: Mr.Cheng
3 * @Description:收集日志模块
4 * @File: taillog
5 * @Version: 1.0.0
6 * @Date: 2021/12/8 下午9:54
7 */
8
9 package taillog
10
11 import (
12 "context"
13 "day21/02.log_agent/kafka"
14 "fmt"
15 "github.com/hpcloud/tail"
16 "time"
17 )
18
19 type TailTask struct {
20 Path string
21 Topic string
22 Instance *tail.Tail
23 // 为了停止任务,存下context
24 ctx context.Context
25 cancel context.CancelFunc
26 }
27
28 func NewTailTask(Path, Topic string) (tailtask *TailTask, err error) {
29 config := tail.Config{
30 Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件那个地方开始读
31 ReOpen: true, // 重新打开
32 MustExist: false, // 文件不存在不报错
33 Poll: true,
34 Follow: true, // 是否跟随
35 }
36 ctx, cancel := context.WithCancel(context.Background())
37 tailObj, err := tail.TailFile(Path, config)
38 if err != nil {
39 fmt.Printf("tail file failed, err:%v\n", err)
40 return nil, err
41 }
42 tailtask = &TailTask{Path: Path, Topic: Topic, Instance: tailObj, ctx: ctx, cancel: cancel}
43 // 开启读取日志并发送给kafka
44 go tailtask.ReadFromTail()
45 return tailtask, nil
46 }
47
48 func (tailtask *TailTask) ReadFromTail() {
49 for {
50 select {
51 case <-tailtask.ctx.Done():
52 return
53 case line, ok := <-tailtask.Instance.Lines:
54 if !ok {
55 fmt.Printf("tail fail close reopen, filename:%s\n", tailtask.Path)
56 time.Sleep(time.Second)
57 continue
58 }
59 kafka.SendToChan(tailtask.Topic, line.Text)
60 default:
61 time.Sleep(time.Second)
62 }
63 }
64 }

taillog_mgr.go

  1 /**
2 * @Author: Mr.Cheng
3 * @Description:
4 * @File: taillogMgr
5 * @Version: 1.0.0
6 * @Date: 2021/12/14 下午3:47
7 */
8
9 package taillog
10
11 import (
12 "day21/02.log_agent/etcd"
13 "fmt"
14 "time"
15 )
16
17 type TailMgr struct {
18 logEntry []*etcd.LogEntry
19 tskMap map[string]*TailTask
20 newConfChan chan []*etcd.LogEntry
21 }
22
23 var tskMgr *TailMgr
24
25 // 循环每一个日志收集项,创建tailObj,并发往kafka
26 func Init(logEntryConf []*etcd.LogEntry) {
27 tskMgr = &TailMgr{
28 logEntry: logEntryConf,
29 tskMap: make(map[string]*TailTask, 16),
30 newConfChan: make(chan []*etcd.LogEntry),
31 }
32
33 for _, LogEntry := range logEntryConf {
34 // fmt.Printf("Path:%v Topic:%v\n", LogEntry.Path, LogEntry.Topic)
35 tailtask, err := NewTailTask(LogEntry.Path, LogEntry.Topic)
36 if err != nil {
37 continue
38 }
39 // 在tskMap中存储一下,以便发生配置变更时做增删改操作
40 key := fmt.Sprintf("%s_%s", tailtask.Path, tailtask.Topic)
41 tskMgr.tskMap[key] = tailtask
42 }
43
44 go tskMgr.run()
45 }
46
47 // 监听newConfChan是否有数据,有数据则表示etcd配置有变化,需做相应的处理
48 func (t *TailMgr) run() {
49 for {
50 select {
51 case newConf := <- t.newConfChan:
52 fmt.Printf("配置发生变更,Conf:%v\n", newConf)
53 // 找出新增项
54 for _, logEntry := range newConf {
55 key := fmt.Sprintf("%s_%s", logEntry.Path, logEntry.Topic)
56 _, ok := t.tskMap[key]
57 if ok {
58 // 表示该配置项原先存在
59 continue
60 } else {
61 // 属于新增配置
62 fmt.Printf("新增项,path:%s topic:%s\n", logEntry.Path, logEntry.Topic)
63 tailtask, err := NewTailTask(logEntry.Path, logEntry.Topic)
64 if err != nil {
65 continue
66 }
67 // TailMgr的logEntry和tskMap增加对应项
68 t.logEntry = append(t.logEntry, logEntry)
69 t.tskMap[key] = tailtask
70 go tailtask.ReadFromTail()
71 }
72 }
73 // 找出删除项
74 for index, c1 := range t.logEntry {
75 isDelete := true
76 for _, c2 := range newConf {
77 if c1.Path == c2.Path && c1.Topic == c2.Topic {
78 isDelete = false
79 break
80 }
81 }
82 if isDelete{
83 // 表示属于删除项,从tskMap拿出tailtask对象,执行对象的cancel函数,并将该对象从tskMap中删除
84 fmt.Printf("删除项,path:%s topic:%s\n", c1.Path, c1.Topic)
85 key := fmt.Sprintf("%s_%s", c1.Path, c1.Topic)
86 t.tskMap[key].cancel()
87 // TailMgr的logEntry和tskMap删除对应项
88 delete(t.tskMap, key)
89 t.logEntry = append(t.logEntry[:index], t.logEntry[index + 1:]...)
90 }
91 }
92 default:
93 time.Sleep(time.Second)
94 }
95 }
96 }
97
98 // 向外暴露newConfChan
99 func NewConfChan() chan<- []*etcd.LogEntry{
100 return tskMgr.newConfChan
101 }

main.go

 1 /**
2 * @Author: Mr.Cheng
3 * @Description:
4 * @File: main
5 * @Version: 1.0.0
6 * @Date: 2021/12/9 下午8:43
7 */
8
9 package main
10
11 import (
12 logAgentConfig "day21/02.log_agent/conf"
13 "day21/02.log_agent/etcd"
14 "day21/02.log_agent/kafka"
15 "day21/02.log_agent/taillog"
16 "fmt"
17 "gopkg.in/ini.v1"
18 "sync"
19 )
20
21 var (
22 cfg = new(logAgentConfig.AppConf)
23 wg sync.WaitGroup
24 )
25
26 func main() {
27 // 加载配置文件
28 err := ini.MapTo(cfg, "./conf/logAgent.ini")
29 if err != nil {
30 fmt.Printf("load ini failed, err:%v\n", err)
31 return
32 }
33
34 // 初始化kafka连接
35 err = kafka.Init([]string{cfg.KafkaConf.Address}, cfg.KafkaConf.Size)
36 if err != nil {
37 return
38 }
39 fmt.Println("init kafka success")
40
41 // 初始化etcd
42 err = etcd.Init(cfg.EtcdConf.Address, cfg.EtcdConf.Timeout)
43 if err != nil {
44 return
45 }
46 fmt.Println("init etcd success")
47
48 // 从etcd中获取日志收集项的配置信息
49 logEntryConf, err := etcd.GetConf(cfg.EtcdConf.Key)
50 if err != nil {
51 return
52 }
53 fmt.Printf("get conf from etcd success, conf:%v\n", logEntryConf)
54
55 // 收集日志发往kafka
56 // 循环每一个日志收集项,创建tailObj,并发往kafka
57 taillog.Init(logEntryConf)
58
59 // 监视etcd中配置的变动,如有变动,给新的配置信息给taillog
60 wg.Add(1)
61 go etcd.WatchConf(cfg.EtcdConf.Key, taillog.NewConfChan())
62 wg.Wait()
63 }

LogAgent —— etcd+kafka+zookeeper+go实现实时读取日志发送到kafka,并实现热加载配置读取的日志路径的更多相关文章

  1. Laravel核心解读--ENV的加载和读取

    Laravel在启动时会加载项目中的.env文件.对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的. 例如,你可能希望在本地使用测试的Mysql数据库而在上线后希望项目能够自动切换到生 ...

  2. 转载 Silverlight实用窍门系列:1.Silverlight读取外部XML加载配置---(使用WebClient读取XAP包同目录下的XML文件))

    转载:程兴亮文章,地址;http://www.cnblogs.com/chengxingliang/archive/2011/02/07/1949579.html 使用WebClient读取XAP包同 ...

  3. as3中xml文件的加载和读取

    ---恢复内容开始--- as代码如下: xml如下: 总结: 用URLReuqest对象加载xml的url 创建一个URLLoader对象,将1中的URLRequest指定给他 给URLLoader ...

  4. java 加载并读取Properties 文件

    1 .系统自带的application.properties  (以下代码仅供参考,不能粘贴复制) 假设application.properties文件有下面两个值: come.test.name = ...

  5. Silverlight实用窍门系列:1.Silverlight读取外部XML加载配置---(使用WebClient读取XAP包同目录下的XML文件))【附带实例源码】

    使用WebClient读取XAP包同目录下的XML文件 我们想要读取XAP包下面的XML文件,需要将此XML文件放在加载XAP包的网页的目录中去,然后使用URI方式读取此URL方式下的XML文件. 首 ...

  6. K8S学习笔记之使用Fluent-bit将容器标准输入和输出的日志发送到Kafka

    0x00 概述 K8S内部署微服务后,对应的日志方案是不落地方案,即微服务的日志不挂在到本地数据卷,所有的微服务日志都采用标准输入和输出的方式(stdin/stdout/stderr)存放到管道内,容 ...

  7. CentOS6.9安装Filebeat监控Nginx的访问日志发送到Kafka

    一.下载地址: 官方:https://www.elastic.co/cn/downloads/beats/filebeat 百度云盘:https://pan.baidu.com/s/1dvhqb0 二 ...

  8. 1. Spring基于xml加载和读取properties文件配置

    在src目录下,新建test.properties配置文件,内容如下 name=root password=123456 logArchiveCron=0/5 * * * * ? 一种是使用sprin ...

  9. Java读取Properties文件 Java加载配置Properties文件

    static{ Properties prop = new Properties(); prop.load(Thread.currentThread().getContextClassLoader() ...

  10. 可能是Asp.net Core On host、 docker、kubernetes(K8s) 配置读取的最佳实践

    写在前面 为了不违反广告法,我竭尽全力,不过"最佳实践"确是标题党无疑,如果硬要说的话 只能是个人最佳实践. 问题引出 ​ 可能很多新手都会遇到同样的问题:我要我的Asp.net ...

随机推荐

  1. LeetCode_788. 旋转数字

    写在前面 难度:简单 原文:https://leetcode-cn.com/problems/rotated-digits/ 题目 我们称一个数 X 为好数, 如果它的每位数字逐个地被旋转 180 度 ...

  2. python 动态导入库

    import sys sys.path.append("d:\\") ll = __import__("ll") if __name__ == '__main_ ...

  3. Nginx02 Nginx的的目录结构、基本工作原理、基本配置文件介绍

    1 Nginx目录结构 1.1 简要介绍 [root@localhost ~]# tree /usr/local/nginx /usr/local/nginx ├── client_body_temp ...

  4. Portainer功能使用之容器管理

    下载镜像 点击左边功能菜单栏[images]下载镜像 容器管理 点击左边功能菜单栏[Containers]创建.启动.重启.停止.监控等功能 创建容器 例如:安装nginx代理服务器,并设置容器信息( ...

  5. Vue前后端交互、生命周期、组件化开发

    目录 Vue前后端交互.生命周期.组件化开发 一.Vue用axios与后端交互 二.Vue的生命周期 三.组件化开发 Vue前后端交互.生命周期.组件化开发 一.Vue用axios与后端交互 ​ 如果 ...

  6. Vue3中的响应式api

    一.setup文件的认识 特点1:script 中间的内容就是一个对象 特点2:script 在第一层 定义的方法 或者 变量 => 就是这个对象 属性  => 顶层的绑定回被暴露给模板( ...

  7. python实现移动二级目录下的文件到一级目录

    python实现移动二级目录下的文件到一级目录 import os import shutil import sys def move_to_work_folder(work_path, cur_pa ...

  8. Can not use keyword ‘await’ outside an async function

  9. Java语法基础课程总结

    1.运行实例EnumTest.java并分析 结论:枚举类型属于引用类型,不属于原始数据类型它的每一个具体值都引用一个特定的对象,可以使用"=="直接比较枚举变量的值,枚举是可以从 ...

  10. chai 3D 之网格对象

    推荐:将 NSDT场景编辑器 加入你的3D开发工具链 介绍 网格对象是由三角形和顶点组成的形状   在 CHAI3D 中,多边形网格是定义多面体对象形状的顶点和三角形的集合.   顶点是一个位置以及其 ...