mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +00:00
When the daemon is set to debug/trace, the GUI now automatically writes a rotated gui-client.log in the user's config dir and the daemon's debug bundle collects it. The UI learns the level both at startup (daemon already in debug) and live, by piggybacking the existing SubscribeEvents stream: the daemon publishes a marked log-level-changed SystemEvent (and a per-subscription snapshot), which DaemonFeed routes to guilog.DebugLog instead of an OS toast. The UI registers its log path via a new RegisterUILog RPC so the root daemon, which can't resolve the user's config dir, knows where to find the file. Manual --log-file (any value) disables the daemon-driven file logging. Fix: client/ui SetLogLevel looked up proto.LogLevel_value with the lowercase logrus name, which never matched the uppercase enum keys and silently fell back to INFO — so trace/debug requests from the bundle flow had no effect.
197 lines
4.9 KiB
Go
197 lines
4.9 KiB
Go
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
|
|
"github.com/DeRuina/timberjack"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/grpclog"
|
|
|
|
"github.com/netbirdio/netbird/formatter"
|
|
)
|
|
|
|
const defaultLogSize = 15
|
|
|
|
const (
|
|
LogConsole = "console"
|
|
LogSyslog = "syslog"
|
|
)
|
|
|
|
var (
|
|
SpecialLogs = []string{
|
|
LogSyslog,
|
|
LogConsole,
|
|
}
|
|
)
|
|
|
|
// InitLog parses and sets log-level input
|
|
func InitLog(logLevel string, logs ...string) error {
|
|
return InitLogger(log.StandardLogger(), logLevel, logs...)
|
|
}
|
|
|
|
// InitLogger parses and sets log-level input for a logrus logger
|
|
func InitLogger(logger *log.Logger, logLevel string, logs ...string) error {
|
|
level, err := log.ParseLevel(logLevel)
|
|
if err != nil {
|
|
return fmt.Errorf("failed parsing log-level %s: %w", logLevel, err)
|
|
}
|
|
|
|
logFmt, err := buildWriters(logger, logs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch logFmt {
|
|
case "json":
|
|
formatter.SetJSONFormatter(logger)
|
|
case "syslog":
|
|
formatter.SetSyslogFormatter(logger)
|
|
default:
|
|
formatter.SetTextFormatter(logger)
|
|
}
|
|
logger.SetLevel(level)
|
|
|
|
setGRPCLibLogger(logger)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetLogOutputs re-points an already-initialized logger to the given targets
|
|
// (console/syslog/file), with the same target semantics as InitLogger, but
|
|
// without re-parsing the level or resetting the formatter. The desktop GUI uses
|
|
// it to attach the rotated gui-client.log alongside the console when the daemon
|
|
// enters debug, and drop back to console-only when it leaves.
|
|
func SetLogOutputs(logger *log.Logger, logs ...string) error {
|
|
if _, err := buildWriters(logger, logs...); err != nil {
|
|
return err
|
|
}
|
|
setGRPCLibLogger(logger)
|
|
return nil
|
|
}
|
|
|
|
// buildWriters resolves the given log targets to writers and points the logger
|
|
// at them (single writer or MultiWriter). It returns the log format implied by
|
|
// the targets (syslog forces "syslog"; otherwise the NB_LOG_FORMAT env value).
|
|
// Shared by InitLogger and SetLogOutputs.
|
|
func buildWriters(logger *log.Logger, logs ...string) (string, error) {
|
|
var writers []io.Writer
|
|
logFmt := os.Getenv("NB_LOG_FORMAT")
|
|
|
|
seen := make(map[string]bool, len(logs))
|
|
for _, logPath := range logs {
|
|
if seen[logPath] {
|
|
continue
|
|
}
|
|
seen[logPath] = true
|
|
|
|
switch logPath {
|
|
case LogSyslog:
|
|
AddSyslogHookToLogger(logger)
|
|
logFmt = "syslog"
|
|
case LogConsole:
|
|
writers = append(writers, os.Stderr)
|
|
case "":
|
|
logger.Warnf("empty log path received: %#v", logPath)
|
|
default:
|
|
writer, err := setupLogFile(logPath, isRotationDisabled(logger))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed setting up log file: %s, %w", logPath, err)
|
|
}
|
|
writers = append(writers, writer)
|
|
}
|
|
}
|
|
|
|
if len(writers) > 1 {
|
|
logger.SetOutput(io.MultiWriter(writers...))
|
|
} else if len(writers) == 1 {
|
|
logger.SetOutput(writers[0])
|
|
}
|
|
|
|
return logFmt, nil
|
|
}
|
|
|
|
// FindFirstLogPath returns the first logs entry that could be a log path, that is neither empty, nor a special value
|
|
func FindFirstLogPath(logs []string) string {
|
|
for _, logFile := range logs {
|
|
if logFile != "" && !slices.Contains(SpecialLogs, logFile) {
|
|
return logFile
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isRotationDisabled(logger *log.Logger) bool {
|
|
v, _ := os.LookupEnv("NB_LOG_DISABLE_ROTATION")
|
|
disabled, _ := strconv.ParseBool(v)
|
|
if disabled {
|
|
logger.Warnf("log rotation is disabled by env flag")
|
|
return true
|
|
}
|
|
conflict, configPath := FindFirstLogrotateConflict()
|
|
if conflict {
|
|
logger.Warnf("log rotation conflict detected in: %#v, rotation is disabled", configPath)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func setupLogFile(logPath string, disableRotation bool) (io.Writer, error) {
|
|
if disableRotation {
|
|
file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return file, nil
|
|
}
|
|
return newRotatedOutput(logPath), nil
|
|
}
|
|
|
|
func newRotatedOutput(logPath string) io.Writer {
|
|
maxLogSize := getLogMaxSize()
|
|
timberjackLogger := &timberjack.Logger{
|
|
// Log file absolute path, os agnostic
|
|
Filename: filepath.ToSlash(logPath),
|
|
MaxSize: maxLogSize, // MB
|
|
MaxBackups: 10,
|
|
MaxAge: 30, // days
|
|
Compression: "gzip",
|
|
}
|
|
return timberjackLogger
|
|
}
|
|
|
|
func setGRPCLibLogger(logger *log.Logger) {
|
|
logOut := logger.Writer()
|
|
if os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL") != "info" {
|
|
grpclog.SetLoggerV2(grpclog.NewLoggerV2(io.Discard, logOut, logOut))
|
|
return
|
|
}
|
|
|
|
var v int
|
|
vLevel := os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL")
|
|
if vl, err := strconv.Atoi(vLevel); err == nil {
|
|
v = vl
|
|
}
|
|
|
|
grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(logOut, logOut, logOut, v))
|
|
}
|
|
|
|
func getLogMaxSize() int {
|
|
if sizeVar, ok := os.LookupEnv("NB_LOG_MAX_SIZE_MB"); ok {
|
|
size, err := strconv.ParseInt(sizeVar, 10, 64)
|
|
if err != nil {
|
|
log.Errorf("failed parsing log-size %s: %s. Should be just an integer", sizeVar, err)
|
|
return defaultLogSize
|
|
}
|
|
|
|
log.Infof("Setting log file max size to %d MB", size)
|
|
|
|
return int(size)
|
|
}
|
|
return defaultLogSize
|
|
}
|