본문 바로가기

Programming/Golang

Go(Golang)으로 Asynchronous Non-Blocking Logger 만들기 2편

개요

 

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 의 성능 차이를 보자.