Skip to content

Instantly share code, notes, and snippets.

@Jerry0420
Last active May 2, 2022 19:36
Show Gist options
  • Save Jerry0420/3da757e694837ba557f7380d1e373e9a to your computer and use it in GitHub Desktop.
Save Jerry0420/3da757e694837ba557f7380d1e373e9a to your computer and use it in GitHub Desktop.
An logger tool written in Golang.
  • Logger file Auto-Rotation.
  • Thread-Safe
  • No need for any dependency.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"runtime"
"strings"
"sync"
"time"
)
type LoggerTool interface {
parseContextValue(context context.Context) []interface{}
generateLogContent(level string, message []interface{}) []interface{}
INFOf(message ...interface{})
WARNf(message ...interface{})
ERRORf(message ...interface{})
FATALf(message ...interface{})
}
type loggerTool struct {
sync.Mutex
logger *log.Logger
logFile *os.File
contextKeys []string
receiveMessage chan bool
}
func getLogFilePathOfToday(logBaseDir string) string {
// save log file inside "logs" dir.
err := os.MkdirAll(logBaseDir, 0777)
if err != nil {
log.Fatalf("Fail to getLogFilePath :%v", err)
}
return fmt.Sprintf("%s/%s.log", logBaseDir, time.Now().Format("20060102"))
}
func rotateLogFile(lt *loggerTool, logBaseDir string) {
for {
<-lt.receiveMessage
// daily rotate the log file.
today := fmt.Sprint(time.Now().Format("20060102"))
if !strings.Contains(lt.logFile.Name(), today) {
lt.Lock()
logFilePath := getLogFilePathOfToday(logBaseDir)
newlogFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Fail to open log file :%v", err)
}
lt.logger.SetOutput(io.MultiWriter(os.Stdout, newlogFile))
err = lt.logFile.Close()
if err != nil {
log.Fatalf("Fail to close old log file :%v", err)
}
lt.logFile = newlogFile
defer lt.Unlock()
}
}
}
func NewLogger(contextKeys []string, logBaseDir string, disable bool) *loggerTool {
var lt *loggerTool
if disable == true {
lt = &loggerTool{
logger: log.New(os.Stdout, "", 0),
receiveMessage: make(chan bool, 10000),
}
return lt
}
logFilePath := getLogFilePathOfToday(logBaseDir)
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Fail to open log file :%v", err)
}
lt = &loggerTool{
logger: log.New(io.MultiWriter(os.Stdout, logFile), "", log.Ldate|log.Ltime),
logFile: logFile,
contextKeys: contextKeys,
receiveMessage: make(chan bool, 10000),
}
go rotateLogFile(lt, logBaseDir)
return lt
}
func (lt *loggerTool) parseContextValue(context context.Context) []interface{} {
var values []interface{}
for _, key := range lt.contextKeys {
// add pipe symbol for seperation.
if key == "sep" {
values = append(values, "|")
continue
}
if value := context.Value(key); value != nil {
values = append(values, value)
}
}
return values
}
func (lt *loggerTool) generateLogContent(level string, message []interface{}) []interface{} {
var content []interface{}
_, file, line, _ := runtime.Caller(2)
content = append(content, fmt.Sprint(level, file, ":", line, " |"))
if len(message) == 0 {
return content
}
if firstMessage, ok := message[0].(context.Context); ok {
contextValues := lt.parseContextValue(firstMessage)
content = append(content, contextValues...)
if len(message) > 1 {
message = message[1:]
} else {
return content
}
}
content = append(content, "|")
if formatString, ok := message[0].(string); ok && len(message) > 1 {
content = append(content, fmt.Sprintf(formatString, message[1:]...))
} else {
content = append(content, message...)
}
return content
}
func (lt *loggerTool) INFOf(message ...interface{}) {
lt.receiveMessage <- true
content := lt.generateLogContent("[INFO] ", message)
lt.logger.Println(content...)
}
func (lt *loggerTool) WARNf(message ...interface{}) {
lt.receiveMessage <- true
content := lt.generateLogContent("[WARN] ", message)
lt.logger.Println(content...)
}
func (lt *loggerTool) ERRORf(message ...interface{}) {
lt.receiveMessage <- true
content := lt.generateLogContent("[ERROR] ", message)
lt.logger.Println(content...)
}
func (lt *loggerTool) FATALf(message ...interface{}) {
lt.receiveMessage <- true
content := lt.generateLogContent("[FATAL] ", message)
lt.logger.Fatalln(content...)
}
package main
import (
"context"
"fmt"
"log"
"net/http"
"path/filepath"
)
func hello(logger LoggerTool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "requestID", "some-random-req-id")
ctx = context.WithValue(ctx, "code", 200)
ctx = context.WithValue(ctx, "duration", "10ms")
// add whatever you want into the context.
r = r.WithContext(ctx)
// 2021/11/29 20:40:21 [INFO] /path/of/this/file/main.go:22 | 200 | some-random-req-id 10ms | message inside request hello
logger.INFOf(ctx, "message inside request %s", "hello")
// 2021/11/29 20:40:21 [INFO] /path/of/this/file/main.go:22 | | im normal logger message without context.
logger.INFOf("im normal logger message without context.")
fmt.Fprintln(w, "This is hello.")
})
}
func main() {
currentPath, _ := filepath.Abs("./")
// logBaseDir is the dir for all log files.
logBaseDir := filepath.Join(currentPath, "logs")
// "sep" is "|", it's a pipe symbol in a log message.
// The other fields: "code", "requestID" and "duration" are the fields inside context.
logger := NewLogger([]string{"code", "sep", "requestID", "duration"}, logBaseDir, false)
mux := http.NewServeMux()
mux.Handle("/hello", hello(logger))
log.Fatal(http.ListenAndServe("0.0.0.0:8080", mux))
logger.INFOf("server closed.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment