Golang 에서 로그 프로그램 만들기

어쩌다가 회사에서 프로젝트로Go 언어를 이용한 서버를 만들게 되었다.
서버도 잘 모르고 Go 언어는 더 몰라서 처음에 많이 어려움을 느꼈다.
많은 샘플 코드를 보고 hello world 도 만들어 보고 하니 조금씩 익숙하게 느껴지기 시작했다.

어느개발자이건 어느 프로그램을 만들건 로그를 남길 일이 많은데
연습삼아 golang 으로 로그프로그램을 만들어 보았다. 
물론 goalng에서는 자체적으로 log 를 지원한다.
하지만 난 파일 출력도 하면서 호출함수 명도 같이 넣어주고 싶어
"log" 를 래핑하여 나만의 로그를 만들게 되었다.


설계(?) 단계에서 내가 필요한 것은 아래와 같았다.
1. 로그 시간
2. 로그 호출 함수
3.멀티 파라미터 로그
4. 파일 출력 (당연하지만)



로그 시간 출력은 golang 의 "log" 를 이용하니 간단하게 해결하였다.

로그 호출 함수는 아래의 함수를 이용하여 얻을 수 있다.
주석에도 써있지만 skip 을 변경하여 자신이 원하는 이름을 가져올 수 있다.
skip변수는 현재 호출스택에서 몇번째 위의 이름을 가져올 지 정하는 변수이다.

func getCallingFunctionName() string {

    fpcs := make([]uintptr, 1)
    // This is the value to skip the number of calling function names.
    // Change this value as you wish.
    skip := 3
    runtime.Callers(skip, fpcs)

    // get the info of the actual function that's in the pointer
    return runtime.FuncForPC(fpcs[0] - 1).Name()
}


멀티 파라미터도 다른 언어와 마찬가지로 golang 에서 간단하고 깔끔하게 지원한다. 
사용법은 변수 ...형식 이다. 아래처럼.
ex:
v ...string

그래서 아래와 같이 하였다.
func Log(v ...string)

파일출력은 두가지 방법이 있는데, 하나는 파일을 열어서 직접 쓰는 방법이 있고, 다른 방법은 log 에 파일 writer 를 설정하여 쓰는 방법이 있다. 나는 두번째 방법을 사용하였는데, 이는 코드 라인수도 줄이고 디버그 창에 동시에 출력하기 위해서다.

func openFile() {

    fpLog, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {

        panic(err)
    }

    isOpen = true
    writer := io.MultiWriter(fpLog, os.Stdout)
    log.SetOutput(writer)
}

위 코드를 보면 log.SetOutput() 이 있는데, 해당 부분이 로그에 파일 writer 를 설정하는 것이다.

이리하여 간단한 로그 프로그램이 완성되었다.
로그 함수를 보면 아래와 같다.

// Log will write log string both on console and in file
func Log(v ...string) {

    if fileName == "" {
        fileName = getNow() + "_log.txt"
    }

    if initialized == false {
        initialized = initialize()
    }

    functionName := getCallingFunctionName()

    logStr := functionName + "() --> " + strings.Join(v[:], ", ")
    log.Println(logStr)
}

파일 이름이 설정되지 않으면 현재 날자 + "_log.txt" 로 저장하게끔 설정하였다.

전체 코드는 아래와 같고, github 에서도 확인할 수 있다.

package logger

import (
    "io"
    "log"
    "os"
    "runtime"
    "strings"
    "sync"
    "time"
)

var fileName string
var mux sync.Mutex
var initialized bool
var printConsole bool
var fpLog *os.File
var err error
var isOpen bool

// SetFileName will set the log file name
// and re initialize the logger
func SetFileName(newFileName string) {

    fileName = newFileName
    initialized = false // in order to stop pollingData()
    initialized = initialize()
}

func getNow() string {

    return time.Now().Local().Format("2006-01-02")
}

// Log will write log string both on console and in file
func Log(v ...string) {

    if fileName == "" {
        fileName = getNow() + "_log.txt"
    }

    if initialized == false {
        initialized = initialize()
    }

    functionName := getCallingFunctionName()

    logStr := functionName + "() --> " + strings.Join(v[:], ", ")
    log.Println(logStr)
}

// Debug will log parameters with Debug string in the beginning
func Debug(v ...string) {

    v = append([]string{"***Debug***"}, v...)
    Log(v...)
}

// Error will log parameters with Debug string in the beginning
func Error(v ...string) {

    v = append([]string{"***Error ***"}, v...)
    Log(v...)
}

func getCallingFunctionName() string {

    fpcs := make([]uintptr, 1)
    // This is the value to skip the number of calling function names.
    // Change this value as you wish.
    skip := 3
    runtime.Callers(skip, fpcs)

    // get the info of the actual function that's in the pointer
    return runtime.FuncForPC(fpcs[0] - 1).Name()
}

func openFile() {

    fpLog, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {

        panic(err)
    }

    isOpen = true
    writer := io.MultiWriter(fpLog, os.Stdout)
    log.SetOutput(writer)
}

func closeFile() {

    if fpLog != nil {

        fpLog.Close()
        isOpen = false
    }
}

func initialize() bool {

    if isOpen == true {
        closeFile()
    }

    openFile()

    return true
}

현재는 Log 함수에 string 형만 받도록 되어있는데, 추후에는 모든 형식이 가능하도록 변경할 예정이다.

댓글