LogAgent —— etcd+kafka+zookeeper+go实现实时读取日志发送到kafka,并实现热加载配置读取的日志路径
工具包目录结构:
.
├── 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,并实现热加载配置读取的日志路径的更多相关文章
- Laravel核心解读--ENV的加载和读取
Laravel在启动时会加载项目中的.env文件.对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的. 例如,你可能希望在本地使用测试的Mysql数据库而在上线后希望项目能够自动切换到生 ...
- 转载 Silverlight实用窍门系列:1.Silverlight读取外部XML加载配置---(使用WebClient读取XAP包同目录下的XML文件))
转载:程兴亮文章,地址;http://www.cnblogs.com/chengxingliang/archive/2011/02/07/1949579.html 使用WebClient读取XAP包同 ...
- as3中xml文件的加载和读取
---恢复内容开始--- as代码如下: xml如下: 总结: 用URLReuqest对象加载xml的url 创建一个URLLoader对象,将1中的URLRequest指定给他 给URLLoader ...
- java 加载并读取Properties 文件
1 .系统自带的application.properties (以下代码仅供参考,不能粘贴复制) 假设application.properties文件有下面两个值: come.test.name = ...
- Silverlight实用窍门系列:1.Silverlight读取外部XML加载配置---(使用WebClient读取XAP包同目录下的XML文件))【附带实例源码】
使用WebClient读取XAP包同目录下的XML文件 我们想要读取XAP包下面的XML文件,需要将此XML文件放在加载XAP包的网页的目录中去,然后使用URI方式读取此URL方式下的XML文件. 首 ...
- K8S学习笔记之使用Fluent-bit将容器标准输入和输出的日志发送到Kafka
0x00 概述 K8S内部署微服务后,对应的日志方案是不落地方案,即微服务的日志不挂在到本地数据卷,所有的微服务日志都采用标准输入和输出的方式(stdin/stdout/stderr)存放到管道内,容 ...
- CentOS6.9安装Filebeat监控Nginx的访问日志发送到Kafka
一.下载地址: 官方:https://www.elastic.co/cn/downloads/beats/filebeat 百度云盘:https://pan.baidu.com/s/1dvhqb0 二 ...
- 1. Spring基于xml加载和读取properties文件配置
在src目录下,新建test.properties配置文件,内容如下 name=root password=123456 logArchiveCron=0/5 * * * * ? 一种是使用sprin ...
- Java读取Properties文件 Java加载配置Properties文件
static{ Properties prop = new Properties(); prop.load(Thread.currentThread().getContextClassLoader() ...
- 可能是Asp.net Core On host、 docker、kubernetes(K8s) 配置读取的最佳实践
写在前面 为了不违反广告法,我竭尽全力,不过"最佳实践"确是标题党无疑,如果硬要说的话 只能是个人最佳实践. 问题引出 可能很多新手都会遇到同样的问题:我要我的Asp.net ...
随机推荐
- Java入门与进阶P-5.3+P-5.4
数组的元素 有效的下标 最小的下标是0,最大的下标是数组的元素个数-1 可是编译器不会检查看你是否用了有效的下标 但是如果运行的时候出现了无效的下标,可能会导致程序终止 定义数组变量 元素个数必须是整 ...
- QtCharts模块勾画折线和曲线图
QtCharts画线图主要三个部分组成 QLIneSeries或QSplineSeries用于保存联系的坐标位置数据,QChart用于管理图像显示,例如图例,坐标主题等,QChartView则用于显示 ...
- Hive删除分区名称中含有特殊字符
先说方案:通过show partitions和hdfs url看到的都不是真正的分区名称,都是经过URI重新编码的,访问这些分区应该使用分区名称的原始字符串. 场景描述 当我们在SQL语句中使用变量时 ...
- springboot集成ElasticApm
jvm参数方式: -javaagent:D:/codesoft/elastic-apm-agent-1.18.0.jar -Delastic.apm.service_name=my-applicati ...
- Cubase11/12 安装破解图文教程 【2022年12月29日亲测有效】
Cubase11/12安装破解图文教程 下载安装包工具 Cubase官网:点击官网进行下载 Cubase11/12工具包:点击立即下载 Cubase12完成破解教程:点击立即查看 安装Cubase11 ...
- webrtc QOS笔记一 Neteq直方图算法浅读
webrtc QOS笔记一 Neteq直方图算法浅读 目录 webrtc QOS笔记一 Neteq直方图算法浅读 Histogram Algorithm 获取目标延迟 遗忘因子曲线 想起博客园帐号了, ...
- WAF Bypass 介绍与实战
前言 WAF是英文"Web Application Firewall"的缩写,中文意思是"Web应用防火墙",也称为"网站应用级入侵防御系统" ...
- 视觉SLAM:VIO的误差和误差雅可比矩阵
1.两个相机之间的非线性优化 观测相机方程关于相机位姿与特征点的雅可比矩阵: 1.1 位姿: 1.2 3D特征点 fx,fy,fz为相机内参 X',Y',Z'为3D点在相机坐标系下的坐标 该误差是观测 ...
- Mockito单元测试 初试
Mockito单元测试相对于Spring Boot 自带的好处理在于,单元测试不需要加载注入Spring Boot 启动项目. 1.需要注入的东西如下,@InjectMocks是注入需要测试的类,@S ...
- 郁金香5 分析游戏内npc 数据
004D4BAE | CC | int3 | 004D4BAF | CC | int3 | 004D4BB0 | 55 | push ebp | 004D4BB1 | 8BEC | mov ebp,e ...