개발자의 오르막

Golang Logrus 로 Json Logging 하기 본문

GoLang

Golang Logrus 로 Json Logging 하기

계단 2022. 11. 3. 22:25

 

https://www.hahwul.com/2022/02/24/logrus-channel-hook/

 

 

이번에 포스팅할 주제는 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

 

logrus package - github.com/sirupsen/logrus - Go Packages

Logrus Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. Logrus is in maintenance-mode. We will not be introducing new features. It's simply too hard to do in a way that won't break many people's pro

pkg.go.dev

 

 

 

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

 

GitHub - lestrrat-go/file-rotatelogs: [ARCHIVED] Port of perl5 File::RotateLogs to Go

[ARCHIVED] Port of perl5 File::RotateLogs to Go. Contribute to lestrrat-go/file-rotatelogs development by creating an account on GitHub.

github.com

 

 

  • 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


 

Comments