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 ...
随机推荐
- WPF开发经验-实现一种三轴机械手控件
一 引入 考虑实现一种三轴机器人控件. 三轴机器人用来将某种工件从一个位置运送到另一个位置. 其X轴为手臂轴,可以正向和反向运动,它处于末端,直接接触工件: 其T轴为旋转轴,可以对手臂进行旋转: 其Z ...
- 结构型模式 - 外观模式Facade
1.tm的NT审核机制,满篇文章哪来的广告? 就算有也是你们自己加的吧?等财富能支持我自己的网站后,就是和你们说再见之时. 2.tm第二遍说,我接着提交,这个审核机制的傻逼设计者或者是程序敲出来的bu ...
- 【Rust学习】内存安全探秘:变量的所有权、引用与借用
作者:京东零售 周凯 一.前言 Rust 语言由 Mozilla 开发,最早发布于 2014 年 9 月,是一种高效.可靠的通用高级语言.其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有 ...
- 真正“搞”懂HTTP协议14之HTTP3
我们前一篇学习了HTTP/2,相比于HTTP/1,HTTP/2在性能上有了大幅的改进,但是HTTP/2因为底层还是基于TCP协议的,虽然HTTP/2在应用层引入了流的概念,利用多路复用解决了队头阻塞的 ...
- 5步带你入门GaussDB(DWS)的GDS导入导出
摘要:本篇文档为使用GDS导入示例的具体简单步骤和示例. 本文分享自华为云社区<带你快速入门GDS导入导出,玩转PB级数仓GaussDB(DWS)>,作者: yd_220527686. 1 ...
- JavaScript数组的方法大全(最新)
JavaScript数组方法大全 趁着有时间,总结了下数组所有的属性和方法,记录博客,便于后续使用 array.at() at方法,用于获取数组中,对应索引位置的值,不能修改. 语法:array.at ...
- Servlet一笔记
Servlet一笔记 一.web相关概念 1. 软件架构 目标 理解B/S 和 C/S的优缺点 讲解 C/S架构 C:Client,客户端:S:Server,服务器 比如:QQ,微信,网游 优点: 显 ...
- orm中多表查询示例
record = session.query(OrderMain, OrderGoods).join(OrderMain, OrderMain.order_code == OrderGoods.ord ...
- Django models.py 表的参数选择
from django.db import models # Create your models here. class Department(models.Model): # 以后可以新增, ...
- Mysql习题系列(三):单行函数
案例数据 提取码:2rd5 #[题目] # 1.显示系统时间(注:日期+时间) # 2.查询员工号,姓名,工资,以及工资提高百分之20%后的结果(new salary) # 3.将员工的姓名按首字母排 ...