实现功能

  • logagent根据etcd的配置创建多个tailtask

  • logagent实现watch新配置

  • logagent实现新增收集任务

  • logagent删除新配置中没有的那个任务

  • logagent根据IP拉取自己的配置

代码实现

  • config/config.ini

    [kafka]
    address=127.0.0.1:9092
    chan_max_size=100000

    [etcd]
    address=127.0.0.1:2379
    timeout=5
    collect_log_key=/logagent/%s/collect_config
  • config/config.go

    package conf

    type Config struct {
    Kafka Kafka `ini:"kafka"`
    Etcd Etcd `ini:"etcd"`
    }

    type Kafka struct {
    Address string `ini:"address"`
    ChanMaxSize int `ini:"chan_max_zise"`
    }

    type Etcd struct {
    Address string `ini:"address"`
    Key string `ini:"collect_log_key"`
    Timeout int `ini:"timeout"`
    }
  • main.go

    package main

    import (
    "fmt"
    "gopkg.in/ini.v1"
    "logagent/conf"
    "logagent/etcd"
    "logagent/kafka"
    "logagent/taillog"
    "logagent/tools"
    "strings"
    "sync"
    "time"
    )

    var config = new(conf.Config)

    // logAgent 入口程序

    func main() {
    // 0. 加载配置文件
    err := ini.MapTo(config, "./conf/config.ini")
    if err != nil {
    fmt.Printf("Fail to read file: %v", err)
    return
    }
    // 1. 初始化kafka连接
    err = kafka.Init(strings.Split(config.Kafka.Address, ";"), config.Kafka.ChanMaxSize)
    if err != nil {
    fmt.Println("init kafka failed, err:%v\n", err)
    return
    }
    fmt.Println("init kafka success.")

    // 2. 初始化etcd
    err = etcd.Init(config.Etcd.Address, time.Duration(config.Etcd.Timeout) * time.Second)
    if err != nil {
    fmt.Printf("init etcd failed,err:%v\n", err)
    return
    }
    fmt.Println("init etcd success.")
    // 实现每个logagent都拉取自己独有的配置,所以要以自己的IP地址实现热加载
    ip, err := tools.GetOurboundIP()
    if err != nil {
    panic(err)
    }
    etcdConfKey := fmt.Sprintf(config.Etcd.Key, ip)
    // 2.1 从etcd中获取日志收集项的配置信息
    logEntryConf, err := etcd.GetConf(etcdConfKey)
    if err != nil {
    fmt.Printf("etcd.GetConf failed, err:%v\n", err)
    return
    }
    fmt.Printf("get conf from etcd success, %v\n", logEntryConf)

    // 2.2 派一个哨兵 一直监视着 zhangyafei这个key的变化(新增 删除 修改))
    for index, value := range logEntryConf{
    fmt.Printf("index:%v value:%v\n", index, value)
    }
    // 3. 收集日志发往kafka
    taillog.Init(logEntryConf)

    var wg sync.WaitGroup
    wg.Add(1)
    go etcd.WatchConf(etcdConfKey, taillog.NewConfChan()) // 哨兵发现最新的配置信息会通知上面的通道
    wg.Wait()
    }
  • kafka/kafka.go

    package kafka

    import (
    "fmt"
    "github.com/Shopify/sarama"
    )
    // 专门往kafka写日志的模块

    type LogData struct {
    topic string
    data string
    }

    var (
    client sarama.SyncProducer // 声明一个全局的连接kafka的生产者client
    logDataChan chan *LogData
    )

    // init初始化client
    func Init(addrs []string, chanMaxSize int) (err error) {
    config := sarama.NewConfig()

    config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认
    config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出⼀个partition
    config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回

    // 连接kafka
    client, err = sarama.NewSyncProducer(addrs, config)
    if err != nil {
    fmt.Println("producer closed, err:", err)
    return
    }
    // 初始化logDataChan
    logDataChan = make(chan *LogData, chanMaxSize)
    // 开启后台的goroutine,从通道中取数据发往kafka
    go SendToKafka()
    return
    }

    // 给外部暴露的一个函数,噶函数只把日志数据发送到一个内部的channel中
    func SendToChan(topic, data string) {
    msg := &LogData{
    topic: topic,
    data: data,
    }
    logDataChan <- msg
    }

    // 真正往kafka发送日志的函数
    func SendToKafka() {
    for {
    select {
    case log_data := <- logDataChan:
    // 构造一个消息
    msg := &sarama.ProducerMessage{}
    msg.Topic = log_data.topic
    msg.Value = sarama.StringEncoder(log_data.data)
    // 发送到kafka
    pid, offset, err := client.SendMessage(msg)
    if err != nil{
    fmt.Println("sned msg failed, err:", err)
    }
    fmt.Printf("send msg success, pid:%v offset:%v\n", pid, offset)
    //fmt.Println("发送成功")
    }
    }

    }
  • etcd/etcd.go

    package etcd

    import (
    "context"
    "encoding/json"
    "fmt"
    "go.etcd.io/etcd/clientv3"
    "strings"
    "time"
    )

    var (
    cli *clientv3.Client
    )

    type LogEntry struct {
    Path string `json:"path"` // 日志存放的路径
    Topic string `json:"topic"` // 日志发往kafka中的哪个Topic
    }

    // 初始化etcd的函数

    func Init(addr string, timeout time.Duration) (err error) {
    cli, err = clientv3.New(clientv3.Config{
    Endpoints: strings.Split(addr, ";"),
    DialTimeout: timeout,
    })
    return
    }

    // 从etcd中获取日志收集项的配置信息
    func GetConf(key string) (logEntryConf []*LogEntry, err error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    resp, err := cli.Get(ctx, key)
    cancel()
    if err != nil {
    fmt.Printf("get from etcd failed, err:%v\n", err)
    return
    }
    for _, ev := range resp.Kvs {
    //fmt.Printf("%s:%s\n", ev.Key, ev.Value)
    err = json.Unmarshal(ev.Value, &logEntryConf)
    if err != nil {
    fmt.Printf("unmarshal etcd value failed,err:%v\n", err)
    return
    }
    }
    return
    }

    // etcd watch
    func WatchConf(key string, newConfChan chan<- []*LogEntry) {
    rch := cli.Watch(context.Background(), key) // <-chan WatchResponse
    // 从通道尝试取值(监视的信息)
    for wresp := range rch {
    for _, ev := range wresp.Events {
    fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
    // 通知taillog.taskMgr
    var newConf []*LogEntry
    //1. 先判断操作的类型
    if ev.Type != clientv3.EventTypeDelete {
    // 如果不是是删除操作
    err := json.Unmarshal(ev.Kv.Value, &newConf)
    if err != nil {
    fmt.Printf("unmarshal failed, err:%v\n", err)
    continue
    }
    }

    fmt.Printf("get new conf: %v\n", newConf)
    newConfChan <- newConf
    }
    }
    }
  • taillog/taillog.go

    package taillog

    import (
    "context"
    "fmt"
    "github.com/hpcloud/tail"
    "logagent/kafka"
    )

    // 专门收集日志的模块


    type TailTask struct {
    path string
    topic string
    instance *tail.Tail
    // 为了能实现退出r,run()
    ctx context.Context
    cancelFunc context.CancelFunc
    }

    func NewTailTask(path, topic string) (t *TailTask) {
    ctx, cancel := context.WithCancel(context.Background())
    t = &TailTask{
    path: path,
    topic: topic,
    ctx: ctx,
    cancelFunc: cancel,
    }
    err := t.Init()
    if err != nil {
    fmt.Println("tail file failed, err:", err)
    }
    return
    }

    func (t TailTask) Init() (err error) {
    config := tail.Config{
    ReOpen: true, // 充新打开
    Follow: true, // 是否跟随
    Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件哪个地方开始读
    MustExist: false, // 文件不存在不报错
    Poll: true}
    t.instance, err = tail.TailFile(t.path, config)
    // 当goroutine执行的函数退出的时候,goriutine就退出了
    go t.run() // 直接去采集日志发送到kafka
    return
    }

    func (t *TailTask) run() {
    for {
    select {
    case <- t.ctx.Done():
    fmt.Printf("tail task:%s_%s 结束了...\n", t.path, t.topic)
    return
    case line :=<- t.instance.Lines: // 从TailTask的通道中一行一行的读取日志
    // 3.2 发往kafka
    fmt.Printf("get log data from %s success, log:%v\n", t.path, line.Text)
    kafka.SendToChan(t.topic, line.Text)
    }
    }
    }
  • taillog/taillog_mgr

    package taillog

    import (
    "fmt"
    "logagent/etcd"
    "time"
    )

    var taskMrg *TailLogMgr

    type TailLogMgr struct {
    logEntry []*etcd.LogEntry
    taskMap map[string]*TailTask
    newConfChan chan []*etcd.LogEntry
    }

    func Init(logEntryConf []*etcd.LogEntry) {
    taskMrg = &TailLogMgr{
    logEntry: logEntryConf,
    taskMap: make(map[string]*TailTask, 16),
    newConfChan: make(chan []*etcd.LogEntry), // 无缓冲区的通道
    }
    for _, logEntry := range logEntryConf{
    // 3.1 循环每一个日志收集项,创建TailObj
    // logEntry.Path 要收集的全日志文件的路径
    // 初始化的时候齐了多少个tailTask 都要记下来,为了后续判断方便
    tailObj := NewTailTask(logEntry.Path, logEntry.Topic)
    mk := fmt.Sprintf("%s_%s", logEntry.Path, logEntry.Topic)
    taskMrg.taskMap[mk] = tailObj
    }
    go taskMrg.run()
    }

    // 监听自己的newConfChan,有了新的配合过来之后就做对应的处理

    func (t *TailLogMgr) run() {
    for {
    select {
    case newConf := <- t.newConfChan:
    // 1. 配置新增
    for _, conf := range newConf {
    mk := fmt.Sprintf("%s_%s", conf.Path, conf.Topic)
    _, ok := t.taskMap[mk]
    if ok {
    // 原来就有,不需要操作
    continue
    }else {
    // 新增的
    tailObj := NewTailTask(conf.Path, conf.Topic)
    t.taskMap[mk] = tailObj
    }
    }
    // 找出原来t.logEntry有,但是newConf中没有的,删掉
    for _, c1 := range t.logEntry{ // 循环原来的配置
    isDelete := true
    for _, c2 := range newConf{ // 取出新的配置
    if c2.Path == c1.Path && c2.Topic == c1.Topic {
    isDelete = false
    continue
    }
    }
    if isDelete {
    // 把c1对应的这个tailObj给停掉
    mk := fmt.Sprintf("%s_%s", c1.Path, c1.Topic)
    // t.taskNap[mk] ==> tailObj
    t.taskMap[mk].cancelFunc()
    }
    }
    // 2. 配置删除
    // 3. 配置变更
    fmt.Println("新的配置来了!", newConf)
    default:
    time.Sleep(time.Second)
    }
    }
    }

    // 一个函数,向外暴露taskMgr的newConfChan
    func NewConfChan() chan <-[]*etcd.LogEntry {
    return taskMrg.newConfChan
    }
  • tools/get_ip

    package tools

    import (
    "net"
    "strings"
    )

    // 获取本地对外IP
    func GetOurboundIP() (ip string, err error) {
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
    return
    }
    defer conn.Close()
    localAddr := conn.LocalAddr().(*net.UDPAddr)
    //fmt.Println(localAddr.String())
    ip = strings.Split(localAddr.IP.String(), ":")[0]
    return
    }

