Correct template to encapsulate log setting in golang

When trying to move the log setup code into a separate function, I ran into the inability to hide the target file object from the function main

. The following simplified INCORRECT example attempts to set up logging to both Stderr and a file with a single function call:

package main

import (
    "io"
    "log"
    "os"
)

func SetupLogging() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    defer logFile.Close()

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
}

func main() {
    SetupLogging()
    log.Println("Test message")
}

      

Obviously this doesn't work because it defer

closes the log file at the end of the function SetupLogging

.

The working example below adds additional code and IMHO loses some clarity if repeated in a larger application as a template:

package main

import (
    "io"
    "log"
    "os"
)

func SetupLogging() *os.File {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    return logFile
}

func main() {
    logf := SetupLogging()
    defer logf.Close()

    log.Println("Test message")
}

      

Is there another way to completely encapsulate the open file control into a function, but still nicely free the handle?

+3


source to share


4 answers


I have now successfully used this approach for about one year on several projects. The idea is to return a function from a setup call. This resulting function contains destruction logic. Here's an example:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func LogSetupAndDestruct() func() {
    logFile, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
    if err != nil {
        log.Panicln(err)
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))

    return func() {
        e := logFile.Close()
        if e != nil {
            fmt.Fprintf(os.Stderr, "Problem closing the log file: %s\n", e)
        }
    }
}

func main() {
    defer LogSetupAndDestruct()()

    log.Println("Test message")
}

      



A closure around the cleanup logic is used, deferred.

A slightly more thoughtful public example of this approach is in the Viper code: here is the return from a test initializer , and here it is used to encapsulate logic and cleanup objects

+5


source


The correct way to do this is to pass the main element to SetupLogging

:

func SetupLogging(lf *os.File) {
    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    log.Println("Started")
}

func main() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    defer logFile.Close()
    SetupLogging(logFile)
    log.Println("Test message")
}

      



Another option is to use runtime.SetFinalizer

, but it is not always guaranteed to run before the main outputs.

func SetupLogging() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    runtime.SetFinalizer(logFile, func(h *os.File) {
        h.Close()
    })

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
}

      

+1


source


You can do it using channels, here is my approach

type InfoLog struct {
    InfoChan chan string
    CloseChan chan struct{} //empty signal
    log *log.Logger
    file *os.File
}

func NewInfoLog(file *os.File) *InfoLog {
    return &InfoLog{
        InfoChan: make(chan string),
        CloseChan: make(chan struct{}),
        log: log.New(file, "TAG", log.Ldate|log.Ltime),
        file: file,
    }
}

func (i *InfoLog) listen() {
    for {
        select {
        case infoMsg := <-i.InfoChan:
            i.log.Println(infoMsg)
        case <-i.CloseChan:
            i.file.Close()
            close(i.InfoChan)
        }
    }
}

      

then in the main

func main() {
    infoLog := NewInfoLog(ANY_OPEN_FILE_HERE)
    go infoLog.listen()
    infoLog.InfoChan <- "msg"
    infoLog.InfoChan <- "msg"
    infoLog.InfoChan <- "msg"
    infoLog.CloseChan <- struct{}{}
    // exits normaly
}

      

you can see the asynchronous logging system I made for a complete example: https://github.com/sescobb27/ciudad-gourmet/blob/master/services/log_service.go

+1


source


in the case where multiple "break" processes are required, a great solution is to use the google context package ( https://blog.golang.org/context ). The advantage is that you can undo all the procedures currently in progress with a single context. smth like this:

package main

import (
    "fmt"
    "io"
    "log"
    "os"

    "golang.org/x/net/context"
)

func LogSetup(ctx context.Context) error {
    logFile, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
    if err != nil {
        return err
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    // here we could f.ex. execute:
    // sendLogOutputToExternalService(ctx)
    // and it could have it own teardown procedure
    // which would be called on main context expiration
    go func() {
       for _ = range ctx.Done() {
         err := logFile.Close()
         if err = nil {
             fmt.Fprintf(os.Stderr, "Problem closing the log file: %s\n", e)
        }
    }()
    return nil
}


func main() {
    var stopAll func()
    mainContext, stopAll = context.WithCancel(context.Background())
    defer stopAll()

    err := LogSetup(mainContext)
    if err!=nil {
        log.Fatal("error while initializing logging") 
    }

    log.Println("Test message")
}

      

0


source







All Articles