Golang實戰之海量日誌收集系統(三)簡單版本logAgent的實現

目錄:

GitHub項目地址https://github.com/PlutoaCharon/Golang_logCollect

Golang實戰之海量日誌收集系統(一)項目背景介紹

Golang實戰之海量日誌收集系統(二)收集應用程序日誌到Kafka中

Golang實戰之海量日誌收集系統(三)簡單版本logAgent的實現

Golang實戰之海量日誌收集系統(四)etcd介紹與使用etcd獲取配置信息

Golang實戰之海量日誌收集系統(五)根據etcd配置項創建多個tailTask

Golang實戰之海量日誌收集系統(六)監視etcd配置項的變更

Golang實戰之海量日誌收集系統(七)logTransfer之從kafka中獲取日誌信息

Golang實戰之海量日誌收集系統(八)logTransfer之將日誌入庫到Elasticsearch並通過Kibana進行展示

簡單版本LogAgent的實現

這裏主要是先實現核心的功能,後續再做優化和改進,主要實現能夠根據配置文件中配置的日誌路徑去讀取日誌並將讀取的實時推送到kafka消息隊列中

關於logagent的主要結構如下:
在這裏插入圖片描述

	.
	├─conf
	│      logagent.conf
	│
	├─kafka
	│      kafka.go
	│
	├─logs
	│      my.log
	│
	├─main
	│      config.go
	│      log.go
	│      main.go
	│      server.go
	│
	├─tailf
	│      tail.go
	│  go.mod
	└─ go.sum

現在使用tail庫能讀取到日誌,使用sarama庫能到推送消息到kafka,我們結合這兩個庫,實現一邊讀取文件日誌,一遍寫入到kafka

logagent.conf :配置文件
my.log:產生的日誌文件
config.go:用於初始化讀取配置文件中的內容,這裏的配置文件加載是通過之前自己實現的配置文件熱加載包處理的
kafka.go:對kafka的操作,包括初始化kafka連接,以及給kafka發送消息
server.go:主要是tail 的相關操作,用於去讀日誌文件並將內容放到channel中
log.go:日誌的處理與序列化
tail.go: 用於去讀日誌文件
main.go: 初始化入口文件,與執行server的入口函數

LogAgent的初步框架實現

現在使用tail庫能讀取到日誌,使用sarama庫能到推送消息到kafka,我們結合這兩個庫,實現一邊讀取文件日誌,一遍寫入到kafka

新建kafka/kafka.go和taillog/tail.go,分別先建立一個初始化函數

kafka/kafka.go

package kafka

import (
	"fmt"

	"github.com/Shopify/sarama"
)

var (
    client sarama.SyncProducer
)

func Init(addrs []string) (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([]string{addrs}, config)
	if err != nil {
		fmt.Println("producer closed, err:", err)
		return
	}
	return
}

tail/tail.go

package tail

import (
	"fmt"

	"github.com/hpcloud/tail"
)

var (
	tailObj *tail.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 {
		fmt.Println("tail file failed, err:", err)
		return
	}
	return
}

main.go

package main

import (
	"fmt"
	"logAgent/kafka"
	"logAgent/taillog"
)

func main() {
	// 1.初始化kafka
	err := kafka.Init([]string{"127.0.0.1:29092"})
	if err != nil {
		fmt.Printf("init kafka failed ,err:%v\n", err)
		return
	}
	fmt.Println("init kafka success")
	// 2.初始化taillog
	err = taillog.Init("./my.log")
	if err != nil {
		fmt.Printf("init taillog failed, err:%v\n", err)
		return
	}
	fmt.Println("init taillog success")
}

都初始化之後,就是怎麼將日誌發給kafka了

tail/tail.go中創建一個ReadChan函數

func ReadChan() <-chan *tail.Line {
	return tailObj.Lines
}

kafka/kafka.go中創建一個SendToKafka的函數,該函數接收從外部提供的topic和data參數

func SendToKafka(topic, data string) {
	// 構造⼀個消息
	msg := &sarama.ProducerMessage{}
	msg.Topic = topic
	msg.Value = sarama.StringEncoder(data)

	// 發送消息
	pid, offset, err := client.SendMessage(msg)
	if err != nil {
		fmt.Printf("send msg failed, err: %v\n", err)
		return
	}
	fmt.Printf("pid:%v offset:%v\n", pid, offset)
}

在main.go中創建run函數,執行具體的任務,並在main函數中調用它

func run() {
	for {
		select {
		case line := <-taillog.ReadChan():
			kafka.SendToKafka("web_log", line.Text)
		default:
			time.Sleep(time.Millisecond * 500)
		}
	}
}

往my.log中寫入一點數據進行測試

LogAgent的初步框架改進

通過github.com/astaxie/beego/logs解析配置文件, 將所有的配置信息寫入logagent.conf

logagent.conf

[logs]
log_level = debug
log_path = E:\\Go\\logagent\\logs\\my.log