三. 连接kafka进行消费

  • 将收集项配置放入etcd

    package main

    import (
    "context"
    "fmt"
    "net"
    "strings"
    "time"

    "go.etcd.io/etcd/clientv3"
    )

    // 获取本地对外IP
    func GetOurboundIP() (ip string, err error) {
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
    return
    }
    defer conn.Close()
    localAddr := conn.LocalAddr().(*net.UDPAddr)
    fmt.Println(localAddr.String())
    ip = strings.Split(localAddr.IP.String(), ":")[0]
    return
    }

    func main() {
    // etcd client put/get demo
    // use etcd/clientv3
    cli, err := clientv3.New(clientv3.Config{
    Endpoints: []string{"127.0.0.1:2379"},
    DialTimeout: 5 * time.Second,
    })
    if err != nil {
    // handle error!
    fmt.Printf("connect to etcd failed, err:%v\n", err)
    return
    }
    fmt.Println("connect to etcd success")
    defer cli.Close()
    // put
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    value := `[{"path":"f:/tmp/nginx.log","topic":"web_log"},{"path":"f:/tmp/redis.log","topic":"redis_log"},{"path":"f:/tmp/mysql.log","topic":"mysql_log"}]`
    //value := `[{"path":"f:/tmp/nginx.log","topic":"web_log"},{"path":"f:/tmp/redis.log","topic":"redis_log"}]`
    //_, err = cli.Put(ctx, "zhangyafei", "dsb")

    //初始化key
    ip, err := GetOurboundIP()
    if err != nil {
    panic(err)
    }
    log_conf_key := fmt.Sprintf("/logagent/%s/collect_config", ip)
    _, err = cli.Put(ctx, log_conf_key, value)

    //_, err = cli.Put(ctx, "/logagent/collect_config", value)
    cancel()
    if err != nil {
    fmt.Printf("put to etcd failed, err:%v\n", err)
    return
    }
    // get
    ctx, cancel = context.WithTimeout(context.Background(), time.Second)

    resp, err := cli.Get(ctx, log_conf_key)
    //resp, err := cli.Get(ctx, "/logagent/collect_config")
    cancel()
    if err != nil {
    fmt.Printf("get from etcd failed, err:%v\n", err)
    return
    }
    for _, ev := range resp.Kvs {
    fmt.Printf("%s:%s\n", ev.Key, ev.Value)
    }
    }
  • 消费者代码

    package main

    import (
    "fmt"
    "github.com/Shopify/sarama"
    )

    // 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
    }
    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)
    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()
    // 异步从每个分区消费信息
    go func(sarama.PartitionConsumer) {
    for msg := range pc.Messages() {
    fmt.Printf("Partition:%d Offset:%d Key:%s Value:%s\n", msg.Partition, msg.Offset, msg.Key, msg.Value)
    }
    }(pc)
    }
    select {}
    }
  • 运行步骤

    1. 开启zookeeper

    2. 开启kafka

    3. 开启etcd

    4. 设置收集项配置到etcd

    5. 运行logagent从etcd加载收集项配置,使用taillog监听日志文件内容,将新增的日志内容发往kafka

    6. 连接kafka进行消费

    7. 添加日志内容,观察logagent生产和kafka消费状态

 项目地址:https://gitee.com/zhangyafeii/go-log-collect

