하스켈에서 어떤 Logger를 쓸까?

Eunmin Kim·2022년 6월 6일
2

하스켈 일기

목록 보기
6/12
post-thumbnail

오늘은 Logger에 대해 지금까지 탐구한 내용을 적어보려고 한다. 지금 만들고 있는 API 서버는 Docker로 빌드해서 GPC에 있는 쿠버네티스(GKE)에 배포한다. 카카오에 있을 때는 로그 플랫폼을 전담하는 팀이 있어 서비스 개발팀은 로그 플랫폼을 구성할 필요가 없었다. 그냥 인스턴스에서 JSON 형태로 구조화해 Stdout으로 남기면 모든 인스턴스가 남기는 로그를 수집해 Kibana로 조회할 수 있었다. 이제 로깅을 위한 솔루션을 찾아봐야 할 것 같은데 그전에 애플리케이션에서 구조화된 로그(Structured Logging)를 남길 수 있어야 편리할 것 같다.
찾아보니 하스켈에는 몇 가지 Logger 패키지가 있었다. monad-logger, logging-effect, Katip이 있었는데, 많이 쓰는 패키지는 monad-logger였지만 JSON 포맷으로 Structured Logging을 쉽게 남길 수 있는 패키지는 Katip이었다. 그리고 자주 언급하고 있는 Practical Web Development with Haskell 책에서도 사용했다. 그래서 우리 프로젝트에도 Katip을 적용했다. 그런데 최근에 구독하고 있는 Haskell Weekly 뉴스에서 monad-logger를 사용하면서 JSON으로 로그를 남길 수 있는 monad-logger-aeson 에 대해 소개하는 글을 봤다. monad-loggger가 더 많이 쓰는 라이브러리기 같아서 monad-logger-aeson도 살펴보기로 했다.
먼저 Katip을 살펴보자.

Katip

Katip을 사용하려면 katip 의존성을 추가해야 한다.

dependencies:
  - katip

Katip도 다른 하스켈 라이브러리처럼 Katip 모나드 콘텍스트 안에서 로그를 남길 수 있다.

app :: KatipContextT IO Int
app = do
  let user = User 1 "Todd"
  katipAddContext (sl "user" user) $ do
      $(logTM) InfoS "Hello Katip"
  pure 1

katipContext로 로그와 함께 추가 정보를 남길 수 있다. 여기서 "user"는 키 값이고 뒤에 User 타입은 JSON으로 바꿀 수 있게 Aeson의 ToJSON 인스턴스가 있어야 한다. 이렇게 남긴 로그는 다음과 같은 형태로 남는다. $(logTM) 하스켈 템플릿으로 로그 수준과 함께 남기는 방법이 간단하지만 로그를 남기는 함수가 여러 가지 있다.

{
  "data": {
    "user": {
      "userName": "Todd",
      "userId": 1
    }
  },
  "msg": "Hello Katip",
  ... 생략

Context로 남긴 데이터는 data 키 아래 남는다. 생략된 부분에는 Katip에서 기본으로 남겨주는 여러 가지 정보가 있다. 어떤 것이 남는지 보려면 Katip에 Item 데이터 타입을 보면 된다. Katip은 로그 포맷이나 로그를 어디에 남길지 정할 수 있는 확장성이 있다. Scribe라고 하는 것에 로그 포맷이나 로그를 기록하는 함수를 넣을 수 있다. 다음은 JSON 포맷으로 Stdout으로 남기는 scribe이다.

scribe <- mkHandleScribeWithFormatter jsonFormat ColorIfTerminal stdout (permitItem InfoS) V2

scribe는 mkHandleScribeWithFormatter 함수로 만들 수 있고 IO 콘텍스트에서 만들 수 있다. 그리고 LogEnv라고 하는 로그 환경에 scribe를 등록해주면 된다. 등록된 scribecloseScribes로 닫아 줘야 한다. 그래야 각종 리소스가 해제된다.

scribe <- mkHandleScribeWithFormatter jsonFormat ColorIfTerminal stdout (permitItem InfoS) V2
  logEnv <- initLogEnv "MyApp" "dev"
  le <- registerScribe "stdout" scribe defaultScribeSettings logEnv

LogEnvKatipContextT를 모나드 트랜스포머를 실행하는 runKatipContextT에 인자로 전달한다.

result <- runKatipContextT le () "main" app

다음으로 monad-logger-aeson을 살펴보자.

monad-logger-aeson

monad-logger-aeson으로도 JSON 형식의 로그를 쉽게 남길 수 있다. monad-logger 함수를 거의 그대로 사용하지만 기존 monad-logger에 있는 logDebug와 같은 함수 뒤에 추가 정보를 남길 수 있는 버전이 있다.

app :: LoggingT IO Int
app = do
  let user = User 1 "Todd"
  withThreadContext ["user" .= user ] $ do
    logDebug $ "Hello monad-logger-aeson"  :# [ "user" .= user ]
  pure 1

Katip처럼 Context에 추가 데이터를 남길 수 있고 인라인 로그에도 추가 정보를 남길 수 있다. 이렇게 남긴 로그는 다음과 같은 JSON 형식으로 남는다.

{
"context": {
    "user": {
      "userName": "Todd",
      "userId": 1
    }
  },
  "message": {
    "text": "Hello monad-logger-aeson",
    "meta": {
      "user": {
        "userName": "Todd",
        "userId": 1
      }
    }
  }
  ... 생략

Katip과 다른 점은 Katip은 data 키 아래 추가 정보가 남는데 monad-logger-aeson은 Context로 남긴 추가 정보는 context 키 아래 남고 인라인 로그로 남긴 추가 정보는 message 키 아래 meta 키 아래 남는다. 아쉽게도 아직 meta 키라는 이름을 바꿀 수 있는 방법은 없다. monad-logger-aeson의 LoggerT를 실행하는 방법은 runLoggerT에 여러 설정을 넘겨서 할 수 있고 편의 함수로 Stdout으로 남기는 runStdoutLoggingT와 같은 함수가 있다.

main :: IO ()
main = do 
	runStdoutLoggingT app
    pure ()

전체 코드

{-# LANGUAGE DeriveAnyClass  #-}
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import           Control.Monad.Logger.Aeson
import           Data.Aeson
import           GHC.Generics               (Generic)
import           Katip
import           System.IO

data User = User
  { userId   :: Int
  , userName :: String
  } deriving (Eq, Show, Generic, ToJSON)

app :: KatipContextT IO Int
app = do
  let user = User 1 "Todd"
  katipAddContext (sl "user" user) $ do
      $(logTM) InfoS "Hello Katip"
  pure 1

app2 :: LoggingT IO Int
app2 = do
  let user = User 1 "Todd"
  withThreadContext ["user" .= user ] $ do
    logDebug $ "Hello monad-logger-aeson"  :# [ "user" .= user ]
  pure 1

main :: IO ()
main = do
  -- with Katip
  scribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V2
  scribe <- mkHandleScribeWithFormatter jsonFormat ColorIfTerminal stdout (permitItem InfoS) V2
  logEnv <- initLogEnv "MyApp" "dev"
  le <- registerScribe "stdout" scribe defaultScribeSettings logEnv
  result <- runKatipContextT le () "main" app
  closeScribes le

  -- with monad-logger-aeson
  runStdoutLoggingT app2
  pure ()

정리

간단하게 살펴봐서 아직 어떤 것이 더 좋을지 판단하기 어렵지만 사용하기에는 monad-logger-aeson이 쉬운 것 같고 확장성은 Katip이 더 좋은 것 같은 느낌이다.

profile
Functional Programmer @Constacts, Inc.

0개의 댓글