safing-portmaster/base/log/logging.go
2024-11-28 13:27:10 +02:00

251 lines
5 KiB
Go

package log
import (
"fmt"
"log/slog"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tevino/abool"
)
// concept
/*
- Logging function:
- check if file-based levelling enabled
- if yes, check if level is active on this file
- check if level is active
- send data to backend via big buffered channel
- Backend:
- wait until there is time for writing logs
- write logs
- configurable if logged to folder (buffer + rollingFileAppender) and/or console
- console: log everything above INFO to stderr
- Channel overbuffering protection:
- if buffer is full, trigger write
- Anti-Importing-Loop:
- everything imports logging
- logging is configured by main module and is supplied access to configuration and taskmanager
*/
// Severity describes a log level.
type Severity uint32
func (s Severity) toSLogLevel() slog.Level {
// Convert to slog level.
switch s {
case TraceLevel:
return slog.LevelDebug
case DebugLevel:
return slog.LevelDebug
case InfoLevel:
return slog.LevelInfo
case WarningLevel:
return slog.LevelWarn
case ErrorLevel:
return slog.LevelError
case CriticalLevel:
return slog.LevelError
}
// Failed to convert, return default log level
return slog.LevelWarn
}
// Message describes a log level message and is implemented
// by logLine.
type Message interface {
Text() string
Severity() Severity
Time() time.Time
File() string
LineNumber() int
}
type logLine struct {
msg string
tracer *ContextTracer
level Severity
timestamp time.Time
file string
line int
}
func (ll *logLine) Text() string {
return ll.msg
}
func (ll *logLine) Severity() Severity {
return ll.level
}
func (ll *logLine) Time() time.Time {
return ll.timestamp
}
func (ll *logLine) File() string {
return ll.file
}
func (ll *logLine) LineNumber() int {
return ll.line
}
func (ll *logLine) Equal(ol *logLine) bool {
switch {
case ll.msg != ol.msg:
return false
case ll.tracer != nil || ol.tracer != nil:
return false
case ll.file != ol.file:
return false
case ll.line != ol.line:
return false
case ll.level != ol.level:
return false
}
return true
}
// Log Levels.
const (
TraceLevel Severity = 1
DebugLevel Severity = 2
InfoLevel Severity = 3
WarningLevel Severity = 4
ErrorLevel Severity = 5
CriticalLevel Severity = 6
)
var (
logBuffer chan *logLine
forceEmptyingOfBuffer = make(chan struct{})
logLevelInt = uint32(InfoLevel)
logLevel = &logLevelInt
logsWaiting = make(chan struct{}, 1)
logsWaitingFlag = abool.NewBool(false)
shutdownFlag = abool.NewBool(false)
shutdownSignal = make(chan struct{})
shutdownWaitGroup sync.WaitGroup
initializing = abool.NewBool(false)
started = abool.NewBool(false)
startedSignal = make(chan struct{})
)
// GetLogLevel returns the current log level.
func GetLogLevel() Severity {
return Severity(atomic.LoadUint32(logLevel))
}
// SetLogLevel sets a new log level. Only effective after Start().
func SetLogLevel(level Severity) {
atomic.StoreUint32(logLevel, uint32(level))
// Setup slog here for the transition period.
setupSLog(level)
}
// Name returns the name of the log level.
func (s Severity) Name() string {
switch s {
case TraceLevel:
return "trace"
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarningLevel:
return "warning"
case ErrorLevel:
return "error"
case CriticalLevel:
return "critical"
default:
return "none"
}
}
// ParseLevel returns the level severity of a log level name.
func ParseLevel(level string) Severity {
switch strings.ToLower(level) {
case "trace":
return 1
case "debug":
return 2
case "info":
return 3
case "warning":
return 4
case "error":
return 5
case "critical":
return 6
}
return 0
}
// Start starts the logging system. Must be called in order to see logs.
func Start(level string, logToStdout bool, logDir string) error {
if !initializing.SetToIf(false, true) {
return nil
}
// Parse log level argument.
initialLogLevel := InfoLevel
if level != "" {
initialLogLevel = ParseLevel(level)
if initialLogLevel == 0 {
fmt.Fprintf(os.Stderr, "log warning: invalid log level %q, falling back to level info\n", level)
initialLogLevel = InfoLevel
}
}
// Setup writer.
if logToStdout {
GlobalWriter = NewStdoutWriter()
} else {
// Create file log writer.
var err error
GlobalWriter, err = NewFileWriter(logDir)
if err != nil {
return fmt.Errorf("failed to initialize log file: %w", err)
}
}
// Init logging systems.
SetLogLevel(initialLogLevel)
logBuffer = make(chan *logLine, 1024)
if !schedulingEnabled {
close(writeTrigger)
}
startWriter()
started.Set()
close(startedSignal)
// Delete all logs older than one month.
if !logToStdout {
err := CleanOldLogs(logDir, 30*24*time.Hour)
if err != nil {
Errorf("log: failed to clean old log files: %s", err)
}
}
return nil
}
// Shutdown writes remaining log lines and then stops the log system.
func Shutdown() {
if shutdownFlag.SetToIf(false, true) {
close(shutdownSignal)
}
shutdownWaitGroup.Wait()
GlobalWriter.Close()
}