Files
vikunja/pkg/log/logging.go
kolaente 0e05d1cc9d fix(log): write each log category to its own file (#2206)
Previously, `makeLogHandler()` hardcoded "standard" as the logfile name
passed to `getLogWriter()`, causing all log categories (`database`,
`http`, `events`, `mail`) to write to `standard.log` instead of their
own files.

Add a logfile parameter to `makeLogHandler()` so each caller specifies
its category name, producing `database.log`, `http.log`, `echo.log`,
`events.log`, and `mail.log` as expected.

Fixes https://github.com/go-vikunja/vikunja/issues/2177
2026-02-08 15:22:58 +00:00

204 lines
5.9 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package log
import (
"fmt"
"io"
"log/slog"
"os"
"strings"
)
// logInstance is the instance of the logger which is used under the hood to log
var logInstance *slog.Logger
// logpath is the path in which log files will be written.
// This value is a mere fallback for other modules that could but shouldn't be used before calling ConfigureLogger
var logPath = "."
// InitLogger initializes the global log handler
func InitLogger() {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
logInstance = slog.New(handler)
}
func makeLogHandler(enabled bool, output string, logfile string, level string, format string) slog.Handler {
var slogLevel slog.Level
switch strings.ToUpper(level) {
case "CRITICAL", "ERROR":
slogLevel = slog.LevelError
case "WARNING":
slogLevel = slog.LevelWarn
case "NOTICE", "INFO":
slogLevel = slog.LevelInfo
case "DEBUG":
slogLevel = slog.LevelDebug
default:
slogLevel = slog.LevelInfo
}
format = strings.ToLower(format)
if format == "" {
format = "text"
}
if format != "text" && format != "structured" {
Fatalf("invalid log format %s", format)
}
writer := io.Discard
if enabled && output != "off" {
writer = getLogWriter(output, logfile)
}
return createHandler(writer, slogLevel, format)
}
// createHandler creates a consistent slog handler for all loggers
func createHandler(writer io.Writer, level slog.Level, format string) slog.Handler {
handlerOpts := &slog.HandlerOptions{
Level: level,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
// Remove message attribute when empty
if a.Key == slog.MessageKey && a.Value.String() == "" {
return slog.Attr{}
}
return a
},
}
if strings.ToLower(format) == "structured" {
return slog.NewJSONHandler(writer, handlerOpts)
}
return slog.NewTextHandler(writer, handlerOpts)
}
// NewHTTPLogger creates and initializes a new HTTP logger
func NewHTTPLogger(enabled bool, output string, format string) *slog.Logger {
handler := makeLogHandler(enabled, output, "http", "DEBUG", format)
return slog.New(handler).With("component", "http")
}
// ConfigureStandardLogger configures the global log handler
func ConfigureStandardLogger(enabled bool, output string, path string, level string, format string) {
logPath = path
handler := makeLogHandler(enabled, output, "standard", level, format)
logInstance = slog.New(handler)
}
// wrapLogger is used for libraries requiring a Debugf method.
type wrapLogger struct{}
func (wrapLogger) Debugf(format string, args ...interface{}) {
logInstance.Debug(fmt.Sprintf(format, args...))
}
// GetLogWriter returns the writer to where the normal log goes, depending on the config
func getLogWriter(logfmt string, logfile string) (writer io.Writer) {
writer = os.Stdout // Set the default case to prevent nil pointer panics
switch logfmt {
case "file":
if err := os.MkdirAll(logPath, 0744); err != nil {
Fatalf("Could not create log path: %s", err.Error())
}
fullLogFilePath := logPath + "/" + logfile + ".log"
f, err := os.OpenFile(fullLogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
Fatalf("Could not create logfile %s: %s", fullLogFilePath, err.Error())
}
writer = f
case "stderr":
writer = os.Stderr
case "stdout":
default:
writer = os.Stdout
}
return
}
// GetLogger returns the logging instance. DO NOT USE THIS TO LOG STUFF.
// GetLogger returns a logger which can be used by external libraries expecting a Debugf method.
// It only implements Debugf and forwards to the global logger.
func GetLogger() interface{ Debugf(string, ...interface{}) } {
return wrapLogger{}
}
// The following functions are to be used as an "eye-candy", so one can just write log.Error() instead of log.Log.Error()
// Debug is for debug messages
func Debug(args ...interface{}) {
logInstance.Debug(fmt.Sprint(args...))
}
// Debugf is for debug messages
func Debugf(format string, args ...interface{}) {
logInstance.Debug(fmt.Sprintf(format, args...))
}
// Info is for info messages
func Info(args ...interface{}) {
logInstance.Info(fmt.Sprint(args...))
}
// Infof is for info messages
func Infof(format string, args ...interface{}) {
logInstance.Info(fmt.Sprintf(format, args...))
}
// Error is for error messages
func Error(args ...interface{}) {
logInstance.Error(fmt.Sprint(args...))
}
// Errorf is for error messages
func Errorf(format string, args ...interface{}) {
logInstance.Error(fmt.Sprintf(format, args...))
}
// Warning is for warning messages
func Warning(args ...interface{}) {
logInstance.Warn(fmt.Sprint(args...))
}
// Warningf is for warning messages
func Warningf(format string, args ...interface{}) {
logInstance.Warn(fmt.Sprintf(format, args...))
}
// Critical is for critical messages
func Critical(args ...interface{}) {
logInstance.Error(fmt.Sprint(args...))
}
// Criticalf is for critical messages
func Criticalf(format string, args ...interface{}) {
logInstance.Error(fmt.Sprintf(format, args...))
}
// Fatal is for fatal messages
func Fatal(args ...interface{}) {
logInstance.Error(fmt.Sprint(args...))
os.Exit(1)
}
// Fatalf is for fatal messages
func Fatalf(format string, args ...interface{}) {
logInstance.Error(fmt.Sprintf(format, args...))
os.Exit(1)
}