Ведение журнала в Go

Завершено

Журналы играют важную роль в программе, так как они становятся источником информации, к которой можно обратиться, когда что-то пойдет не так. Как правило, при возникновении ошибки конечные пользователи просто видят сообщение о проблеме с программой. Нам как разработчикам требуется больше информации об ошибке, чем заключено в простом сообщении. Это связано с тем, что мы хотим воспроизвести проблему, чтобы внести в программу нужное исправление. В этом модуле вы узнаете, как записывать сообщения в журнал в Go. Вы также узнаете о некоторых методиках, которые всегда следует реализовывать.

Пакет log

Прежде всего, в Go есть простой стандартный пакет для работы с журналами. Его можно использовать так же, как и пакет fmt. Стандартный пакет не позволяет использовать уровни ведения журнала и настраивать отдельные средства ведения журнала для каждого пакета. Если вам необходимы более сложные конфигурации ведения журнала, можно использовать платформу ведения журналов. Мы рассмотрим их позже.

Вот самый простой способ использования журналов:

import (
    "log"
)

func main() {
    log.Print("Hey, I'm a log!")
}

При выполнении приведенного выше кода получается следующий результат:

2020/12/19 13:39:17 Hey, I'm a log!

По умолчанию функция log.Print() включает дату и время в качестве префикса в сообщения журнала. Тот же результат можно было получить с помощью функции fmt.Print(), но с пакетом log можно выполнять другие действия, например, отправлять журналы в файл. Мы рассмотрим другие функции пакета log позже.

С помощью функции log.Fatal() можно записать ошибку в журнал и завершить программу так же, как если бы использовалась функция os.Exit(1). Чтобы попробовать эту функцию в действии, воспользуемся следующим фрагментом кода:

package main

import (
    "fmt"
    "log"
)

func main() {
    log.Fatal("Hey, I'm an error log!")
    fmt.Print("Can you see me?")
}

При выполнении приведенного выше кода получается следующий результат:

2020/12/19 13:53:19  Hey, I'm an error log!
exit status 1

Обратите внимание, что последняя строка, fmt.Print("Can you see me?"), не выполняется. Это связано с тем, что при вызове функции log.Fatal() программа останавливается. Похожее поведение наблюдается при использовании функции log.Panic(), которая также вызывает функцию panic():

package main

import (
    "fmt"
    "log"
)

func main() {
    log.Panic("Hey, I'm an error log!")
    fmt.Print("Can you see me?")
}

При выполнении приведенного выше кода получается следующий результат:

2020/12/19 13:53:19  Hey, I'm an error log!
panic: Hey, I'm an error log!

goroutine 1 [running]:
log.Panic(0xc000060f58, 0x1, 0x1)
        /usr/local/Cellar/go/1.15.5/libexec/src/log/log.go:351 +0xae
main.main()
        /Users/christian/go/src/helloworld/logs.go:9 +0x65
exit status 2

Вы по-прежнему получаете сообщение журнала, но теперь также получаете трассировку стека для ошибки.

Еще одна важная функция — log.SetPrefix(). Ее можно использовать для добавления префикса к сообщениям, записываемым программой в журнал. Например, рассмотрим следующий фрагмент кода:

package main

import (
    "log"
)

func main() {
    log.SetPrefix("main(): ")
    log.Print("Hey, I'm a log!")
    log.Fatal("Hey, I'm an error log!")
}

При выполнении приведенного выше кода получается следующий результат:

main(): 2021/01/05 13:59:58 Hey, I'm a log!
main(): 2021/01/05 13:59:58 Hey, I'm an error log!
exit status 1

Префикс задается один раз и позволяет указать в сообщениях журнала дополнительную информацию, например имя функции, из которой было записано сообщение.

На сайте Go можно ознакомиться с другими функциями.

Ведение журнала в файле

Наряду с выводом сообщений журнала в консоль может потребоваться отправлять эти сообщения в файл для последующей обработки или для обработки в режиме реального времени.

Зачем нужно отправить сообщения журнала в файл? Во-первых, может потребоваться скрыть определенные сведения от конечных пользователей. Эти сведения могут не представлять для них интереса или содержать конфиденциальную информацию. Настроив запись сообщений в файлы, можно централизованно разместить все журналы в одном расположении и связать их с другими событиями. Это типичная схема для распределенных приложений, которые могут быть временными, таких как контейнеры.

Используйте следующий код для тестирования отправки сообщений журнала в файл:

package main

import (
    "log"
    "os"
)

func main() {
    file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }

    defer file.Close()

    log.SetOutput(file)
    log.Print("Hey, I'm a log!")
}

При выполнении приведенного выше кода никаких сообщений в консоли не отображается. В каталоге вы увидите новый файл с именем info.log, содержащий сообщения журнала, которые были отправлены с помощью функции log.Print(). Обратите внимание, что сначала необходимо создать или открыть файл, а затем настроить пакет log для отправки всех выходных данных в файл. Затем можно продолжить использовать функцию log.Print() как обычно.

Платформы ведения журналов

Наконец, могут возникнуть ситуации, когда функций пакета log недостаточно. В таких случаях вместо написания собственных библиотек стоит использовать платформу ведения журналов. Некоторые из платформ ведения журналов для Go — это Logrus, zerolog, zap и Apex.

Давайте посмотрим, что можно сделать с помощью платформы zerolog.

Сначала необходимо установить пакет. Если вы уже изучали другие руководства в этой серии, то, скорее всего, уже используете модули Go. В этом случае никаких дополнительных действий не требуется. На всякий случай можно выполнить следующую команду на рабочей станции, чтобы установить библиотеки zerolog:

go get -u github.com/rs/zerolog/log

Теперь воспользуйтесь следующим фрагментом кода, чтобы опробовать платформу в действии:

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Print("Hey! I'm a log message!")
}

При выполнении приведенного выше кода получается следующий результат:

{"level":"debug","time":1609855453,"message":"Hey! I'm a log message!"}

Обратите внимание, что достаточно просто указать правильные имена в операторе import, после чего можно продолжать использовать функцию log.Print() как обычно. Также обратите внимание, что выходные данные теперь имеют формат JSON. Этот формат удобен для журналов при поиске в централизованном расположении.

Еще одна полезная возможность заключается в том, что можно быстро включить контекстные данные, например, так:

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix

    log.Debug().
        Int("EmployeeID", 1001).
        Msg("Getting employee information")

    log.Debug().
        Str("Name", "John").
        Send()
}

При выполнении приведенного выше кода получается следующий результат:

{"level":"debug","EmployeeID":1001,"time":1609855731,"message":"Getting employee information"}
{"level":"debug","Name":"John","time":1609855731}

Обратите внимание на то, как мы добавили идентификатор сотрудника в качестве контекста. Он станет частью строки журнала в качестве еще одного свойства. Кроме того, важно подчеркнуть, что включаемые поля являются строго типизированными.

С помощью платформы zerolog можно реализовать и другие функции, например уровни ведения журналов, использование форматированных трассировок стека и применение нескольких экземпляров средства ведения журнала для управления различными выходными данными. Дополнительные сведения см. на сайте GitHub.