Как стать автором
Поиск
Написать публикацию
Обновить

Гибкая система логирования на Go

Время на прочтение10 мин
Количество просмотров5.8K
Данная статья это адское изобретение нового велосипеда. Так что на продакшене использовать только на свой страх и риск. Я долго искал систему для ведения логов на Go которая удовлетворила бы мои запросы (гибкая, возможность уведомления на емейл, очень быстрая и хранение логов в мускуле).

Скажу честно искал я дня три так не чего и не нашел. Потом я начал писать свой велосипед (первая его версия была очень кривая и еле еле работала). Потом я удалил весь тот код и начал думать писать заново.

Я сразу понял что писать в бд каждый раз очень утомительно. По этому я сделал так:

Библиотека для каждого типа логов делает ключ в редисе куда пишет данные в таком формате:

Данные в редисе
('Debug','2015-11-05 20:12:37.700052989 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.700506704 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.700663127 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.700803651 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.700987999 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701128513 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701293643 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701433496 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701602372 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701745287 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.701925988 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702093499 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702276867 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702431455 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702581625 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702738953 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.702899007 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.703055622 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.703210768 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.70340691 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.703566623 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.7037252 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.703954549 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.704119435 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.704281902 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.704536707 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.704721061 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.704901908 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705106033 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705284342 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705465074 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705633484 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705802108 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.705962381 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.706129288 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.706314702 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.706463092 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.706674268 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.706848586 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707050005 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707221136 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707379335 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707583978 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707742422 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.707967253 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.708164671 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.708410554 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.708578324 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.708775197 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.708955609 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.709184168 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.709349784 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.709510939 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.709726286 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.709940253 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.710141611 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.71034329 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.710537637 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.710763157 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.710969449 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.711167704 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.711355522 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.711550562 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.711756 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.712048767 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.712273974 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.712517739 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.712828333 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.71306392 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.713335398 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.713570618 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.71389819 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.714182802 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.714448273 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.714754937 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.715018147 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.715291228 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.715596998 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.715910118 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.7162719 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.716552975 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.716807074 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.717153412 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.717434854 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.717704591 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.717991896 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.718283451 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.718590239 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.718849058 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.719152303 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.719424972 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.719734567 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.720070491 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.720386241 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.720651655 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.72094698 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.721207595 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.721514296 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.721776408 +0200 EET','Testing'),('Debug','2015-11-05 20:12:37.722090163 +0200 EET','Testing'),


Я сразу захотел их встроить в sql запрос но наткнулся на ошибку sql синтаксиса. Я долго бился голов об клавиатуру искал багу в коде, а оказалось что из — за способа добавления данных в редис в конце строки всегда будет кома. Когда я её заметил я долго гуглил спросил на тостере как же убрать эту кому. Оказалось проще некуда:

strings.TrimRight(data, ",")

Потом я начал пытаться из библиотеки запустить функцию в отдельном потоке. Со временем я понял что я осёл что это я сделать не могу и поэтому вынес её в демон.

На выходе я имею:

Библиотека -> пишет данные в редис. Вся нагрузка на редис
Демон -> одним запросом все логи с редиса перемещаю в mysql

На слабом впс (частота 1.6; 1ядро, 2 гб озу) 1 000 000 записей в мускуль добавилось за 25мл.сек (бд без тюнинга) правда в редис эти данные писались около 6 минут. Вся нагрузка на на редисе.

Вот вроде и всё.

Конфиг
{

"MailConf" : [
"логин на smtp сервере",
"пароль",
"smtp сервер",
"порт smtp сервера"
],

"MailTo" : [
"на_какие_емейлы_отправлять@gmail.com",
"v.grabko99@yandex.ru"
],

"Types" : [
"Debug",
"Info",
"Warn",
"Error",
"Fatal"
],

"EmailSend" : [
"Error",
"Fatal"
],

"Redis" : [
"localhost:6379",
"parsh888",
"log_"
],

"MysqlConnect" : [
"юзер",
"пароль",
"база данных"
],

"MysqlTable" : "log",
"ReplicationTimeSecond" : 320

}


