pages-server/server/log/log.go
Gusted 1e183d7249
Improve logging
- Actually log useful information at their respective log level.
- Add logs in hot-paths to be able to deep-dive and debug specific
requests (see server/handler.go)
- Remove zerologger and instead use custom logger that doesn't log
JSON (directly inspired by https://codeberg.org/Codeberg/moderation/pulls/7).
- Add more information to existing fields(e.g. the host that the user is
visiting, this was noted by @fnetX).
2022-07-24 06:22:52 +02:00

194 lines
4.8 KiB
Go

package log
import (
"errors"
"fmt"
"io"
stdLog "log"
"os"
"runtime"
"strings"
"time"
)
var (
// logger is the variable to which the code can write its output.
logger *stdLog.Logger
// The current absolute path to backend/ is stored in filePrefix; this is
// removed from the filename because the logging doesn't require any of the other junk.
filePrefix string
// logLevel is the variable that decides at which log level the logger will output
// to the logger.
logLevel LogLevel
)
type LogLevel uint8
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
func init() {
// Make sure that logger is always initalized, this is to still be able to use the logger
// even when logger isn't yet initilazed with the correct values.
initializeLogger(os.Stdout)
// A quick and dirty way to get the backend/ absolute folder
_, filename, _, _ := runtime.Caller(0)
filePrefix = strings.TrimSuffix(filename, "server/log/log.go")
if filePrefix == filename {
// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
panic("unable to detect correct package prefix, please update file: " + filename)
}
}
// initializeLogger takes a input of multiple writers, which to all writers will be logged to.
func initializeLogger(w ...io.Writer) {
logger = stdLog.New(io.MultiWriter(w...), "", 0)
}
func stringToLogLevel(input string) (LogLevel, error) {
switch strings.ToLower(input) {
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn":
return WarnLevel, nil
case "info":
return InfoLevel, nil
}
return FatalLevel, errors.New("invalid log level value")
}
// Config takes the ini config and correctly adjust the logger variable.
func Config(outputs []string, outputFile, level string) error {
newLogLevel, err := stringToLogLevel(level)
if err != nil {
return err
}
logLevel = newLogLevel
isFileLog := false
isConsoleLog := false
for _, output := range outputs {
switch output {
case "file":
isFileLog = true
case "console":
isConsoleLog = true
}
}
writers := []io.Writer{}
if isConsoleLog {
writers = append(writers, os.Stdout)
}
if isFileLog {
f, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return fmt.Errorf("cannot open log output's file %q: %v", outputFile, err)
}
writers = append(writers, f)
}
initializeLogger(writers...)
return nil
}
// the Skipper struct can be passed as firt argument to adjust the skip offset.
type Skipper struct {
// SkipOffset will be added on top of the existing "2" skips.
SkipOffset int
}
func log(msg, prefix string, print bool, args ...interface{}) string {
caller := "?()"
skipOffset := 2
// Check if the first argument is Skipper and add the skipoffset accordingly.
if len(args) > 0 {
if skip, ok := args[0].(Skipper); ok {
skipOffset += skip.SkipOffset
args = args[1:]
}
}
pc, filename, line, ok := runtime.Caller(skipOffset)
if ok {
// Get caller function name.
fn := runtime.FuncForPC(pc)
if fn != nil {
caller = fn.Name() + "()"
// Remove prefix of binary's name.
lastIndex := strings.LastIndexByte(caller, '.')
if lastIndex > 0 && len(caller) > lastIndex+1 {
caller = caller[lastIndex+1:]
}
}
}
filename = strings.TrimPrefix(filename, filePrefix)
// Don't output long file names.
if len(filename) > 40 {
filename = "..." + filename[len(filename)-40:]
}
now := time.Now()
year, month, day := now.Date()
hour, min, sec := now.Clock()
// Output message:
// DATE TIME FILENAME:LINE:CALLER PREFIX: MSG
prefixedMessage := fmt.Sprintf("%d/%02d/%02d %02d:%02d:%02d %s:%d:%s %s: %s", year, month, day, hour, min, sec, filename, line, caller, prefix, msg)
// Only print the message if it has been requested.
if print {
if len(args) > 0 {
logger.Printf(prefixedMessage+"\n", args...)
} else {
logger.Println(prefixedMessage)
}
return ""
} else {
return fmt.Sprintf(prefixedMessage, args...)
}
}
// Debug logs a message with the DEBUG prefix.
func Debug(msg string, args ...interface{}) {
if logLevel > DebugLevel {
return
}
log(msg, "DEBUG", true, args...)
}
// Info logs a message with the INFO prefix.
func Info(msg string, args ...interface{}) {
if logLevel > InfoLevel {
return
}
log(msg, "INFO", true, args...)
}
// Warn logs a message with the WARN prefix.
func Warn(msg string, args ...interface{}) {
if logLevel > WarnLevel {
return
}
log(msg, "WARN", true, args...)
}
// Error logs a message with the ERROR prefix.
func Error(msg string, args ...interface{}) {
if logLevel > ErrorLevel {
return
}
log(msg, "ERROR", true, args...)
}
// Fatal logs a message with the FATAL prefix and then exit the program.
func Fatal(msg string, args ...interface{}) {
log(msg, "FATAL", true, args...)
os.Exit(1)
}