[collect]
log_path = E:\\Go\\logagent\\logs\\my.log
topic = nginx_log
chan_size = 100

[kafka]
server_addr = 0.0.0.0:9092

引入完整代碼:

main.go

主要功能是初始化配置

package main

import (
	"fmt"
	"github.com/astaxie/beego/logs"
	"logagent/kafka"
	"logagent/tailf"
)

func main() {

	fmt.Println("開始")
	// 讀取初始化配置文件
	filename := "E:\\Go\\logagent\\conf\\logagent.conf"
	err := loadInitConf("ini", filename)
	if err != nil {
		fmt.Printf("導入配置文件錯誤:%v\n", err)
		panic("導入配置文件錯誤")
		return
	}

	// 初始化日誌信息
	err = initLogger()
	if err != nil {
		fmt.Printf("導入日誌文件錯誤:%v\n", err)
		panic("導入日誌文件錯誤")
		return
	}
	// 輸出成功信息
	logs.Debug("導入日誌成功%v", logConfig)

	// 初始化tailf
	err = tailf.InitTail(logConfig.CollectConf, logConfig.chanSize)
	if err != nil {
		logs.Error("初始化tailf失敗:", err)
		return
	}
	logs.Debug("初始化tailf成功!")

	// 初始化Kafka
	err = kafka.InitKafka(logConfig.KafkaAddr)
	if err != nil {
		logs.Error("初識化kafka producer失敗:", err)
		return
	}
	logs.Debug("初始化Kafka成功!")

	// 運行
	err = serverRun()
	if err != nil {
		logs.Error("serverRun failed:", err)
	}
	logs.Info("程序退出")
}

config.go
導入logagent.conf的配置信息

package main

import (
	"errors"
	"fmt"
	"github.com/astaxie/beego/config"
	"logagent/tailf"
)

var (
	logConfig *Config
)

// 日誌配置
type Config struct {
	logLevel    string
	logPath     string
	chanSize    int
	KafkaAddr   string
	CollectConf []tailf.CollectConf
}

// 日誌收集配置
func loadCollectConf(conf config.Configer) (err error) {
	var c tailf.CollectConf

	c.LogPath = conf.String("collect::log_path")
	if len(c.LogPath) == 0 {
		err = errors.New("無效的 collect::log_path ")
		return
	}

	c.Topic = conf.String("collect::topic")
	if len(c.Topic) == 0 {
		err = errors.New("無效的 collect::topic ")
		return
	}

	logConfig.CollectConf = append(logConfig.CollectConf, c)
	return
}

// 導入初始化配置
func loadInitConf(confType, filename string) (err error) {
	conf, err := config.NewConfig(confType, filename)
	if err != nil {
		fmt.Printf("初始化配置文件出錯:%v\n", err)
		return
	}
	// 導入配置信息
	logConfig = &Config{}
	// 日誌級別
	logConfig.logLevel = conf.String("logs::log_level")
	if len(logConfig.logLevel) == 0 {
		logConfig.logLevel = "debug"
	}
	// 日誌輸出路徑
	logConfig.logPath = conf.String("logs::log_path")
	if len(logConfig.logPath) == 0 {
		logConfig.logPath = "E:\\Go\\logagent\\logs\\my.log"
	}

	// 管道大小
	logConfig.chanSize, err = conf.Int("collect::chan_size")
	if err != nil {
		logConfig.chanSize = 100
	}

	// Kafka
	logConfig.KafkaAddr = conf.String("kafka::server_addr")
	if len(logConfig.KafkaAddr) == 0 {
		err = fmt.Errorf("初識化Kafka失敗")
		return
	}

	err = loadCollectConf(conf)
	if err != nil {
		fmt.Printf("導入日誌收集配置錯誤:%v", err)
		return
	}
	return
}

log.go

解析日誌

package main

import (
	"encoding/json"
	"fmt"
	"github.com/astaxie/beego/logs"
)

func convertLogLevel(level string) int {

	switch level {
	case "debug":
		return logs.LevelDebug
	case "warn":
		return logs.LevelWarn
	case "info":
		return logs.LevelInfo
	case "trace":
		return logs.LevelTrace
	}
	return logs.LevelDebug
}

func initLogger() (err error) {

	config := make(map[string]interface{})
	config["filename"] = logConfig.logPath
	config["level"] = convertLogLevel(logConfig.logLevel)
	configStr, err := json.Marshal(config)
	if err != nil {
		fmt.Println("初始化日誌, 序列化失敗:", err)
		return
	}
	_ = logs.SetLogger(logs.AdapterFile, string(configStr))

	return
}

tail.go

定義TailObjMgr結構體, 將tail監控到的配置消息通過tailObjMgr.msgChan <- textMsg放入管道中

package tailf

import (
	"fmt"
	"github.com/astaxie/beego/logs"
	"github.com/hpcloud/tail"
	"time"
)

