diff --git a/.dockerignore-client b/.dockerignore-client new file mode 100644 index 000000000..a93ef97c0 --- /dev/null +++ b/.dockerignore-client @@ -0,0 +1,3 @@ +* +!client/netbird-entrypoint.sh +!netbird diff --git a/.gitignore b/.gitignore index abb728b19..e6c0c0aca 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ infrastructure_files/setup-*.env .vscode .DS_Store vendor/ +/netbird diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ca5eafa62..d4a97b447 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -155,13 +155,15 @@ dockers: goarch: amd64 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}" - "--label=maintainer=dev@netbird.io" - image_templates: - netbirdio/netbird:{{ .Version }}-arm64v8 @@ -171,6 +173,8 @@ dockers: goarch: arm64 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -188,6 +192,8 @@ dockers: goarm: 6 use: buildx dockerfile: client/Dockerfile + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm" - "--label=org.opencontainers.image.created={{.Date}}" @@ -205,6 +211,8 @@ dockers: goarch: amd64 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -221,6 +229,8 @@ dockers: goarch: arm64 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.created={{.Date}}" @@ -238,6 +248,8 @@ dockers: goarm: 6 use: buildx dockerfile: client/Dockerfile-rootless + extra_files: + - client/netbird-entrypoint.sh build_flag_templates: - "--platform=linux/arm" - "--label=org.opencontainers.image.created={{.Date}}" diff --git a/client/Dockerfile b/client/Dockerfile index 5f1f70040..e19a09909 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,9 +1,27 @@ -FROM alpine:3.21.3 +# build & run locally with: +# cd "$(git rev-parse --show-toplevel)" +# CGO_ENABLED=0 go build -o netbird ./client +# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client . +# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest + +FROM alpine:3.22.0 # iproute2: busybox doesn't display ip rules properly -RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables +RUN apk add --no-cache \ + bash \ + ca-certificates \ + ip6tables \ + iproute2 \ + iptables + +ENV \ + NETBIRD_BIN="/usr/local/bin/netbird" \ + NB_LOG_FILE="console,/var/log/netbird/client.log" \ + NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \ + NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \ + NB_ENTRYPOINT_LOGIN_TIMEOUT="1" + +ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ] ARG NETBIRD_BINARY=netbird -COPY ${NETBIRD_BINARY} /usr/local/bin/netbird - -ENV NB_FOREGROUND_MODE=true -ENTRYPOINT [ "/usr/local/bin/netbird","up"] +COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh +COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 5055cb20d..5fa8de0a5 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -1,18 +1,33 @@ -FROM alpine:3.21.0 +# build & run locally with: +# cd "$(git rev-parse --show-toplevel)" +# CGO_ENABLED=0 go build -o netbird ./client +# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client . +# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest -ARG NETBIRD_BINARY=netbird -COPY ${NETBIRD_BINARY} /usr/local/bin/netbird +FROM alpine:3.22.0 -RUN apk add --no-cache ca-certificates \ +RUN apk add --no-cache \ + bash \ + ca-certificates \ && adduser -D -h /var/lib/netbird netbird + WORKDIR /var/lib/netbird USER netbird:netbird -ENV NB_FOREGROUND_MODE=true -ENV NB_USE_NETSTACK_MODE=true -ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true -ENV NB_CONFIG=config.json -ENV NB_DAEMON_ADDR=unix://netbird.sock -ENV NB_DISABLE_DNS=true +ENV \ + NETBIRD_BIN="/usr/local/bin/netbird" \ + NB_USE_NETSTACK_MODE="true" \ + NB_ENABLE_NETSTACK_LOCAL_FORWARDING="true" \ + NB_CONFIG="/var/lib/netbird/config.json" \ + NB_STATE_DIR="/var/lib/netbird" \ + NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \ + NB_LOG_FILE="console,/var/lib/netbird/client.log" \ + NB_DISABLE_DNS="true" \ + NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \ + NB_ENTRYPOINT_LOGIN_TIMEOUT="1" -ENTRYPOINT [ "/usr/local/bin/netbird", "up" ] +ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ] + +ARG NETBIRD_BINARY=netbird +COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh +COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird diff --git a/client/cmd/down.go b/client/cmd/down.go index 3a324cc19..cfa69bce2 100644 --- a/client/cmd/down.go +++ b/client/cmd/down.go @@ -20,7 +20,7 @@ var downCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { log.Errorf("failed initializing log %v", err) return err diff --git a/client/cmd/login.go b/client/cmd/login.go index 14abcd034..8ac7086b8 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -32,7 +32,7 @@ var loginCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } @@ -50,7 +50,7 @@ var loginCmd = &cobra.Command{ } // workaround to run without service - if logFile == "console" { + if util.FindFirstLogPath(logFiles) == "" { err = handleRebrand(cmd) if err != nil { return err diff --git a/client/cmd/login_test.go b/client/cmd/login_test.go index fa20435ea..cf98a5854 100644 --- a/client/cmd/login_test.go +++ b/client/cmd/login_test.go @@ -21,7 +21,7 @@ func TestLogin(t *testing.T) { "--config", confPath, "--log-file", - "console", + util.LogConsole, "--setup-key", strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"), "--management-url", diff --git a/client/cmd/root.go b/client/cmd/root.go index bfd0d06c5..1774602c4 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -10,6 +10,7 @@ import ( "os/signal" "path" "runtime" + "slices" "strings" "syscall" "time" @@ -51,7 +52,7 @@ var ( defaultLogFile string oldDefaultLogFileDir string oldDefaultLogFile string - logFile string + logFiles []string daemonAddr string managementURL string adminURL string @@ -120,7 +121,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") - rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.") + rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character") rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") rootCmd.PersistentFlags().StringVar(&setupKeyPath, "setup-key-file", "", "The path to a setup key obtained from the Management Service Dashboard (used to register peer) This is ignored if the setup-key flag is provided.") rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file") @@ -265,7 +266,7 @@ func getSetupKeyFromFile(setupKeyPath string) (string, error) { func handleRebrand(cmd *cobra.Command) error { var err error - if logFile == defaultLogFile { + if slices.Contains(logFiles, defaultLogFile) { if migrateToNetbird(oldDefaultLogFile, defaultLogFile) { cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir) err = cpDir(oldDefaultLogFileDir, defaultLogFileDir) diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 2545623ec..df84342c9 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, configPath, logFile) + serverInstance := server.New(p.ctx, configPath, util.FindFirstLogPath(logFiles)) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } @@ -112,7 +112,7 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel return nil, err } - if err := util.InitLog(logLevel, logFile); err != nil { + if err := util.InitLog(logLevel, logFiles...); err != nil { return nil, fmt.Errorf("init log: %w", err) } @@ -136,7 +136,7 @@ var runCmd = &cobra.Command{ ctx, cancel := context.WithCancel(cmd.Context()) SetupCloseHandler(ctx, cancel) - SetupDebugHandler(ctx, nil, nil, nil, logFile) + SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles)) s, err := setupServiceControlCommand(cmd, ctx, cancel) if err != nil { diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go index 951efcc73..c994801a6 100644 --- a/client/cmd/service_installer.go +++ b/client/cmd/service_installer.go @@ -12,6 +12,8 @@ import ( "github.com/kardianos/service" "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/util" ) var ErrGetServiceStatus = fmt.Errorf("failed to get service status") @@ -41,7 +43,7 @@ func buildServiceArguments() []string { args = append(args, "--management-url", managementURL) } - if logFile != "" { + for _, logFile := range logFiles { args = append(args, "--log-file", logFile) } @@ -54,7 +56,7 @@ func configurePlatformSpecificSettings(svcConfig *service.Config) error { // Respected only by systemd systems svcConfig.Dependencies = []string{"After=network.target syslog.target"} - if logFile != "console" { + if logFile := util.FindFirstLogPath(logFiles); logFile != "" { setStdLogPath := true dir := filepath.Dir(logFile) diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index f9dbc26fc..264f643ee 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -46,7 +46,7 @@ var sshCmd = &cobra.Command{ cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } diff --git a/client/cmd/status.go b/client/cmd/status.go index 2d6e41bc2..e50156ac9 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -59,7 +59,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { return err } - err = util.InitLog(logLevel, "console") + err = util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } diff --git a/client/cmd/up.go b/client/cmd/up.go index b9781c0df..529beeac7 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -79,7 +79,7 @@ func upFunc(cmd *cobra.Command, args []string) error { cmd.SetOut(cmd.OutOrStdout()) - err := util.InitLog(logLevel, "console") + err := util.InitLog(logLevel, util.LogConsole) if err != nil { return fmt.Errorf("failed initializing log %v", err) } @@ -484,7 +484,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) { if !isValidAddrPort(customDNSAddress) { return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress) } - if customDNSAddress == "" && logFile != "console" { + if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" { parsed = []byte("empty") } else { parsed = []byte(customDNSAddress) diff --git a/client/iface/wgproxy/proxy_test.go b/client/iface/wgproxy/proxy_test.go index 2165b8aba..6882f9ea2 100644 --- a/client/iface/wgproxy/proxy_test.go +++ b/client/iface/wgproxy/proxy_test.go @@ -17,7 +17,7 @@ import ( ) func TestMain(m *testing.M) { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 6455b3aaf..220e6854d 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -16,6 +16,7 @@ import ( "path/filepath" "runtime" "runtime/pprof" + "slices" "sort" "strings" "time" @@ -28,6 +29,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" mgmProto "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/util" ) const readmeContent = `Netbird debug bundle @@ -283,7 +285,7 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("Failed to add wg show output: %v", err) } - if g.logFile != "console" && g.logFile != "" { + if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) { if err := g.addLogfile(); err != nil { log.Errorf("Failed to add log file to debug bundle: %v", err) if err := g.trySystemdLogFallback(); err != nil { diff --git a/client/internal/dns/file_repair_unix_test.go b/client/internal/dns/file_repair_unix_test.go index e948557b6..3aa0b859e 100644 --- a/client/internal/dns/file_repair_unix_test.go +++ b/client/internal/dns/file_repair_unix_test.go @@ -14,7 +14,7 @@ import ( ) func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index fffbed533..69586b47a 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -196,7 +196,7 @@ func (m *MockWGIface) LastActivities() map[string]monotime.Time { } func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index c5055e646..7cad45953 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -31,7 +31,7 @@ var connConf = ConnConfig{ } func TestMain(m *testing.M) { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/client/netbird-entrypoint.sh b/client/netbird-entrypoint.sh new file mode 100755 index 000000000..2422d2683 --- /dev/null +++ b/client/netbird-entrypoint.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="5"} +: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="1"} +NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}" +export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}" +service_pids=() +log_file_path="" + +_log() { + # mimic Go logger's output for easier parsing + # 2025-04-15T21:32:00+08:00 INFO client/internal/config.go:495: setting notifications to disabled by default + printf "$(date -Isec) ${1} ${BASH_SOURCE[1]}:${BASH_LINENO[1]}: ${2}\n" "${@:3}" >&2 +} + +info() { + _log INFO "$@" +} + +warn() { + _log WARN "$@" +} + +on_exit() { + info "Shutting down NetBird daemon..." + if test "${#service_pids[@]}" -gt 0; then + info "terminating service process IDs: ${service_pids[@]@Q}" + kill -TERM "${service_pids[@]}" 2>/dev/null || true + wait "${service_pids[@]}" 2>/dev/null || true + else + info "there are no service processes to terminate" + fi +} + +wait_for_message() { + local timeout="${1}" message="${2}" + if test "${timeout}" -eq 0; then + info "not waiting for log line ${message@Q} due to zero timeout." + elif test -n "${log_file_path}"; then + info "waiting for log line ${message@Q} for ${timeout} seconds..." + grep -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null) + else + info "log file unsupported, sleeping for ${timeout} seconds..." + sleep "${timeout}" + fi +} + +locate_log_file() { + local log_files_string="${1}" + + while read -r log_file; do + case "${log_file}" in + console | syslog) ;; + *) + log_file_path="${log_file}" + return + ;; + esac + done < <(sed 's#,#\n#g' <<<"${log_files_string}") + + warn "log files parsing for ${log_files_string@Q} is not supported by debug bundles" + warn "please consider removing the \$NB_LOG_FILE or setting it to real file, before gathering debug bundles." +} + +wait_for_daemon_startup() { + local timeout="${1}" + + if test -n "${log_file_path}"; then + if ! wait_for_message "${timeout}" "started daemon server"; then + warn "log line containing 'started daemon server' not found after ${timeout} seconds" + warn "daemon failed to start, exiting..." + exit 1 + fi + else + warn "daemon service startup not discovered, sleeping ${timeout} instead" + sleep "${timeout}" + fi +} + +login_if_needed() { + local timeout="${1}" + + if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered'; then + info "already logged in, skipping 'netbird up'..." + else + info "logging in..." + "${NETBIRD_BIN}" up + fi +} + +main() { + trap 'on_exit' SIGTERM SIGINT EXIT + "${NETBIRD_BIN}" service run & + service_pids+=("$!") + info "registered new service process 'netbird service run', currently running: ${service_pids[@]@Q}" + + locate_log_file "${NB_LOG_FILE}" + wait_for_daemon_startup "${NB_ENTRYPOINT_SERVICE_TIMEOUT}" + login_if_needed "${NB_ENTRYPOINT_LOGIN_TIMEOUT}" + + wait "${service_pids[@]}" +} + +main "$@" diff --git a/client/server/server_test.go b/client/server/server_test.go index 7c46aac5d..11e4d3899 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -32,6 +32,7 @@ import ( "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" + "github.com/netbirdio/netbird/util" ) var ( @@ -92,7 +93,7 @@ func TestConnectWithRetryRuns(t *testing.T) { func TestServer_Up(t *testing.T) { ctx := internal.CtxInitState(context.Background()) - s := New(ctx, t.TempDir()+"/config.json", "console") + s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) err := s.Start() require.NoError(t, err) @@ -130,7 +131,7 @@ func (m *mockSubscribeEventsServer) Context() context.Context { func TestServer_SubcribeEvents(t *testing.T) { ctx := internal.CtxInitState(context.Background()) - s := New(ctx, t.TempDir()+"/config.json", "console") + s := New(ctx, t.TempDir()+"/config.json", util.LogConsole) err := s.Start() require.NoError(t, err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index ace5b71e4..4480adb51 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -66,7 +66,7 @@ func main() { } logFile = file } else { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) } // Create the Fyne application. diff --git a/management/client/client_test.go b/management/client/client_test.go index b59b7c982..5b2a87492 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -41,7 +41,7 @@ import ( const ValidKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/client/client_test.go b/relay/client/client_test.go index c85ec9fd3..2ce8d7e34 100644 --- a/relay/client/client_test.go +++ b/relay/client/client_test.go @@ -30,7 +30,7 @@ var ( ) func TestMain(m *testing.M) { - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/cmd/root.go b/relay/cmd/root.go index 15090024c..7b8e5bbeb 100644 --- a/relay/cmd/root.go +++ b/relay/cmd/root.go @@ -73,7 +73,7 @@ var ( ) func init() { - _ = util.InitLog("trace", "console") + _ = util.InitLog("trace", util.LogConsole) cobraConfig = &Config{} rootCmd.PersistentFlags().StringVarP(&cobraConfig.ListenAddress, "listen-address", "l", ":443", "listen address") rootCmd.PersistentFlags().StringVarP(&cobraConfig.ExposedAddress, "exposed-address", "e", "", "instance domain address (or ip) and port, it will be distributes between peers") diff --git a/relay/test/benchmark_test.go b/relay/test/benchmark_test.go index 2e67ab803..afbb14b84 100644 --- a/relay/test/benchmark_test.go +++ b/relay/test/benchmark_test.go @@ -27,7 +27,7 @@ var ( ) func TestMain(m *testing.M) { - _ = util.InitLog("error", "console") + _ = util.InitLog("error", util.LogConsole) code := m.Run() os.Exit(code) } diff --git a/relay/testec2/main.go b/relay/testec2/main.go index 0c8099a5e..6954d6a50 100644 --- a/relay/testec2/main.go +++ b/relay/testec2/main.go @@ -233,7 +233,7 @@ func TURNReaderMain() []testResult { func main() { var mode string - _ = util.InitLog("debug", "console") + _ = util.InitLog("debug", util.LogConsole) flag.StringVar(&mode, "mode", "sender", "sender or receiver mode") flag.Parse() diff --git a/upload-server/main.go b/upload-server/main.go index dcfb35cdf..546c0f584 100644 --- a/upload-server/main.go +++ b/upload-server/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - err := util.InitLog("info", "console") + err := util.InitLog("info", util.LogConsole) if err != nil { log.Fatalf("Failed to initialize logger: %v", err) } diff --git a/util/log.go b/util/log.go index 53d2b0684..a951eab87 100644 --- a/util/log.go +++ b/util/log.go @@ -16,36 +16,54 @@ import ( const defaultLogSize = 15 +const ( + LogConsole = "console" + LogSyslog = "syslog" +) + +var ( + SpecialLogs = []string{ + LogSyslog, + LogConsole, + } +) + // InitLog parses and sets log-level input -func InitLog(logLevel string, logPath string) error { +func InitLog(logLevel string, logs ...string) error { level, err := log.ParseLevel(logLevel) if err != nil { log.Errorf("Failed parsing log-level %s: %s", logLevel, err) return err } - customOutputs := []string{"console", "syslog"} + var writers []io.Writer + logFmt := os.Getenv("NB_LOG_FORMAT") - if logPath != "" && !slices.Contains(customOutputs, logPath) { - maxLogSize := getLogMaxSize() - lumberjackLogger := &lumberjack.Logger{ - // Log file absolute path, os agnostic - Filename: filepath.ToSlash(logPath), - MaxSize: maxLogSize, // MB - MaxBackups: 10, - MaxAge: 30, // days - Compress: true, + for _, logPath := range logs { + switch logPath { + case LogSyslog: + AddSyslogHook() + logFmt = "syslog" + case LogConsole: + writers = append(writers, os.Stderr) + case "": + log.Warnf("empty log path received: %#v", logPath) + default: + writers = append(writers, newRotatedOutput(logPath)) } - log.SetOutput(io.Writer(lumberjackLogger)) - } else if logPath == "syslog" { - AddSyslogHook() } - //nolint:gocritic - if os.Getenv("NB_LOG_FORMAT") == "json" { + if len(writers) > 1 { + log.SetOutput(io.MultiWriter(writers...)) + } else if len(writers) == 1 { + log.SetOutput(writers[0]) + } + + switch logFmt { + case "json": formatter.SetJSONFormatter(log.StandardLogger()) - } else if logPath == "syslog" { + case "syslog": formatter.SetSyslogFormatter(log.StandardLogger()) - } else { + default: formatter.SetTextFormatter(log.StandardLogger()) } log.SetLevel(level) @@ -55,6 +73,29 @@ func InitLog(logLevel string, logPath string) error { return 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 newRotatedOutput(logPath string) io.Writer { + maxLogSize := getLogMaxSize() + lumberjackLogger := &lumberjack.Logger{ + // Log file absolute path, os agnostic + Filename: filepath.ToSlash(logPath), + MaxSize: maxLogSize, // MB + MaxBackups: 10, + MaxAge: 30, // days + Compress: true, + } + return lumberjackLogger +} + func setGRPCLibLogger() { logOut := log.StandardLogger().Writer() if os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL") != "info" {