Этот пост вдохновлен темой в форуме Go Forum, начатой Nate Finch. Этот пост сконцентрирован на языке Go, но если пройти мимо этого, я думаю, идеи представленные тут широко применимы.
Почему нет любви?
Пакет log в Go не имеет уровней для логов, вы можете сами вручную добавить приставки DEBUG, INFO, WARN, и ERROR. Также logger тип в Go не имеет возможности включить или выключить эти уровни отдельно для выбранных пакетов. Для сравнения давайте глянем на несколько его замен от сторонних разработчиков.
glog от Google имеет уровни:
- Info
- Warning
- Error
- Fatal (завершает программу)
Посмотрим на другую библиотеку, loggo, разработанную для Juju, в ней доступны уровни:
- Trace
- Debug
- Info
- Warning
- Error
- Critical
Loggo также имеет возможность задать уровень детализации лога для нужных пакетов по отдельности.
Перед вами два примера, явно созданных под влиянием других библиотек для логирования на других языках.
Фактически их происхождение можно проследить до syslog(3), возможно, даже раньше. И я думаю, что они не правы.
Я хочу занять противоречивую позицию. Я думаю, что все библиотеки журналов плохи, потому что предлагают слишком много функций; ошеломляющий набор возможностей, который ошеломляет программиста прямо в тот момент, когда он должен ясно думать о том, как общаться с читателем из будущего, с тем, кто будет просматривать эти журналы.
Я утверждаю, что для успешных пакетов логов требуется гораздо меньше возможностей и, конечно, меньше уровней.
Давайте поговорим о предупреждениях (WARNING)
Давайте начнем с самого простого. Никому не нужен уровень журнала WARNING (предупреждение).
Никто не читает предупреждения, потому что по определению ничего плохого не произошло. Возможно, что-то может пойти не так в будущем, но это звучит как чья-то, a не моя проблема.
Кроме того, если вы используете какое-то многоуровневое логирование, зачем вам устанавливать уровень WARNING? Вы установили бы уровень INFO или ERROR. Установка уровня WARNING означает, что вы, вероятно, регистрируете ошибки на уровне WARNING.
Исключите уровень warning — это или информационное сообщение, или ошибка.
Давайте поговорим об уровне невосстановимой ошибки (fatal)
Уровень FATAL фактически заносит сообщение в лог, а затем вызывает os.Exit(1). В принципе это означает:
- отложенные выражения в других подпрограммах(горутинах) не выполняются;
- буферы не очищаются;
- временные файлы и каталоги не удаляются.
По сути, log.Fatal менее многословен, но семантически эквивалентен панике.
Общепринято, что библиотеки не должны использовать panic1, но если вызов log.Fatal2 имеет тот же эффект, он также должен быть запрещен.
Предположения, что эта проблема очистки может быть решена путем регистрации обработчиков завершения работы в логгере, обеспечивает тесную связь между используемым логгером и каждым местом, где происходят операции очистки. Это также нарушает разделение интересов.
Не записывайте сообщения с уровнем FATAL, предпочтите вместо этого вернуть ошибку вызывающей стороне. Если ошибка доходит до main.main, то это правильное место для выполнения любых действий по очистке перед завершением программы.
Давайте поговорим об ошибке (уровень ERROR)
Обработка ошибок и ведение журнала (лога) тесно связаны, поэтому, на первый взгляд, регистрация на уровне ошибок (ERROR) должна быть легко оправданной. Я не согласен.
В Go, если вызов функции или метода возвращает значение ошибки, то реально у вас есть два варианта:
- обработать ошибку.
- вернуть ошибку своему вызвавшей стороне. Вы можете красиво завернуть ошибку в подарочную упаковку (wrap), но это не важно для этого обсуждения.
Если вы решите обработать ошибку, записав ее в лог, то по определению это больше уже не ошибка — вы ее обработали. Сам акт регистрации ошибки и есть обработка ошибки, следовательно, больше не целесообразно ее записывать в лог как ошибку.
Позвольте мне убедить вас с помощью этого фрагмента кода:
err := somethingHard()
if err != nil {
log.Error("oops, something was too hard", err)
return err // what is this, Java ?
}
Вы никогда не должны регистрировать что-либо на уровне ошибки, потому что вы должны либо обработать ошибку, либо передать ее вызывающей стороне.
Нужно четко понимать, я не говорю, что вы не должны записывать в лог, что произошла смена состояния:
if err := planA(); err != nil {
log.Infof("could't open the foo file, continuing with plan b: %v", err)
planB()
}
Но в действительности log.Info и log.Error имеют одну и ту же цель.
Я не говорю «не регистрируйте ошибки»! Вместо этого я ставлю вопрос, что является наименьшим возможным API для ведения журнала (логирования)? И когда дело доходит до ошибок, я считаю, что подавляющая часть вещей, записанных на уровне ERROR, просто делается так, потому что они связаны с ошибкой. На самом деле они просто информационные, поэтому мы можем удалить логирование на уровне ошибок (ERROR) из нашего API.
Что осталось?
Мы исключили предупреждения (WARNING), аргументировали, что ничего не должно регистрироваться на уровне ошибок (ERROR), и показали, что только верхний уровень приложения должен иметь своего рода log.Fatal поведение. Что осталось?
Я считаю, что есть только две вещи, которые вы должны заносить в лог:
- вещи, о которых заботятся разработчики, когда они разрабатывают или отлаживают программу;
- вещи, которые волнуют пользователей при использовании вашей программы.
Очевидно, это уровни отладка (DEBUG) и информационный (INFO) соответственно.
log.Info должен просто записать эту строку в вывод журнала. Не должно быть возможности отключить его, так как пользователю следует рассказывать только то, что ему полезно. Если возникает ошибка, которая не может быть обработана, она должна появиться в main.main там, где программа завершается. Незначительные неудобства, связанные с необходимостью вставки префикса FATAL перед окончательным сообщением журнала или записи непосредственно в os.Stderr с помощью fmt.Fprintf, не является достаточным основанием для расширения пакета матодом log.Fatal.
log.Debug, это совсем другое дело. Он нужен разработчику или инженера поддержки для контроля работы программы. Во время разработки выражения отладки (debug) должны быть многочисленными, не прибегая к уровню трассировки (trace) или debug2 (ты знаешь кто ты). Пакет ведения логов должен поддерживать детализированное управление для включения или отключения выражений отладки, для нужных пакетов пакете или, возможно, даже в более узкой области видимости.
Заключение
Если бы это был опрос в Твиттере, я бы попросил вас выбрать между
- ведение логов — это важно
- ведение логов — это трудно
Но дело в том, что ведение лога является и тем и другим. Решение этой проблемы ДОЛЖНО состоять в том, чтобы разрушать и беспощадно сокращать ненужные отвлечения.
Как вы думаете? Это достаточно сумасбродно, чтобы работать, или просто сумасбродно?
Примечания
Некоторые библиотеки могут использовать panic/recover в качестве механизма внутреннего потока управления, но основная мантра заключается в том, что они не должны допускать утечки этих операций потока управления за границу пакета.
По иронии судьбы, хотя в нем отсутствует уровень вывода DEBUG, стандартный пакет логирования Go имеет функции Fatal и Panic. В этом пакете количество функций, которые приводят к внезапному завершению работы программы, превышает число тех, которые этого не делают.
Об авторе
Автор данной статьи, Дейв Чини, является автором многих популярных пакетов для Go, например github.com/pkg/errors и github.com/davecheney/httpstat. Авторитет и опыт автора вы можете оценить самостоятельно.
От переводчика
Насчет ведения логов обеспокоено достаточно много разработчиков, некоторые обсуждали внесение Logger интерфейса в стандартную библиотеку Go, чтобы упорядочить ведение логов в библиотеках и так стимулировать их разработчиков. Ребята даже оформили свое предложение и выставили документ на обсуждение.
Плюс презентация размышление Нужен ли нам новый логер и каким он должен быть? от Chris Hines.
Есть несколько реализаций идей Дейва go-log и немного отходящий в вопросе уровня ERROR и более тщательно продуманный пакет logr.
Overview ▸
Overview ▾
Package log implements a simple logging package. It defines a type, Logger,
with methods for formatting output. It also has a predefined ‘standard’
Logger accessible through helper functions Print[f|ln], Fatal[f|ln], and
Panic[f|ln], which are easier to use than creating a Logger manually.
That logger writes to standard error and prints the date and time
of each logged message.
Every log message is output on a separate line: if the message being
printed does not end in a newline, the logger will add one.
The Fatal functions call os.Exit(1) after writing the log message.
The Panic functions call panic after writing the log message.
Index ▸
Index ▾
- Constants
- func Fatal(v …any)
- func Fatalf(format string, v …any)
- func Fatalln(v …any)
- func Flags() int
- func Output(calldepth int, s string) error
- func Panic(v …any)
- func Panicf(format string, v …any)
- func Panicln(v …any)
- func Prefix() string
- func Print(v …any)
- func Printf(format string, v …any)
- func Println(v …any)
- func SetFlags(flag int)
- func SetOutput(w io.Writer)
- func SetPrefix(prefix string)
- func Writer() io.Writer
- type Logger
- func Default() *Logger
- func New(out io.Writer, prefix string, flag int) *Logger
- func (l *Logger) Fatal(v …any)
- func (l *Logger) Fatalf(format string, v …any)
- func (l *Logger) Fatalln(v …any)
- func (l *Logger) Flags() int
- func (l *Logger) Output(calldepth int, s string) error
- func (l *Logger) Panic(v …any)
- func (l *Logger) Panicf(format string, v …any)
- func (l *Logger) Panicln(v …any)
- func (l *Logger) Prefix() string
- func (l *Logger) Print(v …any)
- func (l *Logger) Printf(format string, v …any)
- func (l *Logger) Println(v …any)
- func (l *Logger) SetFlags(flag int)
- func (l *Logger) SetOutput(w io.Writer)
- func (l *Logger) SetPrefix(prefix string)
- func (l *Logger) Writer() io.Writer
Examples
(Expand All)
- Logger
- Logger.Output
Package files
log.go
Constants
These flags define which text to prefix to each log entry generated by the Logger.
Bits are or’ed together to control what’s printed.
With the exception of the Lmsgprefix flag, there is no
control over the order they appear (the order listed here)
or the format they present (as described in the comments).
The prefix is followed by a colon only when Llongfile or Lshortfile
is specified.
For example, flags Ldate | Ltime (or LstdFlags) produce,
2009/01/23 01:23:23 message
while flags Ldate | Ltime | Lmicroseconds | Llongfile produce,
2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const ( Ldate = 1 << iota Ltime Lmicroseconds Llongfile Lshortfile LUTC Lmsgprefix LstdFlags = Ldate | Ltime )
func Fatal
¶
func Fatal(v ...any)
Fatal is equivalent to Print() followed by a call to os.Exit(1).
func Fatalf
¶
func Fatalf(format string, v ...any)
Fatalf is equivalent to Printf() followed by a call to os.Exit(1).
func Fatalln
¶
func Fatalln(v ...any)
Fatalln is equivalent to Println() followed by a call to os.Exit(1).
func Flags
¶
func Flags() int
Flags returns the output flags for the standard logger.
The flag bits are Ldate, Ltime, and so on.
func Output
¶
1.5
func Output(calldepth int, s string) error
Output writes the output for a logging event. The string s contains
the text to print after the prefix specified by the flags of the
Logger. A newline is appended if the last character of s is not
already a newline. Calldepth is the count of the number of
frames to skip when computing the file name and line number
if Llongfile or Lshortfile is set; a value of 1 will print the details
for the caller of Output.
func Panic
¶
func Panic(v ...any)
Panic is equivalent to Print() followed by a call to panic().
func Panicf
¶
func Panicf(format string, v ...any)
Panicf is equivalent to Printf() followed by a call to panic().
func Panicln
¶
func Panicln(v ...any)
Panicln is equivalent to Println() followed by a call to panic().
func Prefix
¶
func Prefix() string
Prefix returns the output prefix for the standard logger.
func Print
¶
func Print(v ...any)
Print calls Output to print to the standard logger.
Arguments are handled in the manner of fmt.Print.
func Printf
¶
func Printf(format string, v ...any)
Printf calls Output to print to the standard logger.
Arguments are handled in the manner of fmt.Printf.
func Println
¶
func Println(v ...any)
Println calls Output to print to the standard logger.
Arguments are handled in the manner of fmt.Println.
func SetFlags
¶
func SetFlags(flag int)
SetFlags sets the output flags for the standard logger.
The flag bits are Ldate, Ltime, and so on.
func SetOutput
¶
func SetOutput(w io.Writer)
SetOutput sets the output destination for the standard logger.
func SetPrefix
¶
func SetPrefix(prefix string)
SetPrefix sets the output prefix for the standard logger.
func Writer
¶
1.13
func Writer() io.Writer
Writer returns the output destination for the standard logger.
type Logger
¶
A Logger represents an active logging object that generates lines of
output to an io.Writer. Each logging operation makes a single call to
the Writer’s Write method. A Logger can be used simultaneously from
multiple goroutines; it guarantees to serialize access to the Writer.
type Logger struct { }
▸ Example
▾ Example
logger: example_test.go:19: Hello, log file!
func Default
¶
1.16
func Default() *Logger
Default returns the standard logger used by the package-level output functions.
func New
¶
func New(out io.Writer, prefix string, flag int) *Logger
New creates a new Logger. The out variable sets the
destination to which log data will be written.
The prefix appears at the beginning of each generated log line, or
after the log header if the Lmsgprefix flag is provided.
The flag argument defines the logging properties.
func (*Logger) Fatal
¶
func (l *Logger) Fatal(v ...any)
Fatal is equivalent to l.Print() followed by a call to os.Exit(1).
func (*Logger) Fatalf
¶
func (l *Logger) Fatalf(format string, v ...any)
Fatalf is equivalent to l.Printf() followed by a call to os.Exit(1).
func (*Logger) Fatalln
¶
func (l *Logger) Fatalln(v ...any)
Fatalln is equivalent to l.Println() followed by a call to os.Exit(1).
func (*Logger) Flags
¶
func (l *Logger) Flags() int
Flags returns the output flags for the logger.
The flag bits are Ldate, Ltime, and so on.
func (*Logger) Output
¶
func (l *Logger) Output(calldepth int, s string) error
Output writes the output for a logging event. The string s contains
the text to print after the prefix specified by the flags of the
Logger. A newline is appended if the last character of s is not
already a newline. Calldepth is used to recover the PC and is
provided for generality, although at the moment on all pre-defined
paths it will be 2.
▸ Example
▾ Example
INFO: example_test.go:36: Hello world
func (*Logger) Panic
¶
func (l *Logger) Panic(v ...any)
Panic is equivalent to l.Print() followed by a call to panic().
func (*Logger) Panicf
¶
func (l *Logger) Panicf(format string, v ...any)
Panicf is equivalent to l.Printf() followed by a call to panic().
func (*Logger) Panicln
¶
func (l *Logger) Panicln(v ...any)
Panicln is equivalent to l.Println() followed by a call to panic().
func (*Logger) Prefix
¶
func (l *Logger) Prefix() string
Prefix returns the output prefix for the logger.
func (*Logger) Print
¶
func (l *Logger) Print(v ...any)
Print calls l.Output to print to the logger.
Arguments are handled in the manner of fmt.Print.
func (*Logger) Printf
¶
func (l *Logger) Printf(format string, v ...any)
Printf calls l.Output to print to the logger.
Arguments are handled in the manner of fmt.Printf.
func (*Logger) Println
¶
func (l *Logger) Println(v ...any)
Println calls l.Output to print to the logger.
Arguments are handled in the manner of fmt.Println.
func (*Logger) SetFlags
¶
func (l *Logger) SetFlags(flag int)
SetFlags sets the output flags for the logger.
The flag bits are Ldate, Ltime, and so on.
func (*Logger) SetOutput
¶
1.5
func (l *Logger) SetOutput(w io.Writer)
SetOutput sets the output destination for the logger.
func (*Logger) SetPrefix
¶
func (l *Logger) SetPrefix(prefix string)
SetPrefix sets the output prefix for the logger.
func (*Logger) Writer
¶
1.12
func (l *Logger) Writer() io.Writer
Writer returns the output destination for the logger.
Subdirectories
Name | Synopsis |
---|---|
.. | |
syslog | Package syslog provides a simple interface to the system log service. |
На данный момент в файле main.go
мы выводим лог с помощью функций log.Printf()
и log.Fatal()
.
В Go, обе эти функции выводят сообщения через стандартный логгер, который по умолчанию добавляет к сообщениям префиксы с локальной датой и временем и записывает их в стандартный поток ошибок (который должен отображаться в окне терминала). Функция log.Fatal()
также вызовет os.Exit(1)
после того как выведет в терминал сообщение об ошибке, это приведет к мгновенному завершению работы приложения.
Премиум 👑 канал по Golang
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Подписаться на канал
Уроки, статьи и Видео
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Go в ВК
ЧАТ в Telegram
Содержание статьи
- Разделение логирования для разных задач
- Логирование ошибок от HTTP-сервера
- Дополнительные методы логирования
- Конкурентное логирование в Golang
- Логирование сообщений в файл
Логгирование можно поделить на два различных типа, или уровня. К первому типу относятся информационные сообщения (вроде «Запуск сервера на :4000«), а ко второму типу относятся сообщения об ошибках.
log.Printf(«Запуск сервера на %s», *addr) // Информационное сообщение err := http.ListenAndServe(*addr, mux) log.Fatal(err) // Сообщение об фатальной ошибке в работе программы |
Давайте усовершенствуем наше приложение, добавив возможность многоуровнего логирования, чтобы информационные сообщения и сообщения об ошибках обрабатывались по-разному. А именно:
- Информационным сообщениям добавим префикс
"INFO"
. Такое сообщение будет выводиться в стандартный поток вывода (stdout); - Сообщениям об ошибках добавим префикс
"ERROR"
. Такие сообщения будут выводиться в стандартный поток ошибок (stderr) вместе с соответствующим названием файла и номером строки, которая вызвала логгер для записи (это поможет в отладке на будущее).
Есть несколько способов использования разных логгеров, но самый простой и понятный подход заключается в использовании функции log.New()
для создания двух новых настраиваемых логгеров.
Откройте файл main.go
и обновите его код следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package main import ( «flag» «log» «net/http» «os» // новый импорт ) func main() { addr := flag.String(«addr», «:4000», «Сетевой адрес веб-сервера») flag.Parse() // Используйте log.New() для создания логгера для записи информационных сообщений. Для этого нужно // три параметра: место назначения для записи логов (os.Stdout), строка // с префиксом сообщения (INFO или ERROR) и флаги, указывающие, какая // дополнительная информация будет добавлена. Обратите внимание, что флаги // соединяются с помощью оператора OR |. infoLog := log.New(os.Stdout, «INFOt», log.Ldate|log.Ltime) // Создаем логгер для записи сообщений об ошибках таким же образом, но используем stderr как // место для записи и используем флаг log.Lshortfile для включения в лог // названия файла и номера строки где обнаружилась ошибка. errorLog := log.New(os.Stderr, «ERRORt», log.Ldate|log.Ltime|log.Lshortfile) mux := http.NewServeMux() mux.HandleFunc(«/», home) mux.HandleFunc(«/snippet», showSnippet) mux.HandleFunc(«/snippet/create», createSnippet) fileServer := http.FileServer(http.Dir(«./ui/static/»)) mux.Handle(«/static/», http.StripPrefix(«/static», fileServer)) // Применяем созданные логгеры к нашему приложению. infoLog.Printf(«Запуск сервера на %s», *addr) err := http.ListenAndServe(*addr, mux) errorLog.Fatal(err) } |
Отлично… проверим эти изменения в действии!
Попробуйте запустить приложение, затем откройте другое окно терминала и попробуйте запустить его во второй раз. В результате должна появится сообщение об ошибке, потому что сетевой адрес, который наш сервер хочет прослушать (":4000"
), уже используется другим процессом.
Логи во втором терминале должны выглядеть следующим образом:
go run ./cmd/web INFO 2021/01/23 19:26:13 Запуск сервера на :4000 ERROR 2021/01/23 19:26:13 main.go:37: listen tcp :4000: bind: address already in use exit status 1 |
Обратите внимание, что у этих двух сообщений разные префиксы — чтобы их можно было легко различить в терминале — и наше сообщение об ошибке также включает в себя название файла и номер строки (main.go:37
), которая вызывает логгер для записи возникнувшей ошибки.
На заметку: Если вы хотите включить весь путь файла в лог вместо просто названия файла, при создании логгера можно использовать флаг
log.Llongfile
вместоlog.Lshortfile
. Вы также можете заставить свой логгер использовать UTC дату (вместо локальной), добавив флагlog.LUTC
.
Разделение логирования для разных задач
Большое преимущество логирования сообщений в стандартных потоках (stdout и stderr), как у нас, заключается в том, что само приложение и логирование не связаны. Само приложение не занимается маршрутизацией или хранением логов, и это может упростить управление логами, которое будет различаться в зависимости от среды.
Стандартные потоки отображаются в терминале, поэтому вывод логов можно легко посмотреть после запуска приложения из терминала.
Если приложение запущено в рабочем режиме и обслуживает реальных пользователей, то наши логи должны записываться в специальном месте. Таким местом могут быть файлы на диске или различные сервисы мониторинга работы приложения. В любом случае, конечное место хранения логов может быть указано в самой среде выполнения независимо от приложения.
Например, можно перенаправить потоки из stdout и stderr в файлы на диске при запуске приложения из терминала следующим образом:
$ go run ./cmd/web >>/tmp/info.log 2>>/tmp/error.log |
Логирование ошибок от HTTP-сервера
Нам нужно внести еще одно изменение в коде нашего веб-приложения. По умолчанию, если HTTP-сервер обнаруживает ошибку, он логирует её с помощью стандартного логгера. Но, лучше использовать наш новый логгер errorLog
.
Нам требуется инициализировать новую структуру http.Server, содержащую параметры конфигурации для сервера, вместо использования http.ListenAndServe()
.
Проще всего будет показать это на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package main import ( «flag» «log» «net/http» «os» ) func main() { addr := flag.String(«addr», «:4000», «Сетевой адрес веб-сервера») flag.Parse() infoLog := log.New(os.Stdout, «INFOt», log.Ldate|log.Ltime) errorLog := log.New(os.Stderr, «ERRORt», log.Ldate|log.Ltime|log.Lshortfile) mux := http.NewServeMux() mux.HandleFunc(«/», home) mux.HandleFunc(«/snippet», showSnippet) mux.HandleFunc(«/snippet/create», createSnippet) fileServer := http.FileServer(http.Dir(«./ui/static/»)) mux.Handle(«/static/», http.StripPrefix(«/static», fileServer)) // Инициализируем новую структуру http.Server. Мы устанавливаем поля Addr и Handler, так // что сервер использует тот же сетевой адрес и маршруты, что и раньше, и назначаем // поле ErrorLog, чтобы сервер использовал наш логгер // при возникновении проблем. srv := &http.Server{ Addr: *addr, ErrorLog: errorLog, Handler: mux, } infoLog.Printf(«Запуск сервера на %s», *addr) // Вызываем метод ListenAndServe() от нашей новой структуры http.Server err := srv.ListenAndServe() errorLog.Fatal(err) } |
Дополнительные методы логирования
До сих пор мы использовали методы Println()
, Printf()
и Fatal()
для записи логов. Однако Go предоставляет ряд других методов, с которыми стоит ознакомиться.
Как правило, лучше избегать использования методов Panic()
и Fatal()
за пределами функции main()
. Вместо этого рекомендуется возвращать возникшие ошибки, а паниковать или принудительно завершать приложение непосредственно из самого main()
.
Конкурентное логирование в Golang
Новые логгеры, созданные с помощью log.New()
, конкурентно-безопасны. Вы можете делиться одним логгером и использовать его в нескольких горутинах, не беспокоясь об возможных конфликтах между ними из за записи сообщений в одном и том же логгере.
Если у вас есть несколько логгеров, использующих для записи одно и то же место назначения, вам требуется убедиться, что базовый метод Write()
также безопасен для конкурентного использования.
Логирование сообщений в файл
Как было сказано выше, лучше записывать вывод в стандартные потоки и перенаправлять вывод в файл при запуске приложения из командной строки. Но, если вы не хотите этого делать, всегда можно открыть файл в Go и использовать его в качестве места назначения лога. Например:
f, err := os.OpenFile(«info.log», os.O_RDWR|os.O_CREATE, 0666) if err != nil { log.Fatal(err) } defer f.Close() infoLog := log.New(f, «INFOt», log.Ldate|log.Ltime) |
Скачать исходный код сайта
В конце каждой статьи, вы можете скачать готовый код нашего веб-приложения. В каждой статье мы обновляем код добавляя что-то новое.
Скачать: snippetbox-11.zip
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»
Logging can be a life-saver when it comes to discovering bugs or faults in your Go (Golang) code. The three most popular ways to log errors in Golang are:
- Output the errors to the console
- Log the errors to a file
- Use a logging framework
This article will walk you through how to log errors using each method, when and why you’d want to use each, along with examples.
Basic Logging in Golang
Golang comes with an in-built standard log
package which provides basic error logging features. Though it doesn’t provide any log levels like debug
, warning
, or error
, it still has many features to get started with basic logging.
Let’s look at an example to understand it better.
package main
import "log"
func main() {
log.Println("We are logging in Golang!")
}
When the above code is executed, the log
package prints the output to the standard error (stderr)
stream and automatically appends a timestamp to each log message.
2022/09/30 02:44:31 We are logging in Golang!
Logging to a File in Golang
Despite the fact that the log
package’s default output is to the stderr
stream, it may be adjusted to write to any local file or to any other location that accepts the io.Writer
interface. You must either create a new file or open an existing one and set it up as the log’s output path in order to save log messages in a file.
Example
package main
import (
"log"
"os"
)
func main() {
file, err := os.OpenFile("myLOG.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
log.SetOutput(file)
log.Println("We are logging in Golang!")
}
In the above code, we have created the file myLOG.txt
or opened it if it is already existing in the append and write-only mode. When the above code is executed, the following output is written to the myLOG.txt
file:
2022/09/30 03:02:51 We are logging in Golang!
Writing Custom Logs in Golang
The log.New()
method may be used to create custom loggers. Three arguments must be sent to a custom logger when using the log.New()
function:
out
— It specifies the place where the log data has to be written, for instance, a file path. It could be any interface that implements the io.Writer interface.prefix
— a string or text which has to be appended at the beginning of each log line.flag
— These are sets of constants which allow us to define logging properties.
Example: Writing Custom Logs
package main
import (
"log"
"os"
)
var (
WarningLog *log.Logger
InfoLog *log.Logger
ErrorLog *log.Logger
)
func init() {
file, err := os.OpenFile("myLOG.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLog = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLog = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLog = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
InfoLog.Println("Opening the application...")
InfoLog.Println("Something has occurred...")
WarningLog.Println("WARNING!!!..")
ErrorLog.Println("Some error has occurred...")
}
With the help of the destination
file path
, prefix string
, and flags
provided in the code above, three logs have been generated. These loggers use the println()
method in the main function to write log entries to the file.
When the above code is executed we get the following output written into myLOG.txt
file.
INFO: 2022/09/30 03:20:51 main.go:26: Opening the application...
INFO: 2022/09/30 03:20:51 main.go:27: Something has occurred...
WARNING: 2022/09/30 03:20:51 main.go:28: WARNING!!!..
ERROR: 2022/09/30 03:20:51 main.go:29: Some error has occurred…
Logging Frameworks
When used for local development, the log
package is fantastic. However, logging frameworks are preferable when working on a larger scale. Two logging frameworks you should know about are Logrus and Rollbar.
Logrus
Logrus is completely API compatible with the log
package, supports color-coded formatting of your logs, and works well for structured JSON logging. It can be installed on your system using the command below.
go get "github.com/Sirupsen/logrus"
Example: Logrus
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.SetFormatter(&log.JSONFormatter{})
log.WithFields(
log.Fields{
"field1": "foo",
"field2": "bar",
},
).Info("Log message here!!!")
}
When the above code is executed we get the following output:
{"field1":"bar","field2":"foo","level":"info","msg":"Log message here!!!","time":"2022-09-30T15:55:24+01:00"}
Rollbar
Rollbar is a real-time error reporting service for Go and other languages. It makes it easy to identify the root cause of bugs through stack traces, local variables, telemetry, suspect deploys, and other metadata. Errors are sent to Rollbar asynchronously in a background goroutine and in return you get instant and accurate alerts — grouped using machine learning to reduce noise.
Example: Rollbar
package main
import (
"github.com/rollbar/rollbar-go"
"time"
)
func main() {
rollbar.SetToken("MY_TOKEN")
rollbar.SetEnvironment("production") // defaults to "development"
rollbar.SetCodeVersion("v2") // optional Git hash/branch/tag (required for GitHub integration)
rollbar.SetServerHost("web.1") // optional override; defaults to hostname
rollbar.SetServerRoot("github.com/heroku/myproject") // path of project (required for GitHub integration and non-project stacktrace collapsing) - where repo is set up for the project, the server.root has to be "/"
rollbar.Info("Message body goes here")
rollbar.WrapAndWait(doSomething)
}
func doSomething() {
var timer *time.Timer = nil
timer.Reset(10) // this will panic
}
Errors are displayed on a real-time feed.
For each error, you can drill down to get request parameters, local variables, affected users and IP addresses, browsers and OSes, deployed code versions, and more.
The Rollbar Query Language (RQL) allows you to monitor, perform data analysis and build custom reports on your error data, using a familiar SQL-like language.
Conclusion
Error logging can be very helpful in analyzing the health of your application. The built-in Go logging package should only be used when working with small applications. Logging frameworks like Rollbar should be used for logging in large-scale applications.
Track, Analyze and Manage Errors With Rollbar
Managing errors and exceptions in your code is challenging. It can make deploying production code an unnerving experience. Being able to track, analyze, and manage errors in real-time can help you proceed with more confidence. Rollbar automates error monitoring and triaging, making fixing Go errors easier than ever. Try it today!
You’re relatively new to the Go language. You’re probably using it to write a web app or a server, and you need to create a log file. So, you do a quick web search and find that there are a ton of options for logging in go. How do you know which one to pick? This article will equip you to answer that question.
We will take a look at the built-in log
package and determine what projects it is suited for before exploring other logging solutions that are prevalent in the Go ecosystem.
What to log
I don’t need to tell you how important logging is. Logs are used by every production web application to help developers and operations:
- Spot bugs in the application’s code
- Discover performance problems
- Do post-mortem analysis of outages and security incidents
The data you actually log will depend on the type of application you’re building. Most of the time, you will have some variation in the following:
- The timestamp for when an event occurred or a log was generated
- Log levels such as debug, error, or info
- Contextual data to help understand what happened and make it possible to easily reproduce the situation
What not to log
In general, you shouldn’t log any form of sensitive business data or personally identifiable information. This includes, but is not limited to:
- Names
- IP addresses
- Credit card numbers
These restrictions can make logs less useful from an engineering perspective, but they make your application more secure. In many cases, regulations such as GDPR and HIPAA may forbid the logging of personal data.
Introducing the log package
The Go standard library has a built-in log
package that provides most basic logging features. While it does not have log levels (such as debug, warning, or error), it still provides everything you need to get a basic logging strategy set up.
Here’s the most basic logging example:
package main
import "log"
func main() {
log.Println("Hello world!")
}
The code above prints the text «Hello world!» to the standard error, but it also includes the date and time, which is handy for filtering log messages by date.
2019/12/09 17:21:53 Hello world!
By default, the
log
package prints to the standard error (stderr
) output stream, but you can make it write to local files or any destination that supports theio.Writer
interface. It also adds a timestamp to the log message without any additional configuration.
Logging to a file
If you need to store log messages in a file, you can do so by creating a new file or opening an existing file and setting it as the output of the log. Here’s an example:
package main
import (
"log"
"os"
)
func main() {
// If the file doesn't exist, create it or append to the file
file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
log.SetOutput(file)
log.Println("Hello world!")
}
When we run the code, the following is written to logs.txt.
2019/12/09 17:22:47 Hello world!
As mentioned earlier, you can basically output your logs to any destination that implements the io.Writer
interface, so you have a lot of flexibility when deciding where to log messages in your application.
Creating custom loggers
Although the log
package implements a predefined logger
that writes to the standard error, we can create custom logger types using the log.New()
method.
When creating a new logger, you need to pass in three arguments to log.New()
:
out
: Any type that implements theio.Writer
interface, which is where the log data will be written toprefix
: A string that is appended to the beginning of each log lineflag
: A set of constants that allow us to define which logging properties to include in each log entry generated by the logger (more on this in the next section)
We can take advantage of this feature to create custom loggers. Here’s an
example that implements Info
, Warning
and Error
loggers:
package main
import (
"log"
"os"
)
var (
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLogger = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
InfoLogger.Println("Starting the application...")
InfoLogger.Println("Something noteworthy happened")
WarningLogger.Println("There is something you should know about")
ErrorLogger.Println("Something went wrong")
}
After creating or opening the logs.txt
file at the top of the init
function, we then initialize the three defined loggers by providing the output destination, prefix string, and log flags.
In the main
function, the loggers are utilized by calling the Println
function, which writes a new log entry to the log file. When you run this program, the following will be written to logs.txt
.
INFO: 2019/12/09 12:01:06 main.go:26: Starting the application...
INFO: 2019/12/09 12:01:06 main.go:27: Something noteworthy happened
WARNING: 2019/12/09 12:01:06 main.go:28: There is something you should know about
ERROR: 2019/12/09 12:01:06 main.go:29: Something went wrong
Note that in this example, we are logging to a single file, but you can use a separate file for each logger by passing a different file when creating the logger.
Log flags
You can use log flag constants to enrich a log message by providing additional context information, such as the file, line number, date, and time. For example, passing the message «Something went wrong» through a logger with a flag combination shown below:
log.Ldate|log.Ltime|log.Lshortfile
will print
2019/12/09 12:01:06 main.go:29: Something went wrong
Unfortunately, there is no control over the order in which they appear or the format in which they are presented.
Introducing logging frameworks
Using the log
package is great for local development when getting fast feedback is more important than generating rich, structured logs. Beyond that, you will mostly likely be better off using a logging framework.
A major advantage of using a logging framework is that it helps to standardize the log data. This means that:
- It’s easier to read and understand the log data.
- It’s easier to gather logs from several sources and feed them to a central platform to be analyzed.
In addition, logging is pretty much a solved problem. Why reinvent the wheel?
Choosing a logging framework
Deciding which framework to use can be a challenge, as there are several options to choose from.
The two most popular logging frameworks for Go appear to be
glog and logrus. The popularity of glog is surprising, since it hasn’t been updated in several years. logrus is better maintained and used in popular projects like Docker, so we’ll be focusing on it.
Getting started with logrus
Installing logrus is as simple as running the command below in your terminal:
go get "github.com/Sirupsen/logrus"
One great thing about logrus is that it’s completely compatible with the log
package of the standard library, so you can replace your log imports everywhere with log "github.com/sirupsen/logrus"
and it will just work!
Let’s modify our earlier «hello world» example that used the log package and use logrus instead:
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.Println("Hello world!")
}
Running this code produces the output:
It couldn’t be any easier!
Logging in JSON
logrus
is well suited for structured logging in JSON which — as JSON is a well-defined standard — makes it easy for external services to parse your logs and also makes the addition of context to a log message relatively straightforward through the use of fields, as shown below:
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.SetFormatter(&log.JSONFormatter{})
log.WithFields(
log.Fields{
"foo": "foo",
"bar": "bar",
},
).Info("Something happened")
}
The log output generated will be a JSON object that includes the message, log level, timestamp, and included fields.
{"bar":"bar","foo":"foo","level":"info","msg":"Something happened","time":"2019-12-09T15:55:24+01:00"}
If you’re not interested in outputting your logs as JSON, be aware that several third-party formatters exist for logrus, which you can view on its Github page. You can even write your own formatter if you prefer.
Log levels
Unlike the standard log package, logrus supports log levels.
logrus has seven log levels: Trace, Debug, Info, Warn, Error, Fatal, and Panic. The severity of each level increases as you go down the list.
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
log.Fatal("Bye.")
// Calls panic() after logging
log.Panic("I'm bailing.")
By setting a logging level on a logger, you can log only the entries you need depending on your environment. By default, logrus will log anything that is Info or above (Warn, Error, Fatal, or Panic).
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.SetFormatter(&log.JSONFormatter{})
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
}
Running the above code will produce the following output:
{"level":"info","msg":"Something noteworthy happened!","time":"2019-12-09T16:18:21+01:00"}
{"level":"warning","msg":"You should probably take a look at this.","time":"2019-12-09T16:18:21+01:00"}
{"level":"error","msg":"Something failed but I'm not quitting.","time":"2019-12-09T16:18:21+01:00"}
Notice that the Debug level message was not printed. To include it in the
logs, set log.Level
to equal log.DebugLevel
:
log.SetLevel(log.DebugLevel)
Wrap up
In this post, we explored the use of the built-in log package and established that it should only be used for trivial applications or when building a quick prototype. For everything else, the use of a mainstream logging framework is a must.
We also looked at ways to ensure that the information contained in your
logs is consistent and easy to analyze, especially when aggregating it on a centralized platform.
Thanks for reading!
Logging is helpful for more than just tracking error conditions in your
application. It’s also a great way to record any notable events that occur
during the lifetime of a program so that you can have a good idea of what is
going on and what needs to be changed, dropped, or optimized further. Adopting
good logging practices provides valuable insights into the flow of your program,
and what parameters are responsible for various events, which is immensely
valuable when trying to reproduce problems in your application.
In this tutorial, we will discuss the basics of logging in Go starting with the
built-in log
package, and then proceed to discuss third-party logging
frameworks and how they go far beyond what the default log
package has to
offer. By following through with this article, you’ll learn the following Go
logging concepts:
- Pointers on when and what to log.
- How to use and customize the standard library log package.
- Limitations of the log package.
- Logging into files and options for rotating log files.
- Supercharging your Go logging setup with a logging framework.
Prerequisites
Before proceeding with this article, ensure that you have a recent version of Go
installed on your computer so that you can run the snippets and experiment with
some of the concepts that will be introduced in the following sections.
🔭 Want to centralize and monitor your Go application logs?
Head over to Logtail and start ingesting your logs in 5 minutes.
When to log
Logs need to communicate the various happenings in your application effectively
so they must be descriptive and provide enough context for you to understand
what happened, and what (if anything) needs to be done next. Logs are also
reactive because they can only tell you about something that has already
happened in your application.
You can read the log and take some action afterward to prevent that event from
happening again, but if you don’t have the log in the first place, you will be
at a disadvantage when trying to find the cause of a problem or gain insight
into some notable events.
Therefore, logging should start as early as possible during the development
process and it should be kept in place when the program is deployed to
production. Here are some general recommendations for where to log in your
application:
- At the beginning of an operation (e.g at the start of a scheduled job or when
handing incoming HTTP requests). - When a significant event occurs while the operation is in progress, especially
if the flow of the code changes entirely due to such an event. - When an operation is terminated regardless of whether it succeeds or fails.
What should you log
You should log all notable events in your program, and add sufficient context to
help you understand what triggered the event. For example, if a server error
occurs, the reason for the error and a stack trace should be included in the
corresponding log entry. If a failed login attempt is detected, you can log the
IP address of the client, user agent, username or id, and the reason for failure
(password incorrect, username invalid, etc.).
A high number of failed logins attempts for non-existent users or quickly
reaching the login attempts limits for many accounts may be an indicator of a
coordinated attack on your service, and you can set up alerts to draw your
attention to such issues when they arise if you’re processing your logs through
a log management service (see example from
Logtail below.
Let’s look at a practical example of what to include in a log entry for each
incoming HTTP request to your server. You should log at least the following:
- the route and query parameters (if any),
- request method (GET, POST, etc),
- response code,
- user agent, and
- time taken to complete the request.
Armed with this data, you can figure out your busiest routes, average response
times, if your service is experiencing some degradation in performance (due to
increased server errors or response times), and much more. You can also use the
captured data to build graphs and other visualizations, which could come in
handy when discussing trends and opportunities for future investment with the
management at your organization.
In the case of unexpected server errors (5xx), it may also be necessary to log
the request headers and body so that you can reproduce the request and track
down the issue, but ensure that you are careful not to leak sensitive details
when doing so as the headers and bodies of certain requests can contain
sensitive info like passwords, API keys, or credit card information. If you do
decide to log such data, ensure to write specific rules to sanitize it and
redact all the sensitive fields from the log entry.
What to avoid when logging
Generally, you should include as many details as possible in your log entries,
but you must also be careful not to add details that will make your logs
unnecessarily verbose as this will impact storage requirements and potentially
increase the cost of managing your logs.
As alluded to in the previous section, you should also avoid logging sensitive
information or Personally Identifiable Information (PII) such as phone numbers,
home addresses, and similar details, so you don’t fall afoul of regulations like
GDPR, CCPA, and other data compliance laws.
The Go standard library log package
Now that we’ve discussed a general strategy to help you get started quickly with
logging, let’s discuss the how of logging in Go applications. In this section,
We’ll explore the built-in log
package in the standard library designed to
handle simple logging concerns. While this package does not meet our criteria
for a good logging framework due to some
missing features, it’s still necessary to be
familiar with how it works as many in the Go community rely on it.
Here’s the most basic way to write a log message in Go:
package main
import "log"
func main() {
log.Println("Hello from Go application!")
}
Output
2022/08/10 11:19:52 Hello from Go application!
The output contains the log message and a timestamp in the local time zone that
indicates when the entry was generated. Println()
is one of methods accessible
on the preconfigured logger prints its output to the standard error. The
following other methods are available:
log.Print()
log.Printf()
log.Fatal()
log.Fatalf()
log.Fatalln()
log.Panic()
log.Panicf()
log.Panicln()
The difference between the Fatal
and Panic
methods above is that the former
calls os.Exit(1)
after logging a message, while the latter calls panic()
.
log.Fatalln("cannot connect to the database")
Output
2022/08/10 14:32:51 cannot connect to database
exit status 1
log.Panicln("cannot connect to the database")
Output
2022/08/10 14:34:07 cannot connect to database
panic: cannot connect to database
goroutine 1 [running]:
log.Panicln({0xc00006cf60?, 0x0?, 0x0?})
/usr/local/go/src/log/log.go:402 +0x65
main.main()
/home/ayo/dev/demo/random/main.go:6 +0x45
exit status 2
If you want to customize the default logger, you can call log.Default()
to
access it and then call the appropriate methods on the returned Logger
object.
For example, you can change the output of the logger to stdout
as shown below:
package main
import (
"log"
"os"
)
func main() {
defaultLogger := log.Default()
defaultLogger.SetOutput(os.Stdout)
log.Println("Hello from Go application!")
}
You can also create a completely custom logger through the log.New()
method
which has the following signature:
func New(out io.Writer, prefix string, flag int) *Logger
The first argument is the destination of the log messages produced by the
Logger
, which can be anything that implements the io.Writer
interface. The
second is a prefix that is prepended to each log message, while the third
specifies a set of constants that is
used to add details to each log message.
package main
import (
"log"
"os"
)
func main() {
logger := log.New(os.Stderr, "", log.Ldate|log.Ltime)
logger.Println("Hello from Go application!")
}
The above logger is configured to output to the standard error, and it uses the
initial values for the standard logger, which means the output from the logger
is the same as before:
Output
2022/08/10 11:19:52 Hello from Go application!
We can customize it further by adding the application name, file name, and line
number to the log entry. We’ll also add microseconds to the timestamp and cause
it to be presented in UTC instead of the local time zone:
logger := log.New(
os.Stderr,
"MyApplication: ",
log.Ldate|log.Ltime|log.Lmicroseconds|log.LUTC|log.Lshortfile,
)
logger.Println("Hello from Go application!")
The output becomes:
Output
MyApplication: 2022/08/10 13:55:48.380189 main.go:14: Hello from Go application!
The MyApplication:
prefix appears at the beginning of each log entry and the
timestamp (now UTC instead of the local time) now includes microseconds. The
point of log generation also included in the output to help you locate the
source of each entry in the codebase.
Logging to a file in Go
So far, we’ve seen several examples that log to the standard output or standard
error. Let’s now address the common need to transport logs into a file and also
how to rotate such log
files to prevent them
from growing too large which can make them cumbersome to work with.
Using shell redirection
The easiest way to output logs into a file is to keep logging to the console and
then use shell redirection to append each entry to a file:
go run main.go 2>> myapp.log
The above command redirects the standard error stream to a myapp.log
file in
the current directory.
Output
MyApplication: 2022/08/29 10:05:25.477612 main.go:14: Hello from Go application!
If you’re logging to the standard output, you can use 1>>
or >>
instead:
go run main.go >> myapp.log
To redirect both the standard output and standard error streams to a single
file, you can use &>>
.
go run main.go &>> myapp.log
You can also redirect each output stream to separate files as shown below:
go run main.go 2>> stderr.log >> stdout.log
Finally, you can use the tee
command to retain the console output while
redirecting one or both streams to a file:
go run main.go 2> >(tee -a stderr.log >&2)
go run main.go > >(tee -a stdout.log)
go run main.go > >(tee -a stdout.log) 2> >(tee -a stderr.log >&2)
Writing log messages directly to a file
A second way to log into files in Go is to open the desired file in the program
and write to it directly. When using this approach, you need to ensure that the
file is created or opened with the right permissions to ensure that the program
can write to it.
package main
import (
"log"
"os"
)
func main() {
// create the file if it does not exist, otherwise append to it
file, err := os.OpenFile(
"myapp.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0664,
)
if err != nil {
panic(err)
}
defer file.Close()
logger := log.New(
file,
"MyApplication: ",
log.Ldate|log.Ltime|log.Lmicroseconds|log.LUTC|log.Lshortfile,
)
logger.Println("Hello from Go application!")
}
Since the os.File
type implements a Write()
method that satisfies the
io.Writer
interface, we can open any file and use it as the output for our
logger
as shown above. If you execute the code, you’ll notice that the log
message is placed in a myapp.log
file in the current directory:
Output
. . .
MyApplication: 2022/08/10 14:12:24.638976 main.go:26: Hello from Go application!
You can also use the io.MultiWriter()
method to log to multiple writers at
once such as multiple files or a file and the standard error (or output) at
once:
. . .
logger := log.New(
io.MultiWriter(file, os.Stderr), // log to both a file and the standard error
"MyApplication: ",
log.Ldate|log.Ltime|log.Lmicroseconds|log.LUTC|log.Lshortfile,
)
. . .
When you run the program again, you’ll notice that the log entry is recorded to
the console and the myapp.log
file simultaneously.
Rotating log files
When logging to files, you should take the time to setup a log rotation
policy so that the file
sizes are kept manageable, and older logs get deleted automatically to save
storage space.
While you can rotate the files yourself by appending a timestamp to the log
filename coupled with some checks to delete older files, we generally recommend
using an external program like
logrotate, a standard utility on Linux
, or a well tested third-party Go package for this purpose.
We prefer the former approach since it’s the standard way to solve this problem
on Linux, and it is more flexible than other solutions. It can also copy and
truncate a file so that file deletion doesn’t occur in the middle of writing a
log entry which can be disruptive to the application. If you want to learn more
about rotating log files using
logrotate
, please see
the linked tutorial on the subject.
You can also utilize Lumberjack a
rolling file logger package for Go applications as shown below:
package main
import (
"log"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
fileLogger := &lumberjack.Logger{
Filename: "myapp.log",
MaxSize: 10, // megabytes
MaxBackups: 10, // files
MaxAge: 14, // days
Compress: true, // disabled by default
}
logger := log.New(
fileLogger,
"MyApplication: ",
log.Ldate|log.Ltime|log.Lmicroseconds|log.LUTC|log.Lshortfile,
)
logger.Println("Hello from Go application!")
}
With the above configuration in place, the myapp.log
file will continue to be
populated until it reaches 10 megabytes. Afterward, it will be renamed to
myapp-<timestamp>.log
(such as myapp-2022-08-10T18-30-00.000.log
) and a new
file with the original name (myapp.log
) is created. The backup files are also
gzip compressed so that they take up less space on the server, but you can
disable this feature if you want. When the number of backup files exceeds 10,
the older ones will be deleted automatically. The same thing happens if the file
exceeds the number of days configured in MaxAge
regardless of the number of
files present.
Limitations of the log package
While the log
package is a handy way to get started with logging in Go, it
does have several limitations that make it less than ideal for production
applications whose logs are meant to be processed by machines for monitoring and
further analysis.
Lack of log levels
Levelled logging are one of the most sought-after features in a
logging package, but they are strangely missing from the log
package in Go.
You can fix this omission by create a custom log package that builds on the
standard log
as demonstrated in
this GitHub gist.
It provides the ability to use leveled methods (Debug()
, Error()
, etc), and
turn off certain logs based on their level.
You can utilize the custom log package in your application as follows:
package main
import (
"log"
"os"
"github.com/username/project/internal/logger"
)
func main() {
l := log.New(
os.Stderr,
"MyApplication: ",
log.Ldate|log.Ltime|log.Lmicroseconds|log.LUTC,
)
logger.SetLevel(2) // default to the info level
logger.SetLogger(l) // Override the default logger
logger.Trace("trace message")
logger.Debug("debug message")
logger.Info("info message")
logger.Warn("warning message")
logger.Error("error message")
logger.Fatal("fatal message")
}
Notice that the Trace
and Debug()
methods do not produce any output proving
that the call to logger.SetLevel()
had the desired effect:
Output
MyApplication: 2022/08/10 16:22:45.146744 [INFO] info message
MyApplication: 2022/08/10 16:22:45.146775 [WARN] warning message
MyApplication: 2022/08/10 16:22:45.146789 [ERR] error message
MyApplication: 2022/08/10 16:22:45.146800 [FATAL] fatal message
2. Lack of support for structured logging
The use of basic or unstructured logs is being gradually phased out in the tech
industry as more organizations adopt the use of a structured format (usually
JSON) which enables each log entry to be treated as data that can be
automatically parsed by machines for monitoring, alerting, and other forms of
analysis.
Due to this trend, many logging frameworks have added structured logging APIs to
their public interface, and some support structured logging only. However, the
log
package in Go is not of them as it only supports the basic string
formatted logs through its printf-style methods. If you desire to output your Go
logs in a structured format, you have no choice but to use a third-party logging
framework. In the next section, we will consider a few packages that make
structured logging in Go a breeze.
Third-party logging libraries to consider
Due to the above limitations, the standard library log
package in Go should
generally be used in logging contexts where humans are the primary audience of
the logs. For everyone else, it’s necessary to adopt one of the Go logging
packages discussed below:
1. Zerolog
Zerolog is a structured logging package for Go that boasts of a great
development experience and
impressive performance when compared to
alternative libraries. It offers a chaining API that allows you to specify the
type of each field added to a
log entry which helps with avoiding unnecessary allocations and reflection.
Zerolog only supports JSON or the lesser-known
Concise Binary Object Representation (CBOR) format, but it
also provides a way to prettify its output in development environments.
package main
import (
"github.com/rs/zerolog/log"
)
func main() {
log.Info().
Str("name", "John").
Int("age", 9).
Msg("hello from zerolog")
}
Output
{"level":"info","name":"John","age":9,"time":"2022-08-11T21:28:12+01:00","message":"hello from zerolog"}
You can import a pre-configured global logger (as shown above) or use
zerolog.New()
to create a customizable logger instance. You can also create
child loggers with additional context which can come in handy for logging in
various packages or components in an application. Zerolog also helps you
adequately log errors by providing the ability to include a formatted stacktrace
through its integration with the popular errors
package. It also provides a set of
helper functions for
better integration with HTTP handlers.
2. Zap
Uber’s Zap library pioneered the
reflection-free, zero-allocation logging approach adopted by Zerolog. Still, it
also supports a more loosely typed API that can be used when ergonomics and
flexibility are the overriding concern when logging. This less verbose API
(zap.SugaredLogger
) supports both structured and formatted string logs, while
the base zap.Logger
type supports only structured logs in JSON format by
default. The good news is that you don’t have to pick one or the other
throughout your codebase. You can use both and convert between the two freely at
any time.
package main
import (
"fmt"
"time"
"go.uber.org/zap"
)
func main() {
// returns zap.Logger, a strongly typed logging API
logger, _ := zap.NewProduction()
start := time.Now()
logger.Info("Hello from zap Logger",
zap.String("name", "John"),
zap.Int("age", 9),
zap.String("email", "[email protected]"),
)
// convert zap.Logger to zap.SugaredLogger for a more flexible and loose API
// that's still faster than most other structured logging implementations
sugar := logger.Sugar()
sugar.Warnf("something bad is about to happen")
sugar.Errorw("something bad happened",
"error", fmt.Errorf("oh no!"),
"answer", 42,
)
// you can freely convert back to the base `zap.Logger` type at the boundaries
// of performance-sensitive operations.
logger = sugar.Desugar()
logger.Warn("the operation took longer than expected",
zap.Int64("time_taken_ms", time.Since(start).Milliseconds()),
)
}
Output
{"level":"info","ts":1660252436.0265622,"caller":"random/main.go:16","msg":"Hello from zap Logger","name":"John","age":9,"email":"[email protected]"}
{"level":"warn","ts":1660252436.0271666,"caller":"random/main.go:24","msg":"something bad is about to happen"}
{"level":"error","ts":1660252436.0275867,"caller":"random/main.go:25","msg":"something bad happened","error":"oh no!","answer":42,"stacktrace":"main.mainnt/home/ayo/dev/demo/random/main.go:25nruntime.mainnt/usr/local/go/src/runtime/proc.go:250"}
{"level":"warn","ts":1660252436.0280342,"caller":"random/main.go:33","msg":"the operation took longer than expected","time_taken_ms":1}
Unlike Zerolog, Zap does not provide a functioning global logger by default but
you can configure one yourself through its
ReplaceGlobals()
function. Another difference between the two is that Zap
does not support the TRACE
log level
at the time of writing, which may be a deal-breaker for some. In Zap’s favor,
you can greatly customize its behavior by implementing the interfaces in the
Zapcore package. For example, you
can output your logs in a custom format (like
lgofmt), or transport them directly
to a log aggregation and monitoring service like
Logtail.
Honourable mentions
Logrus was the default structured logging
framework for Go for a long time until it was surpassed by the aforementioned
Zap and Zerolog. Its major advantage was that it was API compatible with the
standard library log
package while providing structured, leveled logging in
JSON or other formats. It is
currently in maintenance mode so no new features will be added, but it will
continue to be maintained for security, bug fixes, and performance improvements
where possible.
Log15’s primary goal is to provide a
structured logging API that outputs logs in a human and machine readable format.
It uses the Logfmt format by default to aid this goal although this can easily
be changed to JSON. It also provides built-in support for logging to files,
Syslog, and the network, and it
is also quite extensible through its
Handler interface.
Apex/log is a structured logging framework
inspired by Logrus, but with a simplified API along with several
built-in handlers that help to facilitate log
centralization. At the time of writing, it has not been updated in two years so
we’re not sure if it’s still being maintained.
Logr is not a logging framework, but an
interface that aims to decouple the practice of structured logging in a Go
application from a particular implementation. This means you get to write all
your logging calls in terms of the APIs provided on the logr.Logger
interface,
while the actual logging implementation (Zap, Zerolog, or something else) is
managed in one place to ease future migrations.
Final thoughts
The log
package in the Go standard library provides a simple way to get
started with logging, but you will likely find that you need to reach out for a
third-party framework to solve many common logging concerns. Once you’ve settled
on a solution that works for your use case, you should consider supercharging
your logs by sending them to a
log management platform, where they can be
monitored and analyzed in-depth.
Thanks for reading, and happy logging!
Centralize all your logs into one place.
Analyze, correlate and filter logs with SQL.
Create actionable
dashboards.
Share and comment with built-in collaboration.
Got an article suggestion?
Let us know
Next article
A Complete Guide to Logging in Go with Zerolog
Zerolog is a high-performance Go structured logging library aimed at latency-sensitive applications where garbage collections are undesirable
→
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Логирование программ Golang
Журнал пакетов стандартной библиотеки предоставляет базовую инфраструктуру для управления логами на языке GO. Основная цель ведения логов — отследить, что происходит в программе, где это происходит и когда это происходит. Логи могут обеспечивать отслеживание кода, профилирование и аналитику. Ведение журнала логирования( глаза и уши программиста) — это способ найти эти ошибки и узнать больше о том, как работает программа.
Для работы с журналом логов мы должны импортировать модуль Log:
import (
"log"
)
В самом простом случае модуль Log формирует сообщения и отправляет их на стандартную проверку ошибок, как показано в следующем примере:
// Program in GO language to demonstrates how to use base log package.
package main
import (
"log"
)
func init(){
log.SetPrefix("LOG: ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
log.Println("init started")
}
func main() {
// Println writes to the standard logger.
log.Println("main started")
// Fatalln is Println() followed by a call to os.Exit(1)
log.Fatalln("fatal message")
// Panicln is Println() followed by a call to panic()
log.Panicln("panic message")
}
После выполнения этого кода вывод будет выглядеть так:
LOG: 2017/06/25 14:49:41.989813 C:/golang/example38.go:11: init started
LOG: 2017/06/25 14:49:41.990813 C:/golang/example38.go:15: main started
LOG: 2017/06/25 14:49:41.990813 C:/golang/example38.go:18: fatal message
exit status 1
Стандартная запись логов содержит следующую информцию: - префикс (log.SetPrefix("LOG: ")) - отметка даты и времени (log.Ldate) - полный путь к файлу исходного кода записи в лог (log.Llongfile) - строка кода Эта часть информации автоматически генерируется для нас, информация о том, когда произошло событие, и информация о том, где оно произошло. Println — это стандартный способ записи сообщений журнала. Fatalln - библиотека выводит сообщение об ошибке, а затем вызывает os.Exit(1), заставляя программу завершить работу. Panic — это встроенная функция, которая останавливает обычный поток управления и начинает паниковать. Когда функция F вызывает panic, выполнение F останавливается, все отложенные вызовы в F выполняются нормально, затем F возвращает управление вызывающей функции. Для вызывающей функции вызов F ведёт себя как вызов panic. Процесс продолжается вверх по стеку, пока все функции в текущей го-процедуре не завершат выполнение, после чего аварийно останавливается программа. Паника может быть вызвана прямым вызовом panic, а также вследствие ошибок времени выполнения, таких как доступ вне границ массива.
Программа на языке GO с реальным примером ведения журнала логов. Теперь приведем пример из реального мира и реализуем вышеуказанный пакет log в своей программе. Проверим, нормально ли работает SMTP-соединение. Для тестового примера я собираюсь подключиться к несуществующему SMTP-серверу smtp.smail.com, поэтому программа завершится с сообщением ошибки.
// Program in GO language with real world example of logging.
package main
import (
"log"
"net/smtp"
)
func init(){
log.SetPrefix("TRACE: ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
log.Println("init started")
}
func main() {
// Connect to the remote SMTP server.
client, err := smtp.Dial("smtp.smail.com:25")
if err != nil {
log.Fatalln(err)
}
client.Data()
}
Вывод:
TRACE: 2017/06/25 14:54:42.662011 C:/golang/example39.go:9: init started
TRACE: 2017/06/25 14:55:03.685213 C:/golang/example39.go:15: dial tcp 23.27.98.252:25: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
exit status 1
еще пример:
package main
import (
"log"
"os"
)
var (
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLogger = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
InfoLogger.Println("Starting the application...")
InfoLogger.Println("Something noteworthy happened")
WarningLogger.Println("There is something you should know about")
ErrorLogger.Println("Something went wrong")
}
Просмотры: 1 353