개발자의 오르막
Golang Logrus 로 Json Logging 하기 본문
이번에 포스팅할 주제는 Logrus 라이브러리를 활용한 Json Logging 입니다.
Why Json Logging ?
Json Logging 을 하는 커다란 이유는 logstash 와 같은 로그 수집을 위해서입니다.
하나의 WAS 로 이루어진 어플리케이션이라면 해당 서버에서만 로그를 확인하면 됩니다.
그러나 WAS 도 여러 개의 서버로 분산될 수 있으며, 하나의 로직(작업) 자체가 여러 미들웨어 서버로 분산될 수 있습니다.
여러 서버에 분산된 로그를 한 곳으로 수집하고, 데이터 가시화를 도와주는 것이 logstash 입니다.
이를 위해 우리는 JSON Format 으로 로그 수집이 용이하게 로그를 남길 필요가 있습니다.
Golang 에서의 Logrus 오픈소스
Golang 에서는 내장 패키지로 제공하는 log 기능이 있으나, 필드를 추가하여 로그를 남길 수 있는 기능, Json Logging 등 다양한 커스텀 기능을 지원하는 대표적인 오픈소스 라이브러리입니다.
- 공식 Logrus Docs
https://pkg.go.dev/github.com/sirupsen/logrus#section-readme
Json Logging 지원
logrus 에서는 Json Formatter 를 지원하여 logstash에서 쉽게 구문 분석을 할 수 있습니다.
- log.SetFormatter(&log.JSONFormatter{})
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
{"level":"warning","msg":"The group's number increased tremendously!",
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
이번 시간에 저희는 아래의 요구사항을 Logrus 로 구현해보도록 하겠습니다.
요구사항
- String 이 아닌 Json Format 형태로 로깅
- 로깅 시 커스텀 필드 추가
- 날짜별로 로깅파일 분리
- Json 필드의 값이 빈 값일 경우 생략처리
Logrus Json Logging 구현
main.go
uploadLogger, err := util.LogInitialize(conf.UploadLogInfo.LogPath, conf.UploadLogInfo.LogLevel)
- 실행함수에서 쓰고자 하는 Logger 객체를 선언합니다.1
- 이때 LogFile 이 저장되는 경로와 LogLevel 을 config 파일에서 받아와 주입시켜줍니다.
utils/logger.go
import "github.com/sirupsen/logrus"
type UploadLogger struct {
log *logrus.Logger
}
- 사용하고자 하는 Logrus 라이브러리 의존성을 import 해준다.
- UploadLogger 라는 구조체를 선언하고, 해당 필드로 라이브러리 의존성을 주입해준다.
func LogInitialize(fileName, level string) (*UploadLogger, error) {
l := new(UploadLogger)
l.log = logrus.New()
lv := l.getLevel(level)
l.log.SetOutput(os.Stdout)
l.log.SetLevel(lv)
return l, nil
}
// getLevel changes the value entered in string form to logrus.Level.
func (l *UploadLogger) getLevel(level string) (lv logrus.Level) {
lv = logrus.InfoLevel
switch strings.ToLower(level) {
case "debug":
lv = logrus.DebugLevel
case "info":
lv = logrus.InfoLevel
case "warn":
lv = logrus.WarnLevel
case "error":
lv = logrus.ErrorLevel
default:
logrus.Info("Unknown level string.")
}
return
}
- UploadLogger 구조체를 생성해줍니다.
- logrus 의 level 을 선언하는 메서드와 logrus에서 실행할 메서드를 선언해줍니다.
- UploadLogger 구조체를 반환하여 프로젝트에서 해당 구조체를 사용할 수 있도록 해줍니다.
func (l *UploadLogger) Info(prefix *logrus.Entry, format string, args ...interface{}) {
if l.log.Level >= logrus.InfoLevel {
prefix.Data["file"] = FileInfo(2)
prefix.Infof(format, args...)
}
}
func (l *UploadLogger) Trace(prefix *logrus.Entry, format string, args ...interface{}) {
if l.log.Level >= logrus.TraceLevel {
prefix.Data["file"] = FileInfo(2)
prefix.Debugf(format, args...)
}
}
func (l *UploadLogger) Debug(prefix *logrus.Entry, format string, args ...interface{}) {
if l.log.Level >= logrus.DebugLevel {
prefix.Data["file"] = FileInfo(2)
prefix.Debugf(format, args...)
}
}
func (l *UploadLogger) Warn(prefix *logrus.Entry, format string, args ...interface{}) {
if l.log.Level >= logrus.WarnLevel {
prefix.Data["file"] = FileInfo(2)
prefix.Debugf(format, args...)
}
}
func (l *UploadLogger) Error(prefix *logrus.Entry, format string, args ...interface{}) {
if l.log.Level >= logrus.ErrorLevel {
prefix.Data["file"] = FileInfo(2)
prefix.Errorf(format, args...)
}
}
func (l *UploadLogger) WithField(key string, obj interface{}) *logrus.Entry {
return (*logrus.Entry)(l.log.WithField(key, obj))
}
func (l *UploadLogger) WithFields(fields logrus.Fields) *logrus.Entry {
return (*logrus.Entry)(l.log.WithFields(logrus.Fields(fields)))
}
func FileInfo(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
file = "<???>"
line = 1
} else {
slash := strings.LastIndex(file, "/")
if slash >= 0 {
file = file[slash+1:]
}
}
return fmt.Sprintf("%s:%d", file, line)
}
- 각 로그의 레벨에 대한 메서드를 정의해줍니다.
- 프로젝트에서 해당 레벨의 로깅을 선언할 때마다 필요한 셋팅을 합니다.
Process.go
up.uploadLogger.Info(prefix, "[INFO][STEP(1/13)] ["+sessionID+"] CreateOneTimeURL : ", "Module_"+uploadFileKey)
- 프로젝트에서 위와 같이 원하는 위치에서 로깅을 하면, JsonFormat 으로 로그가 찍히게 됩니다.
- Json Logging 형태
{"current_step":2,"file":"functions.go:88","level":"info","msg":"[INFO][STEP(2/13)] [20221103-831b7f6f2adcdab7] CreateUploadSession","time":"2022-11-03T10:58:55.064462347+09:00","total_step":13,"upload_type":"n"}
File-RotateLogs Logging 구현
위의 Json Logging 으로 Json 형태의 로그를 Console 에 출력할 수 있습니다.
그러나 우리는 LogPath 로 넘겨받은 경로로 로그파일을 생성하고자 합니다.
또한 로그파일이 날짜별로 Rolling 되기를 원합니다.
이때 같이 쓸 수 있는 라이브러리로 File-RotateLogs 가 있습니다.
https://github.com/lestrrat-go/file-rotatelogs
- utils/logger.go Import 문에 추가
import (
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/sirupsen/logrus"
)
// LogInitialize Logger Initial
func LogInitialize(fileName, level string) (*UploadLogger, error) {
l := new(UploadLogger)
l.log = logrus.New()
lv := l.getLevel(level)
l.log.SetFormatter(&logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano})
if len(fileName) > 0 {
apiLogger, err := SetRollingLogFile(fileName)
if err != nil {
log.Printf(fileName+" : %s", err.Error())
return nil, err
}
l.log.SetOutput(apiLogger)
} else {
l.log.SetOutput(os.Stdout)
}
l.log.SetLevel(lv)
return l, nil
}
- LogInitialize 에 log.SetOutput 을 변경
// SetRollingLogFile periodically changes the log file.
func SetRollingLogFile(path string) (*rotatelogs.RotateLogs, error) {
apiLogger, err := rotatelogs.New(
path+".%Y%m%d",
rotatelogs.WithMaxAge(-1),
rotatelogs.WithRotationTime(24*time.Hour),
rotatelogs.WithLinkName(path),
)
if err != nil {
return nil, err
}
return apiLogger, nil
}
- SetRollingLogFile 을 통해 *rotatelogs.RotateLogs 를 반환
- 생성한 구조체를 log.SetOutPut() 인자값으로 전달
Rolling Log File 생성
Logrus CustomFiled 추가
Logrus 에서 기본제공하는 필드 이외의 다른 값을 로깅에 필요할 수 있습니다.
이 때 우리는 *logrus.Entry 를 통해 커스텀한 필드값을 추가할 수 있습니다.
- utils/logger.go
func (l *UploadLogger) MakePrefixData(uploadKey string) *logrus.Entry {
prefix := l.WithFields(logrus.Fields{})
prefix.Data[currentStepField] = 13
prefix.Data[totalStepField] = 13
prefix.Data[uploadKeyField] = uploadKey
return prefix
}
const (
currentStepField = "current_step"
totalStepField = "total_step"
uploadKeyField = "upload_key"
)
- prefix.Data[”FieldName”] 에서 정의되는 FieldName 은 JsonLogging 시 사용되는 필드값임으로, enum 처리하여 사용하였습니다.
Json Field 의 값이 빈 값인 경우 생략처리
Json Logging 은 Logstash 의 로그 수집을 위한 로깅입니다.
이에 필요에 따라서 값이 빈 값인 경우에는 로깅을 하지 말아야 할 경우가 있습니다.
이에 우리는 Logrus Hook 을 사용하여 해당 값이 빈 값인 필드를 생략처리할 수 있습니다.
- utils/logger.go
type hook struct {
}
func (h *hook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *hook) Fire(e *logrus.Entry) error {
for k, v := range e.Data {
if s, ok := v.(string); ok {
if len(s) == 0 {
delete(e.Data, k)
continue
}
}
}
return nil
}
- logrus 에 적용시킬 훅을 생성합니다.
- 먼저 빈 구조체로 hook 을 선언합니다.
- 이후 logrus 에서 추상화된 Levels 와 Fire 을 구체화해줍니다.
- 우리는 Fire 에서 Logrus.Entry 가 들고 있는 FieldData 를 색인하여, String 값이 없을 때에는 슬라이스에서 Delete 처리를 해줍니다.
// LogInitialize Logger Initial
func LogInitialize(fileName, level string) (*UploadLogger, error) {
l := new(UploadLogger)
l.log = logrus.New()
lv := l.getLevel(level)
// Set Logger File Save physical
l.log.SetFormatter(&logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano})
if len(fileName) > 0 {
apiLogger, err := SetRollingLogFile(fileName)
if err != nil {
log.Printf(fileName+" : %s", err.Error())
return nil, err
}
l.log.SetOutput(apiLogger)
} else {
l.log.SetOutput(os.Stdout)
}
l.log.SetLevel(lv)
l.log.Hooks.Add(&hook{})
return l, nil
}
- 선언한 Hook 을 UploadLogger 구조체 생성시에 생성하여 주입시켜줍니다.
Golang 에서 Logrus 를 통한 Json Logging 을 진행해보았습니다.
Reference
'GoLang' 카테고리의 다른 글
Golang 2차원 배열 문제를 통한 array, slice 의 개념정리 (0) | 2023.05.06 |
---|---|
Go, Makefile, Docker로 프로젝트 셋팅 및 빌드 정보 활용하기 (0) | 2022.10.08 |
Golang 과 TDD (0) | 2022.10.02 |
golang과 Docker로 환경변수 제어하기 (0) | 2022.08.23 |
GoLang 의 포인터 (0) | 2022.07.03 |