日志收集系统系列(四)之LogAgent优化的更多相关文章

  1. 日志收集系统系列(三)之LogAgent

    一.什么是LogAhent 类似于在linux下通过tail的方法读日志文件,将读取的内容发给kafka,这里的tailf是可以动态变化的,当配置文件发生变化时,可以通知我们程序自动增加需要增加的配置 ...

  2. 日志收集系统系列(五)之LogTransfer

    从kafka里面把日志取出来,写入ES,使用Kibana做可视化展示 1. ElasticSearch 1.1 介绍 Elasticsearch(ES)是一个基于Lucene构建的开源.分布式.RES ...

  3. 基于Flume的美团日志收集系统(二)改进和优化

    在<基于Flume的美团日志收集系统(一)架构和设计>中,我们详述了基于Flume的美团日志收集系统的架构设计,以及为什么做这样的设计.在本节中,我们将会讲述在实际部署和使用过程中遇到的问 ...

  4. 基于Flume的美团日志收集系统 架构和设计 改进和优化

    3种解决办法 https://tech.meituan.com/mt-log-system-arch.html 基于Flume的美团日志收集系统(一)架构和设计 - https://tech.meit ...

  5. [转载] 一共81个,开源大数据处理工具汇总(下),包括日志收集系统/集群管理/RPC等

    原文: http://www.36dsj.com/archives/25042 接上一部分:一共81个,开源大数据处理工具汇总(上),第二部分主要收集整理的内容主要有日志收集系统.消息系统.分布式服务 ...

  6. 一共81个,开源大数据处理工具汇总(下),包括日志收集系统/集群管理/RPC等

    作者:大数据女神-诺蓝(微信公号:dashujunvshen).本文是36大数据专稿,转载必须标明来源36大数据. 接上一部分:一共81个,开源大数据处理工具汇总(上),第二部分主要收集整理的内容主要 ...

  7. GO学习-(33) Go实现日志收集系统2

    Go实现日志收集系统2   一篇文章主要是关于整体架构以及用到的软件的一些介绍,这一篇文章是对各个软件的使用介绍,当然这里主要是关于架构中我们agent的实现用到的内容 关于zookeeper+kaf ...

  8. 用fabric部署维护kle日志收集系统

    最近搞了一个logstash kafka elasticsearch kibana 整合部署的日志收集系统.部署参考lagstash + elasticsearch + kibana 3 + kafk ...

  9. 基于Flume的美团日志收集系统(一)架构和设计

    美团的日志收集系统负责美团的所有业务日志的收集,并分别给Hadoop平台提供离线数据和Storm平台提供实时数据流.美团的日志收集系统基于Flume设计和搭建而成. <基于Flume的美团日志收 ...

