From 3d7f734ac98b74ad3fa149498b881f30ff6b4805 Mon Sep 17 00:00:00 2001
From: Dimitry Kolyshev <dkolyshev@adguard.com>
Date: Fri, 5 Jul 2024 11:05:14 +0300
Subject: [PATCH] all: slog logger

---
 internal/aghos/syslog.go         |  6 ++-
 internal/aghos/syslog_others.go  | 13 +++----
 internal/aghos/syslog_windows.go | 23 ++++++-----
 internal/home/home.go            | 21 ++++++----
 internal/home/log.go             | 66 +++++++++++++++++++++++++++++---
 internal/next/cmd/log.go         |  6 ++-
 6 files changed, 102 insertions(+), 33 deletions(-)

diff --git a/internal/aghos/syslog.go b/internal/aghos/syslog.go
index a67a5635..5e58bdcb 100644
--- a/internal/aghos/syslog.go
+++ b/internal/aghos/syslog.go
@@ -1,6 +1,8 @@
 package aghos
 
-// ConfigureSyslog reroutes standard logger output to syslog.
-func ConfigureSyslog(serviceName string) error {
+import "io"
+
+// ConfigureSyslog returns an output rerouted to syslog.
+func ConfigureSyslog(serviceName string) (w io.Writer, err error) {
 	return configureSyslog(serviceName)
 }
diff --git a/internal/aghos/syslog_others.go b/internal/aghos/syslog_others.go
index 1659ae49..4242dd84 100644
--- a/internal/aghos/syslog_others.go
+++ b/internal/aghos/syslog_others.go
@@ -3,16 +3,15 @@
 package aghos
 
 import (
+	"io"
 	"log/syslog"
-
-	"github.com/AdguardTeam/golibs/log"
 )
 
-func configureSyslog(serviceName string) error {
-	w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
+func configureSyslog(serviceName string) (w io.Writer, err error) {
+	w, err = syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	log.SetOutput(w)
-	return nil
+
+	return w, nil
 }
diff --git a/internal/aghos/syslog_windows.go b/internal/aghos/syslog_windows.go
index c8e86e78..21de19c1 100644
--- a/internal/aghos/syslog_windows.go
+++ b/internal/aghos/syslog_windows.go
@@ -3,9 +3,9 @@
 package aghos
 
 import (
+	"io"
 	"strings"
 
-	"github.com/AdguardTeam/golibs/log"
 	"golang.org/x/sys/windows"
 	"golang.org/x/sys/windows/svc/eventlog"
 )
@@ -19,23 +19,26 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
 	return len(b), w.el.Info(1, string(b))
 }
 
-func configureSyslog(serviceName string) error {
-	// Note that the eventlog src is the same as the service name
-	// Otherwise, we will get "the description for event id cannot be found" warning in every log record
+func configureSyslog(serviceName string) (w io.Writer, err error) {
+	// Note that the eventlog src is the same as the service name.  Otherwise,
+	// we will get "the description for event id cannot be found" warning in
+	// every log record.
 
 	// Continue if we receive "registry key already exists" or if we get
 	// ERROR_ACCESS_DENIED so that we can log without administrative permissions
 	// for pre-existing eventlog sources.
-	if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil {
-		if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED {
-			return err
+	err = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)
+	if err != nil {
+		if !strings.Contains(err.Error(), "registry key already exists") &&
+			err != windows.ERROR_ACCESS_DENIED {
+			return nil, err
 		}
 	}
+
 	el, err := eventlog.Open(serviceName)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	log.SetOutput(&eventLogWriter{el: el})
-	return nil
+	return &eventLogWriter{el: el}, nil
 }
diff --git a/internal/home/home.go b/internal/home/home.go
index fc64f376..0897d474 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -38,6 +38,7 @@ import (
 	"github.com/AdguardTeam/golibs/errors"
 	"github.com/AdguardTeam/golibs/hostsfile"
 	"github.com/AdguardTeam/golibs/log"
+	"github.com/AdguardTeam/golibs/logutil/slogutil"
 	"github.com/AdguardTeam/golibs/netutil"
 	"github.com/AdguardTeam/golibs/osutil"
 )
@@ -547,15 +548,21 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
 	// Configure config filename.
 	initConfigFilename(opts)
 
+	ls := getLogSettings(opts)
+
 	// Configure log level and output.
-	err = configureLogger(opts)
+	err = configureLogger(ls)
+	fatalOnError(err)
+
+	// Configure slog logger.
+	l, err := initLogger(ls)
 	fatalOnError(err)
 
 	// Print the first message after logger is configured.
-	log.Info(version.Full())
-	log.Debug("current working directory is %s", Context.workDir)
+	l.Info(version.Full())
+	l.Debug("current working directory is set", "work_dir", Context.workDir)
 	if opts.runningAsService {
-		log.Info("AdGuard Home is running as a service")
+		l.Info("AdGuard Home is running as a service")
 	}
 
 	err = setupContext(opts)
@@ -586,7 +593,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
 	}
 
 	confPath := configFilePath()
-	log.Debug("using config path %q for updater", confPath)
+	l.Debug("using config path for updater", "path", confPath)
 
 	upd := updater.NewUpdater(&updater.Config{
 		Client:          config.Filtering.HTTPClient,
@@ -628,7 +635,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
 
 	Context.tls, err = newTLSManager(config.TLS, config.DNS.ServePlainDNS)
 	if err != nil {
-		log.Error("initializing tls: %s", err)
+		l.Error("initializing tls", slogutil.KeyError, err)
 		onConfigModified()
 	}
 
@@ -652,7 +659,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
 		if Context.dhcpServer != nil {
 			err = Context.dhcpServer.Start()
 			if err != nil {
-				log.Error("starting dhcp server: %s", err)
+				l.Error("starting dhcp server", slogutil.KeyError, err)
 			}
 		}
 	}
diff --git a/internal/home/log.go b/internal/home/log.go
index fd18d1ec..a83b9782 100644
--- a/internal/home/log.go
+++ b/internal/home/log.go
@@ -3,11 +3,14 @@ package home
 import (
 	"cmp"
 	"fmt"
+	"io"
+	"log/slog"
 	"path/filepath"
 	"runtime"
 
 	"github.com/AdguardTeam/AdGuardHome/internal/aghos"
 	"github.com/AdguardTeam/golibs/log"
+	"github.com/AdguardTeam/golibs/logutil/slogutil"
 	"gopkg.in/natefinch/lumberjack.v2"
 	"gopkg.in/yaml.v3"
 )
@@ -16,10 +19,58 @@ import (
 // for logger output.
 const configSyslog = "syslog"
 
-// configureLogger configures logger level and output.
-func configureLogger(opts options) (err error) {
-	ls := getLogSettings(opts)
+// initLogger returns new [*slog.Logger] configured with the given settings.
+func initLogger(ls *logSettings) (l *slog.Logger, err error) {
+	if !ls.Enabled {
+		return slogutil.NewDiscardLogger(), nil
+	}
 
+	w, err := logOutput(ls)
+	if err != nil {
+		return nil, fmt.Errorf("cannot initialize log output: %w", err)
+	}
+
+	return slogutil.New(&slogutil.Config{
+		Output:       w,
+		Format:       slogutil.FormatDefault,
+		AddTimestamp: true,
+		Verbose:      ls.Verbose,
+	}), nil
+}
+
+// logOutput returns a log output [io.Writer] configured with the settings.
+func logOutput(ls *logSettings) (w io.Writer, err error) {
+	if ls.File == "" {
+		return nil, nil
+	}
+
+	if ls.File == configSyslog {
+		// Use syslog where it is possible and eventlog on Windows.
+		w, err = aghos.ConfigureSyslog(serviceName)
+		if err != nil {
+			return nil, fmt.Errorf("cannot initialize syslog: %w", err)
+		}
+
+		return w, nil
+	}
+
+	logFilePath := ls.File
+	if !filepath.IsAbs(logFilePath) {
+		logFilePath = filepath.Join(Context.workDir, logFilePath)
+	}
+
+	return &lumberjack.Logger{
+		Filename:   logFilePath,
+		Compress:   ls.Compress,
+		LocalTime:  ls.LocalTime,
+		MaxBackups: ls.MaxBackups,
+		MaxSize:    ls.MaxSize,
+		MaxAge:     ls.MaxAge,
+	}, nil
+}
+
+// configureLogger configures logger level and output.
+func configureLogger(ls *logSettings) (err error) {
 	// Configure logger level.
 	if !ls.Enabled {
 		log.SetLevel(log.OFF)
@@ -33,16 +84,19 @@ func configureLogger(opts options) (err error) {
 
 	// Write logs to stdout by default.
 	if ls.File == "" {
-		return nil
+		return err
 	}
 
 	if ls.File == configSyslog {
 		// Use syslog where it is possible and eventlog on Windows.
-		err = aghos.ConfigureSyslog(serviceName)
+		var w io.Writer
+		w, err = aghos.ConfigureSyslog(serviceName)
 		if err != nil {
 			return fmt.Errorf("cannot initialize syslog: %w", err)
 		}
 
+		log.SetOutput(w)
+
 		return nil
 	}
 
@@ -60,7 +114,7 @@ func configureLogger(opts options) (err error) {
 		MaxAge:     ls.MaxAge,
 	})
 
-	return nil
+	return err
 }
 
 // getLogSettings returns a log settings object properly initialized from opts.
diff --git a/internal/next/cmd/log.go b/internal/next/cmd/log.go
index 3aa2a0e5..6c834082 100644
--- a/internal/next/cmd/log.go
+++ b/internal/next/cmd/log.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"fmt"
+	"io"
 	"os"
 
 	"github.com/AdguardTeam/AdGuardHome/internal/aghos"
@@ -22,10 +23,13 @@ func setLog(opts *options) (err error) {
 	case "stderr":
 		log.SetOutput(os.Stderr)
 	case "syslog":
-		err = aghos.ConfigureSyslog(syslogServiceName)
+		var w io.Writer
+		w, err = aghos.ConfigureSyslog(syslogServiceName)
 		if err != nil {
 			return fmt.Errorf("initializing syslog: %w", err)
 		}
+
+		log.SetOutput(w)
 	default:
 		// TODO(a.garipov): Use the path.
 	}