Данная статья это адское изобретение нового велосипеда. Так что на продакшене использовать только на свой страх и риск. Я долго искал систему для ведения логов на Go которая удовлетворила бы мои запросы (гибкая, возможность уведомления на емейл, очень быстрая и хранение логов в мускуле).
Скажу честно искал я дня три так не чего и не нашел. Потом я начал писать свой велосипед (первая его версия была очень кривая и еле еле работала). Потом я удалил весь тот код и началдумать писать заново.
Я сразу понял что писать в бд каждый раз очень утомительно. По этому я сделал так:
Библиотека для каждого типа логов делает ключ в редисе куда пишет данные в таком формате:
Я сразу захотел их встроить в sql запрос но наткнулся на ошибку sql синтаксиса. Я долгобился голов об клавиатуру искал багу в коде, а оказалось что из — за способа добавления данных в редис в конце строки всегда будет кома. Когда я её заметил я долго гуглил спросил на тостере как же убрать эту кому. Оказалось проще некуда:
Потом я начал пытаться из библиотеки запустить функцию в отдельном потоке. Со временем я понялчто я осёл что это я сделать не могу и поэтому вынес её в демон.
На выходе я имею:
На слабом впс (частота 1.6; 1ядро, 2 гб озу) 1 000 000 записей в мускуль добавилось за 25мл.сек (бд без тюнинга) правда в редис эти данные писались около 6 минут. Вся нагрузка на на редисе.
Вот вроде и всё.
В коде ещё есть импорт пакета mail.
Скажу честно искал я дня три так не чего и не нашел. Потом я начал писать свой велосипед (первая его версия была очень кривая и еле еле работала). Потом я удалил весь тот код и начал
Я сразу понял что писать в бд каждый раз очень утомительно. По этому я сделал так:
Библиотека для каждого типа логов делает ключ в редисе куда пишет данные в таком формате:
Данные в редисе
('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+“)»)
}
}
}
}
}
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 ;
leoismyname
Какие были варианты и чем не подошли? Например, logrus с хуками. Там точно можно писать в любые хранилища, добавить хуки на типы сообщений и дублировать в мэссенджеры или на почту. Все бы свелось к написанию демона, который перекладывает логи из редиса в мускуль, если уж это так нужно.