개요
Go(Golang)으로 Asynchronous Non-Blocking Logger 만들기: Asynchronous Non-Blocking Logging in Golang
먼저 들어가기 전에 non-block 이나 golang 기본 적인 로깅에 대해서 모르신다면 이 전 포스팅을 확인해주세요. Go(Golang)로 로거 만들기: Logging in Golang 개요 프로그램 개발하면서 로그의 중요성을 말
banaconda.tistory.com
오늘은 이전 포스팅에 이어서 구현 부분에 대해서 알아보자. 몇 가지 함수들은 go 기본 패키지중 log 패키지에서 가져오고 그 외의 기능은 모두 구현했다. 완성된 로거 패키지는 github 에서 소스를 보거나 아래 명령으로 받아서 사용할 수 있다.
$ go get github.com/banaconda/nb-logger@v1.0.0
구현
Logger 구조체
먼저 Logger Client 가 될 BasicLogger 구조체를 살펴 보자.
- level: logging 할 수 있는 level 을 지정한다. default 는 Info 이고, 지정된 level 보다 낮은 log 들은 찍지 않고 무시한다.
- flags: 날짜, 시간, 파일, line 등 logging 시 표시할 내용에 대한 포맷이나. 표준 출력을 할지 or 블록킹 모드로 돌지 등을 지정할 수 있다.
- writer: logging 시 파일 쓰기, 표준 출력하기에 필요한 인터페이스.
- ch: logging client 와 server 간의 주고 받은 버퍼.
- lock: 여러 프로세스가 하나의 logger 를 공유할 때, race condtion 이 발생하지 않도록 사용하는 mutex lock.
- wg: WaitGroup 을 사용해서 logger 종료 시 server go routine 이 모든 쓰기 작업을 끝내고 종료 될 수 있도록 함.
type BasicLogger struct {
level int
flags int
writer io.Writer
ch chan logMessage
lock sync.Mutex
wg sync.WaitGroup
}
생성자
생성자는 file 을 생성하고, 생성된면 아까 봤던 BasicLogger 를 초기화해서 리턴한다. 생성자에서 두 가지 flags(Lstdout, Lblocking)에 대해서 처리 해주는데 다음과 같다.
- Lstdout: 기본적으로 writer 는 MultiWriter 로 구성되고, 해당 flag 가 켜지면 writer 에 os.Stdout writer 를 추가해서 로그 입력 시 표준 출력도 같이 되게 한다.
- Lbloking: 기본적으로 WaitGroup 을 하나 추가하고 logger.server() method 를 go routine 으로 실핼하는데, 해당 flag 가 켜지면 전자의 작업을 하지 않고, logging 시 logger 를 가지고 있는 메인 프로세스에서 직접 쓴다.
func NewLogger(path string, level int, bufferSize int, flags int) (Logger, error) {
logFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, errors.New("file creation fail")
}
var writer io.Writer
if flags&Lstdout != 0 {
writer = io.MultiWriter(logFile, os.Stdout)
} else {
writer = io.MultiWriter(logFile)
}
logger := &BasicLogger{
level: level,
flags: flags,
writer: writer,
ch: make(chan logMessage, bufferSize),
lock: sync.Mutex{},
wg: sync.WaitGroup{},
}
if logger.flags&Lblocking == 0 {
logger.wg.Add(1)
go logger.server()
}
return logger, nil
}
Logging Server
위 섹션에서 go routine 으로 수행했던 logger.server() 의 구현부이다. server 가 종료될 때는 WaitGroup 에 Done Signal을 추게 되고, logger.ch 로 부터 logMessage 를 받는데 cmd 가 exit 이 아닐 때까지 동작한다. (지금 구현체는 write 에 대해서만 동작하고 이외는 다 종료되게 되어 있음)
Logging Server 의 역할을 간단하다. logMessage 의 buf 내용을 ch에서 읽어와서 loggger.writer 에 쓰는 게 끝이다.
const (
write = iota
exit
)
type logMessage struct {
cmd int
buf []byte
}
func (logger *BasicLogger) server() {
defer logger.wg.Done()
for {
if message, err := <-logger.ch; err && message.cmd == write {
logger.writer.Write(message.buf)
} else {
break
}
}
}
Logging
logging 하는 부분은 go 의 기본 패키지인 log 에서 많이 참고했다. 또한. iota 와 formatHeader 는 log 기본 패키지와 거의 같다.
이곳에서는 logger 의 level 보다 낮은 level 의 로깅은 무시하고 포맷 스트링, 가변 인자, 플래그 등을 참고해서 실제 로그 스트링을 만든다. 그리고 Lblocking flag 에 따라서 block 일 때는 직접 파일에 로그 스트링을 쓰고, 아니면 ch 로 보내서 logging server 가 쓸 수 있게 한다.
func (logger *BasicLogger) logging(level int, format string, v ...any) {
if logger.level < level {
return
}
now := time.Now()
logger.lock.Lock()
defer logger.lock.Unlock()
var file string
var line int
if logger.flags&(Lshortfile|Llongfile) != 0 {
logger.lock.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(2)
if !ok {
file = "unknown"
line = 0
}
logger.lock.Lock()
}
s := fmt.Sprintf(format, v...)
var buf []byte
logger.formatHeader(&buf, level, now, file, line)
buf = append(buf, s...)
if len(s) == 0 || s[len(s)-1] != '\n' {
buf = append(buf, '\n')
}
if logger.flags&Lblocking == 0 {
message := logMessage{
cmd: write,
buf: buf,
}
logger.ch <- message
} else {
logger.writer.Write(buf)
}
}
Logger Interface 구현
이전 포스팅에서 봤던 Logger interface 의 구현체이다.
type Logger interface {
Trace(format string, v ...any)
Debug(format string, v ...any)
Info(format string, v ...any)
Warn(format string, v ...any)
Error(format string, v ...any)
SetLogLevel(level int)
GetLogLevel() int
Close()
}
Trace, Debug, Info, Warn, Error 구현체는 logging method 의 wrapper 형태이고, Set/GetLogLevel 은 동적으로 로그 레벨을 조정하기 위한 method 이다.
종료 시 Close 를 호출하면 logging server 가 Done signal을 보낼 때 까지 대기하다가 종료하게 된다.
func (logger *BasicLogger) Trace(format string, v ...any) {
logger.logging(Trace, format, v...)
}
func (logger *BasicLogger) Debug(format string, v ...any) {
logger.logging(Debug, format, v...)
}
func (logger *BasicLogger) Info(format string, v ...any) {
logger.logging(Info, format, v...)
}
func (logger *BasicLogger) Warn(format string, v ...any) {
logger.logging(Warn, format, v...)
}
func (logger *BasicLogger) Error(format string, v ...any) {
logger.logging(Error, format, v...)
}
func (logger *BasicLogger) SetLogLevel(level int) {
logger.level = level
}
func (logger *BasicLogger) GetLogLevel() int {
return logger.level
}
func (logger *BasicLogger) Close() {
logger.ch <- logMessage{cmd: exit}
logger.wg.Wait()
}
정리
이 번 포스팅은 실제 Logger 구현체에 대해서 알아봤다. 다음 포스팅에서는 테스트 코드를 작성하면서 sync logger 와 async logger 의 성능 차이를 보자.
'Programming > Golang' 카테고리의 다른 글
| Go(Golang)으로 Asynchronous Non-Blocking Logger 만들기 3편 (0) | 2022.07.11 |
|---|---|
| Go(Golang)으로 Asynchronous Non-Blocking Logger 만들기 1편 (0) | 2022.07.07 |
| Go(Golang)로 로거 만들기: Logging in Golang (0) | 2022.07.04 |
| 견고한 Go 모듈 만들기 (0) | 2022.07.03 |