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!")
}

위 코드를 실행하면 콘솔에 아무것도 표시되지 않습니다. 디렉터리를 확인하면 log.Print() 함수를 사용하여 전송한 로그가 포함된 info.log라는 새 파일을 볼 수 있습니다. 먼저 파일을 만들거나 연 다음 모든 출력이 파일로 전송되도록 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 형식으로 변경되는 것도 알 수 있습니다. 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}

직원 ID를 컨텍스트로 추가한 것을 알 수 있습니다. 이제 직원 ID는 로그 줄에서 또 하나의 속성이 됩니다. 사용자가 포함하는 필드에는 엄격한 형식이 적용된다는 사실도 알아두세요.

zerolog를 사용하여 수준별 로깅 사용하기, 서식이 지정된 스택 추적 사용하기, 둘 이상의 로거 인스턴스를 사용하여 여러 출력 관리하기와 같은 기능도 구현할 수 있습니다. 자세한 내용은 GitHub 사이트를 참조하세요.