// 將日誌收集配置放在tailf包下,方便其他包引用
type CollectConf struct {
	LogPath string
	Topic   string
}

// 存入Collect
type TailObj struct {
	tail *tail.Tail
	conf CollectConf
}

// 定義Message信息
type TextMsg struct {
	Msg   string
	Topic string
}

// 管理系統所有tail對象
type TailObjMgr struct {
	tailsObjs []*TailObj
	msgChan   chan *TextMsg
}

// 定義全局變量
var (
	tailObjMgr *TailObjMgr
)

func GetOneLine() (msg *TextMsg) {
	msg = <- tailObjMgr.msgChan
	return
}

func InitTail(conf []CollectConf, chanSize int) (err error) {

	// 加載配置項
	if len(conf) == 0 {
		err = fmt.Errorf("無效的log collect conf:%v", conf)
		return
	}
	tailObjMgr = &TailObjMgr{
		msgChan: make(chan *TextMsg, chanSize), // 定義Chan管道
	}
	// 循環導入
	for _, v := range conf {
		// 初始化Tail
		fmt.Println(v)
		tails, errTail := tail.TailFile(v.LogPath, tail.Config{
			ReOpen:    true,
			Follow:    true,
			Location:  &tail.SeekInfo{Offset: 0, Whence: 2},
			MustExist: false,
			Poll:      true,
		})
		if errTail != nil {
			err = errTail
			fmt.Println("tail 操作文件錯誤:", err)
			return
		}
		// 導入配置項
		obj := &TailObj{
			conf: v,
			tail: tails,
		}

		tailObjMgr.tailsObjs = append(tailObjMgr.tailsObjs, obj)

		go readFromTail(obj)
	}

	return
}

// 讀入日誌數據
func readFromTail(tailObj *TailObj) {
	for true {
		msg, ok := <-tailObj.tail.Lines
		if !ok {
			logs.Warn("Tail file close reopen, filename:%s\n", tailObj.tail.Filename)
			time.Sleep(100 * time.Millisecond)
			continue
		}

		textMsg := &TextMsg{
			Msg:   msg.Text,
			Topic: tailObj.conf.Topic,
		}

		// 放入chan裏面
		tailObjMgr.msgChan <- textMsg
	}
}

server.go
在server.go中添加了sendToKafka函數, 該函數作用是取出tail.go文件中放入管道中的msg

並且調用kafka包中kafka.goSendToKafka函數發送消息到Kafka中

package main

import (
	"github.com/astaxie/beego/logs"
	"logagent/kafka"
	"logagent/tailf"
	"time"
)

func serverRun() (err error) {

	for {
		msg := tailf.GetOneLine()
		err = sendToKafka(msg)
		if err != nil {
			logs.Error("發送消息到Kafka 失敗, err:%v", err)
			time.Sleep(time.Second)
			continue
		}
	}

}

func sendToKafka(msg *tailf.TextMsg) (err error) {
	//fmt.Printf("讀取 msg:%s, topic:%s\n", msg.Msg, msg.Topic) // 將消息打印在終端
	_ = kafka.SendToKafka(msg.Msg, msg.Topic)
	return
}

kafka.go

定義了初始化kafka函數InitKafka與發送消息到Kafka的函數SendToKafka

package kafka

import (
	"github.com/Shopify/sarama"
	"github.com/astaxie/beego/logs"
)

var (
	client sarama.SyncProducer
)

func InitKafka(addr string) (err error) {

	// Kafka生產者配置
	config := sarama.NewConfig()
	config.Producer.RequiredAcks = sarama.WaitForAll          // 發送完數據需要leader和follow都確認
	config.Producer.Partitioner = sarama.NewRandomPartitioner // 新選出⼀個partition
	config.Producer.Return.Successes = true                   // 成功交付的消息將在success channel返回

	// 新建一個生產者對象
	client, err = sarama.NewSyncProducer([]string{addr}, config)
	if err != nil {
		logs.Error("初識化Kafka producer失敗:", err)
		return
	}
	logs.Debug("初始化Kafka producer成功,地址爲:", addr)
	return
}

func SendToKafka(data, topic string) (err error) {

	msg := &sarama.ProducerMessage{}
	msg.Topic = topic
	msg.Value = sarama.StringEncoder(data)

	pid, offset, err := client.SendMessage(msg)

	if err != nil {
		logs.Error("發送信息失敗, err:%v, data:%v, topic:%v", err, data, topic)
		return
	}

	logs.Debug("read success, pid:%v, offset:%v, topic:%v\n", pid, offset, topic)
	return
}

開發環境:

我這裏的環境是Go1.14, 使用了Go module模塊, 所以想要快速運行該項目需要在項目文件夾下 go mod init, 運行時自動下載依賴

運行main函數:

E:\Go\logagent\main>go build

注: 如果想使用Goland直接運行,這裏需要同時運行main包下的四個go文件
在這裏插入圖片描述

運行完如圖:kafka消費成功, 寫入my.log成功
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章