Библиотека
package GeneralsLog

import (
	"encoding/json"
	"gopkg.in/redis.v3"
	"io/ioutil"
	"log"
	"microService/libs/mail"
	"time"
)

type Config struct {
	MailTo, MailConf, Types, EmailSend, Redis, MysqlConnect []string
	MysqlTable                                              string
}

var (
	R           *redis.Client
	MailConf    map[string]string
	MailTo      []string
	Types       []string
	EmailSend   []string
	RedisConfig []string

	//Путь к файлу с конфигами
	config_file string = "/home/v-smerti/localhost/api/src/microService/config/log.json"
)

func init() {

	//Спарсим конфиг
	bs, err := ioutil.ReadFile(config_file)
	if err != nil {
		log.Panicln(err)
	}

	b := []byte(bs)
	var conf Config
	err = json.Unmarshal(b, &conf)
	if err != nil {
		log.Panicln(err)
	}
	//Передаём данные с конфиг файла в глобальные переменные
	MailConf = map[string]string{
		"username": conf.MailConf[0],
		"password": conf.MailConf[1],
		"host":     conf.MailConf[2],
		"port":     conf.MailConf[3],
	}

	MailTo = conf.MailTo
	Types = conf.Types
	EmailSend = conf.EmailSend
	RedisConfig = conf.Redis

	//Конект с редисом
	R = redis.NewClient(&redis.Options{
		Addr:     RedisConfig[0],
		Password: RedisConfig[1],
		DB:       0,
	})
	//Инициализация пакета. Здесь проверям есть ли в редисе такие типы логов. Если нет то создаём
	for _, typ := range Types {
		_, err := R.Get(RedisConfig[2] + typ).Result()
		if err == redis.Nil {
			if err := R.Set(RedisConfig[2]+typ, " ", 0).Err(); err != nil {
				mail.Send(MailConf, MailTo, "Fatal error game", "package GeneralsLog func Init____R.Set (Создание пустой записи в редис ключ "+typ+")")
			}
		} else if err != nil {
			mail.Send(MailConf, MailTo, "Fatal error game", "package GeneralsLog func Init____R.Set (Проверка записи "+typ+" в редисе)")
		}
	}
}

func New(types string, messages string) {
	for _, typ := range Types {
		if typ == types {
			if data, err := R.Get(RedisConfig[2] + typ).Result(); err == nil {

				//Надо для проверки текущего типа в списке на отправку уведомления на e-mail
				for _, b := range EmailSend {
					//надо отправлять
					if b == typ {
						mail.Send(MailConf, MailTo, typ, messages)
					}
				}

				data = data + "('" + types + "','" + time.Now().String() + "','" + messages + "'),"

				if err := R.Set(RedisConfig[2]+typ, data, 0).Err(); err != nil {
					mail.Send(MailConf, MailTo, "Fatal error game", "package GeneralsLog func "+typ+"____R.Set (Обновление записи в редисе)")
				}
			} else {
				mail.Send(MailConf, MailTo, "Fatal error game", "package GeneralsLog func "+typ+"____R.Get (Получение данных с редиса)")
			}
		} else {
		}
	}

}



Демон
package main

import (
«database/sql»
_ «github.com/go-sql-driver/mysql» //можно подключить любую sql базу данных
«gopkg.in/redis.v3»
«log»
«microService/libs/mail»
«strings»
«time»

«encoding/json»
«io/ioutil»
)

type Config struct {
MailTo, MailConf, Types, EmailSend, Redis, MysqlConnect []string
MysqlTable string
ReplicationTimeSecond time.Duration
}

var (
DB *sql.DB
R *redis.Client
MailConf map[string]string
MailTo []string
Types []string
EmailSend []string
RedisConfig []string
MysqlTable string
ReplicationSecond time.Duration
//Путь к файлу с конфигами
config_file string = "/home/v-smerti/localhost/api/src/microService/config/log.json"
)

