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

  • Tutorial
Данная статья это адское изобретение нового велосипеда. Так что на продакшене использовать только на свой страх и риск. Я долго искал систему для ведения логов на 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 ;

  • –9
  • 4,7k
  • 1
Поделиться публикацией

Комментарии 1

    +2
    Скажу честно искал я дня три так не чего и не нашел.

    Какие были варианты и чем не подошли? Например, logrus с хуками. Там точно можно писать в любые хранилища, добавить хуки на типы сообщений и дублировать в мэссенджеры или на почту. Все бы свелось к написанию демона, который перекладывает логи из редиса в мускуль, если уж это так нужно.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое