mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +00:00
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
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
|
|
}
|