func init() {
print(«Starting...»)
//Спарсим конфиг
bs, err := ioutil.ReadFile(config_file)
if err != nil {
log.Panicln(err)
}

b := []byte(bs)
var conf Config
err = json.Unmarshal(b, &conf)
if err != nil {
log.Panicln(err)
}
//Передаём данные с конфиг файла в глобальные переменные
MailConf = map[string]string{
«username»: conf.MailConf[0],
«password»: conf.MailConf[1],
«host»: conf.MailConf[2],
«port»: conf.MailConf[3],
}

MailTo = conf.MailTo
Types = conf.Types
EmailSend = conf.EmailSend
RedisConfig = conf.Redis
MysqlTable = conf.MysqlTable
ReplicationSecond = conf.ReplicationTimeSecond

//Конект с редисом
R = redis.NewClient(&redis.Options{
Addr: RedisConfig[0],
Password: RedisConfig[1],
DB: 0,
})

//конект с бд
db, err := sql.Open(«mysql», conf.MysqlConnect[0]+":"+conf.MysqlConnect[1]+"@/"+conf.MysqlConnect[2])
if err != nil {
log.Fatal(err)
}
DB = db

//Инициализация пакета. Здесь проверям есть ли в редисе такие типы логов. Если нет то создаём
for _, typ := range Types {
_, err := R.Get(RedisConfig[2] + typ).Result()
if err == redis.Nil {
if err := R.Set(RedisConfig[2]+typ, " ", 0).Err(); err != nil {
mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func Init____R.Set (Создание пустой записи в редис ключ „+typ+“)»)
}
} else if err != nil {
mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func Init____R.Set (Проверка записи „+typ+“ в редисе)»)
}
}
print(" Ok!")
}

func main() {
for {
replication_db()
time.Sleep(time.Second * ReplicationSecond)
}
}

func replication_db() {

for _, typ := range Types {
data, err := R.Get(RedisConfig[2] + typ).Result()
if err == redis.Nil {

mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func replication_db____R.Set (Нету в редисе „+typ+“)»)
if err := R.Set(RedisConfig[2]+typ, " ", 0).Err(); err != nil {
mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func replication_db____R.Set (Создание пустой записи в редис ключ „+typ+“)»)
}
} else if err != nil {
log.Fatal(err)
mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func replication_db____R.GET (Фатальная ошибка редиса)»)

} else {
if data != " " {
_, err := DB.Exec(«INSERT INTO » + MysqlTable + " (type,time,messages) VALUES" + strings.TrimRight(data, ","))
if err != nil {
log.Fatal(err)
} else {
log.Println(«replication»)
}
if err := R.Set(RedisConfig[2]+typ, " ", 0).Err(); err != nil {
mail.Send(MailConf, MailTo, «Fatal error game», «package GeneralsLog func replication_db____R.Set (Очистка записии „+typ+“)»)
}
}

}
}
}


В коде ещё есть импорт пакета mail.

Пакет mail
package mail

import (
	"fmt"
	"net/smtp"
)

func Send(conf map[string]string, to []string, subject string, msg string) error {
	auth := smtp.PlainAuth(
		"",
		conf["username"],
		conf["password"],
		conf["host"],
	)
	address := fmt.Sprintf("%v:%v", conf["host"], conf["port"])
	body := []byte("Subject: " + subject + "\r\n\r\n" + msg)
	err := smtp.SendMail(
		address,
		auth,
		conf["username"],
		to,
		body,
	)
	if err != nil {
		return err
	}
	return nil
}


--
-- Структура таблицы `log`
--

CREATE TABLE IF NOT EXISTS `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` text NOT NULL,
  `time` datetime NOT NULL,
  `messages` text CHARACTER SET utf32 NOT NULL,
  PRIMARY KEY (`id`),
  KEY `type` (`type`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Теги:
Хабы:
Всего голосов 23: ↑7 и ↓16-9
Комментарии1

Публикации

Ближайшие события