随机推荐

  1. 代码图形统计工具git_stats web

    目录 一.简介 二.安装ruby 三.配置git_stats 四.通过nginx把网页展示出来 一.简介 仓库代码统计工具之一,可以按git提交人.提交次数.修改文件数.代码行数.注释量在时间维度上进 ...

  2. 搞IT的应届生如何写好简历?

    本人在互联网大厂和外企做过技术面试官,也有过校招和招聘应届毕业生的经验,所以自认为在这个问题上有一定的发言权.   应届毕业生(其实其他求职者也一样)首先要知道,面试官凭什么决定这份简历有面试机会?而 ...

  3. Numpy.frompyfunc()将计算单个值的函数转化为计算数组中每个元素的函数

    Numpy.frompyfunc()将计算单个值的函数转化为计算数组中每个元素的函数 不再通过遍历,对数组中的元素进行运算,利用frompyfunc()将计算单个值的函数转化为计算数组中每个元素的函数 ...

  4. git 省略 commit message

    每次提交使用 git commit --allow-empty-message --no-edit 也可以设置命令别名 git config --global alias.nocommit " ...

  5. LuoguP7869 「Wdoi-4」使用三个系统程度的能力 题解

    Content 现在有一个转换后的文本文件,以一个长度为 \(n\) 的字符串表示.请判断这个文件是用哪一种写的,详情请返回题面. 数据范围:\(n\leqslant 10^5\).字符串里面至少有一 ...

  6. LuoguB2101 计算矩阵边缘元素之和 题解

    Content 给定一个 \(m\times n\) 的矩阵,求矩阵边缘元素之和. 数据范围:\(1\leqslant m,n\leqslant 100\). Solution 对于新手来说,看到这题 ...

  7. 重学c#系列——string.empty 和 "" 还有null[二十]

    前言 简单整理一下string.empty 和 "" 还有 null的区别. 正文 首先null 和 string.empty 还有 "" 是不一样的. nul ...

  8. flink使用命令开始、停止任务

    命令操作 进行flink的安装目录 动态上传jar包启动job ./bin/flink run -c com.test.CountMain -P 3 Test-1. 0-SNAPSHOT.jar -- ...

  9. 重学c#系列——datetime 和 datetimeoffset[二十一]

    前言 简单介绍一下datetime和 datetimeoffset. 正文 了解一个国家的文化,就要了解一个国家的历史. 要了解datetimeoffset,那么很有必要了解一下datetime. 表 ...

  10. 再谈多线程模型之生产者消费者(单一生产者和多消费者 )(c++11实现)

    0.关于 为缩短篇幅,本系列记录如下: 再谈多线程模型之生产者消费者(基础概念)(c++11实现) 再谈多线程模型之生产者消费者(单一生产者和单一消费者)(c++11实现) 再谈多线程模型之生产者消费 ...