Repository: metallb/metallb
Pull request #1180 (2022-01-21 16:07:52)

Forward speaker log level to FRR configuration in order to improve debuggability
Hi,

To allow debugging of FRR speaker container, it would be useful to forward the speaker `--log-level` parameter to the FRR configuration. Not sure about the design in forwarding the logLevel parameters from `main()` to `frr.NewSessionManager(...)`, feedback is wellcome.

It would also useful to add lines like these to the configuration, in order to debug specific protocols:

```
debug zebra events
debug zebra nht
debug zebra kernel
debug zebra rib
debug zebra nexthop
debug bgp bfd
debug bgp neighbor-events
debug bgp updates
debug bgp keepalives
debug bgp nht
debug bgp zebra
debug bfd distributed
debug bfd network
debug bfd peer
debug bfd zebra
```

To achieve that, the following options come to my mind:
a. Add all that configuration if `--log-level` is set do debug, or if condition like that is met.
b. Allow the user to inject custom configuration snippets at the bottom of the FRR configuration file. It can be done either through the ConfigMap (or CR in the future), or with an environment variable. This option can become handy when facing unexpected scenarios.
c. Add a speaker parameters like `--debug-frr-protocols <comma-separated-list-of-protocols>` (like `--debug-frr-protocols bfd, bgp`) ot enable protocol specific debug commands.

Please, tell me if I'm over-engineered this last feature. Maybe it can be much simpler.

Regards

  




cgoncalves: > a. Add all that configuration if `--log-level` is set do debug, or if condition like that is met.

Yes.

> b. Allow the user to inject custom configuration snippets at the bottom of the FRR configuration file. It can be done either through the ConfigMap (or CR in the future), or with an environment variable. This option can become handy when facing unexpected scenarios.

Extra care is needed when allowing users to add arbitrary configurations (security reasons, project/product support levels, etc). There might be some other non-logging options that users would like to have more control of by means of arbitrary FRR configurations. I think that should be discussed separately from this PR.

> c. Add a speaker parameters like `--debug-frr-protocols <comma-separated-list-of-protocols>` (like `--debug-frr-protocols bfd, bgp`) ot enable protocol specific debug commands.

Zebra and BGP are core functions of any FRR-based MetalLB BGP deployment. BFD may or may not be used, and will only output debug level messages if enabled by at least one of its peers. I am in favor of enabling all of the abovementioned.
zeeke: > Zebra and BGP are core functions of any FRR-based MetalLB BGP deployment. BFD may or may not be used, and will only output debug level messages if enabled by at least one of its peers. I am in favor of enabling all of the abovementioned.

Agree, simple is better!

cgoncalves: Upgrading to FRR 8.x is a sensitive topic that was considered not so long ago when adding initial FRR support and later BFD support. While upgrading can easily be done upstream, we have to consider the possible ramifications it may have on vendors who may not so easily offer the latest and greatest FRR. In this case and in my opinion, upgrading FRR so that those two debug options can be enabled does not outweigh bumping the minimum required FRR version. Happy to discuss this further!
zeeke: Again, I agree.
liornoy: LGTM
fedepaol: @zeeke not sure you missed the last comments
fedepaol: LGTM
fedepaol: @zeeke looks like all the frr lanes are failing, mind having a look?
zeeke: Sure, just pushed.

Do you mind consider [the comment about the parameters help string](https://github.com/metallb/metallb/pull/1180#discussion_r796915665)?
fedepaol: LGTM

Patch:
diff --git a/controller/main.go b/controller/main.go
index 409a8446..bca642f8 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -20,7 +20,6 @@ import (
 	"io/ioutil"
 	"os"
 	"reflect"
-	"strings"
 
 	"go.universe.tf/metallb/internal/allocator"
 	"go.universe.tf/metallb/internal/config"
@@ -129,7 +128,7 @@ func main() {
 		kubeconfig      = flag.String("kubeconfig", "", "absolute path to the kubeconfig file (only needed when running outside of k8s)")
 		mlSecret        = flag.String("ml-secret-name", os.Getenv("METALLB_ML_SECRET_NAME"), "name of the memberlist secret to create")
 		deployName      = flag.String("deployment", os.Getenv("METALLB_DEPLOYMENT"), "name of the MetalLB controller Deployment")
-		logLevel        = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", strings.Join(logging.Levels, ", ")))
+		logLevel        = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", logging.Levels.String()))
 		disableEpSlices = flag.Bool("disable-epslices", false, "Disable the usage of EndpointSlices and default to Endpoints instead of relying on the autodiscovery mechanism")
 		enablePprof     = flag.Bool("enable-pprof", false, "Enable pprof profiling")
 	)
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index cbbfd2fc..a6487420 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -31,6 +31,21 @@ var reloaderPidFileName = "/etc/frr_reloader/reloader.pid"
 const configTemplate = `
 log file /etc/frr/frr.log {{.Loglevel}}
 log timestamp precision 3
+{{- if eq .Loglevel "debugging" }}
+debug zebra events
+debug zebra nht
+debug zebra kernel
+debug zebra rib
+debug zebra nexthop
+debug bgp neighbor-events
+debug bgp updates
+debug bgp keepalives
+debug bgp nht
+debug bgp zebra
+debug bfd network
+debug bfd peer
+debug bfd zebra
+{{- end }}
 hostname {{.Hostname}}
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 430bafe9..d63be8a8 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -14,6 +14,7 @@ import (
 	"go.universe.tf/metallb/internal/bgp"
 	metallbconfig "go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/ipfamily"
+	"go.universe.tf/metallb/internal/logging"
 )
 
 // As the MetalLB controller should handle messages synchronously, there should
@@ -23,6 +24,7 @@ type sessionManager struct {
 	sessions     map[string]*session
 	bfdProfiles  []BFDProfile
 	reloadConfig chan *frrConfig
+	logLevel     string
 }
 
 type session struct {
@@ -196,7 +198,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 
 	config := &frrConfig{
 		Hostname:               hostname,
-		Loglevel:               "informational",
+		Loglevel:               sm.logLevel,
 		Routers:                make(map[string]*routerConfig),
 		BFDProfiles:            sm.bfdProfiles,
 		PrefixesV4ForCommunity: make([]communityPrefixes, 0),
@@ -204,6 +206,8 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 		PrefixesV4ForLocalPref: make([]localPrefPrefixes, 0),
 		PrefixesV6ForLocalPref: make([]localPrefPrefixes, 0),
 	}
+
+	// leave it for backward compatibility
 	frrLogLevel, found := os.LookupEnv("FRR_LOGGING_LEVEL")
 	if found {
 		config.Loglevel = frrLogLevel
@@ -354,11 +358,12 @@ func mapToCommunityPrefixes(m map[string]stringSet) []communityPrefixes {
 
 var debounceTimeout = 500 * time.Millisecond
 
-func NewSessionManager(l log.Logger) *sessionManager {
+func NewSessionManager(l log.Logger, logLevel logging.Level) *sessionManager {
 	res := &sessionManager{
 		sessions:     map[string]*session{},
 		bfdProfiles:  []BFDProfile{},
 		reloadConfig: make(chan *frrConfig),
+		logLevel:     logLevelToFRR(logLevel),
 	}
 	reload := func(config *frrConfig) {
 		generateAndReloadConfigFile(config, l)
@@ -380,3 +385,22 @@ func configBFDProfileToFRR(p *metallbconfig.BFDProfile) *BFDProfile {
 	res.MinimumTTL = p.MinimumTTL
 	return res
 }
+
+func logLevelToFRR(level logging.Level) string {
+	// Allowed frr log levels are: emergencies, alerts, critical,
+	// 		errors, warnings, notifications, informational, or debugging
+	switch level {
+	case logging.LevelAll, logging.LevelDebug:
+		return "debugging"
+	case logging.LevelInfo:
+		return "informational"
+	case logging.LevelWarn:
+		return "warnings"
+	case logging.LevelError:
+		return "error"
+	case logging.LevelNone:
+		return "emergencies"
+	}
+
+	return "informational"
+}
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
index 08c74f6d..531d8132 100644
--- a/internal/logging/logging.go
+++ b/internal/logging/logging.go
@@ -11,6 +11,7 @@ import (
 	"os"
 	"regexp"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/go-kit/kit/log"
@@ -19,19 +20,30 @@ import (
 )
 
 const (
-	levelAll   = "all"
-	levelDebug = "debug"
-	levelInfo  = "info"
-	levelWarn  = "warn"
-	levelError = "error"
-	levelNone  = "none"
+	LevelAll   = "all"
+	LevelDebug = "debug"
+	LevelInfo  = "info"
+	LevelWarn  = "warn"
+	LevelError = "error"
+	LevelNone  = "none"
 )
 
+type Level string
+type levelSlice []Level
+
 var (
 	// Levels returns an array of valid log levels.
-	Levels = []string{levelAll, levelDebug, levelInfo, levelWarn, levelError, levelNone}
+	Levels = levelSlice{LevelAll, LevelDebug, LevelInfo, LevelWarn, LevelError, LevelNone}
 )
 
+func (l levelSlice) String() string {
+	strs := make([]string, len(l))
+	for i, v := range l {
+		strs[i] = string(v)
+	}
+	return strings.Join(strs, ", ")
+}
+
 // Init returns a logger configured with common settings like
 // timestamping and source code locations. Both the stdlib logger and
 // glog are reconfigured to push logs into this logger.
@@ -145,17 +157,17 @@ func deformat(logger log.Logger, b []byte) (leveledLogger log.Logger, ts time.Ti
 
 func parseLevel(lvl string) (level.Option, error) {
 	switch lvl {
-	case levelAll:
+	case LevelAll:
 		return level.AllowAll(), nil
-	case levelDebug:
+	case LevelDebug:
 		return level.AllowDebug(), nil
-	case levelInfo:
+	case LevelInfo:
 		return level.AllowInfo(), nil
-	case levelWarn:
+	case LevelWarn:
 		return level.AllowWarn(), nil
-	case levelError:
+	case LevelError:
 		return level.AllowError(), nil
-	case levelNone:
+	case LevelNone:
 		return level.AllowNone(), nil
 	}
 
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 30a8f637..6593da0e 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -26,6 +26,7 @@ import (
 	bgpnative "go.universe.tf/metallb/internal/bgp/native"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/k8s"
+	"go.universe.tf/metallb/internal/logging"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
 
@@ -314,12 +315,12 @@ func (c *bgpController) SetNode(l log.Logger, node *v1.Node) error {
 }
 
 // Create a new 'bgp.SessionManager' of type 'bgpType'.
-var newBGP = func(bgpType bgpImplementation, l log.Logger) bgp.SessionManager {
+var newBGP = func(bgpType bgpImplementation, l log.Logger, logLevel logging.Level) bgp.SessionManager {
 	switch bgpType {
 	case bgpNative:
 		return bgpnative.NewSessionManager(l)
 	case bgpFrr:
-		return bgpfrr.NewSessionManager(l)
+		return bgpfrr.NewSessionManager(l, logLevel)
 	default:
 		panic(fmt.Sprintf("unsupported BGP implementation type: %s", bgpType))
 	}
diff --git a/speaker/main.go b/speaker/main.go
index 3c698588..349f2e58 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -21,7 +21,6 @@ import (
 	"net"
 	"os"
 	"os/signal"
-	"strings"
 	"syscall"
 
 	"go.universe.tf/metallb/internal/bgp"
@@ -72,7 +71,7 @@ func main() {
 		mlSecret        = flag.String("ml-secret-key", os.Getenv("METALLB_ML_SECRET_KEY"), "Secret key for MemberList (fast dead node detection)")
 		myNode          = flag.String("node-name", os.Getenv("METALLB_NODE_NAME"), "name of this Kubernetes node (spec.nodeName)")
 		port            = flag.Int("port", 7472, "HTTP listening port")
-		logLevel        = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", strings.Join(logging.Levels, ", ")))
+		logLevel        = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", logging.Levels.String()))
 		disableEpSlices = flag.Bool("disable-epslices", false, "Disable the usage of EndpointSlices and default to Endpoints instead of relying on the autodiscovery mechanism")
 		enablePprof     = flag.Bool("enable-pprof", false, "Enable pprof profiling")
 	)
@@ -125,10 +124,11 @@ func main() {
 
 	// Setup all clients and speakers, config decides what is being done runtime.
 	ctrl, err := newController(controllerConfig{
-		MyNode:  *myNode,
-		Logger:  logger,
-		SList:   sList,
-		bgpType: bgpImplementation(bgpType),
+		MyNode:   *myNode,
+		Logger:   logger,
+		LogLevel: logging.Level(*logLevel),
+		SList:    sList,
+		bgpType:  bgpImplementation(bgpType),
 	})
 	if err != nil {
 		level.Error(logger).Log("op", "startup", "error", err, "msg", "failed to create MetalLB controller")
@@ -188,9 +188,10 @@ type controller struct {
 }
 
 type controllerConfig struct {
-	MyNode string
-	Logger log.Logger
-	SList  SpeakerList
+	MyNode   string
+	Logger   log.Logger
+	LogLevel logging.Level
+	SList    SpeakerList
 
 	bgpType bgpImplementation
 
@@ -206,7 +207,7 @@ func newController(cfg controllerConfig) (*controller, error) {
 			myNode:         cfg.MyNode,
 			svcAds:         make(map[string][]*bgp.Advertisement),
 			bgpType:        cfg.bgpType,
-			sessionManager: newBGP(cfg.bgpType, cfg.Logger),
+			sessionManager: newBGP(cfg.bgpType, cfg.Logger, cfg.LogLevel),
 		},
 	}
 
diff --git a/website/content/installation/_index.md b/website/content/installation/_index.md
index 256415c4..ea676d91 100644
--- a/website/content/installation/_index.md
+++ b/website/content/installation/_index.md
@@ -189,6 +189,20 @@ and change the `BGP_TYPE` environment variable of the `manager` container to `fr
   value: frr
 ```
 
+## FRR daemons logging level
+
+The FRR daemons logging level are configured using the speaker `--log-level` argument following the below mapping:
+
+Speaker log level | FRR log level
+------------------|--------------
+all, debug        | debugging
+info              | informational
+warn              | warnings
+error             | error
+none              | emergencies
+
+To override this behavior, you can set the `FRR_LOGGING_LEVEL` speaker's environment to any [FRR supported value](https://docs.frrouting.org/en/latest/basic.html#clicmd-log-stdout-LEVEL).
+
 ## Upgrade
 
 When upgrading MetalLB, always check the [release notes](https://metallb.universe.tf/release-notes/)


Test patch:
diff --git a/e2etest/pkg/frr/config/config.go b/e2etest/pkg/frr/config/config.go
index 19d49b6c..7b37317c 100644
--- a/e2etest/pkg/frr/config/config.go
+++ b/e2etest/pkg/frr/config/config.go
@@ -36,7 +36,6 @@ ipv6 nht resolve-via-default
 
 log file /tmp/frr.log debugging
 log timestamp precision 3
-
 route-map RMAP permit 10
 set ipv6 next-hop prefer-global
 {{$ROUTERASN:=.ASN}}
diff --git a/internal/bgp/frr/frr_bfd_test.go b/internal/bgp/frr/frr_bfd_test.go
index 73ab2b41..a7544ded 100644
--- a/internal/bgp/frr/frr_bfd_test.go
+++ b/internal/bgp/frr/frr_bfd_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/go-kit/kit/log"
 	"go.universe.tf/metallb/internal/config"
+	"go.universe.tf/metallb/internal/logging"
 )
 
 func TestBFDProfileNoSessions(t *testing.T) {
@@ -37,7 +38,7 @@ func TestBFDProfileNoSessions(t *testing.T) {
 		},
 	}
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 
 	err := sessionManager.SyncBFDProfiles(pp)
@@ -69,7 +70,7 @@ func TestBFDProfileCornerCases(t *testing.T) {
 	}
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 
 	err := sessionManager.SyncBFDProfiles(pp)
@@ -111,7 +112,7 @@ func TestBFDWithSession(t *testing.T) {
 	}
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 
 	err := sessionManager.SyncBFDProfiles(pp)
@@ -152,7 +153,7 @@ func TestBFDProfileAllDefault(t *testing.T) {
 	}
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 
 	err := sessionManager.SyncBFDProfiles(pp)
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 1bc3b8c7..639b3e80 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/pkg/errors"
 	"go.universe.tf/metallb/internal/bgp"
 	"go.universe.tf/metallb/internal/config"
+	"go.universe.tf/metallb/internal/logging"
 	"k8s.io/apimachinery/pkg/util/wait"
 )
 
@@ -89,7 +90,7 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -104,7 +105,7 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "127.0.0.2:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", false)
 	if err != nil {
@@ -119,7 +120,7 @@ func TestSingleIBGPSession(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 100, time.Second, time.Second, "password", "hostname", "", false)
 	if err != nil {
@@ -134,7 +135,7 @@ func TestSingleSessionClose(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
@@ -149,7 +150,7 @@ func TestTwoSessions(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -169,7 +170,7 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -189,7 +190,7 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -209,7 +210,7 @@ func TestSingleAdvertisement(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -245,7 +246,7 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, nil, 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -275,7 +276,7 @@ func TestSingleAdvertisementInvalidPrefix(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -302,7 +303,7 @@ func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err == nil {
@@ -318,7 +319,7 @@ func TestSingleAdvertisementInvalidNextHop(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -347,7 +348,7 @@ func TestSingleAdvertisementStop(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -382,7 +383,7 @@ func TestSingleAdvertisementChange(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -427,7 +428,7 @@ func TestTwoAdvertisements(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -470,7 +471,7 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 	testSetup(t)
 
 	l := log.NewNopLogger()
-	sessionManager := NewSessionManager(l)
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "", true)
 	if err != nil {
@@ -520,3 +521,55 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 
 	testCheckConfigFile(t)
 }
+
+func TestLoggingConfiguration(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := NewSessionManager(l, logging.LevelWarn)
+	defer close(sessionManager.reloadConfig)
+
+	config, err := sessionManager.createConfig()
+	if err != nil {
+		t.Fatalf("Error while creating configuration: %s", err)
+	}
+
+	sessionManager.reloadConfig <- config
+	testCheckConfigFile(t)
+}
+
+func TestLoggingConfigurationDebug(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := NewSessionManager(l, logging.LevelDebug)
+	defer close(sessionManager.reloadConfig)
+
+	config, err := sessionManager.createConfig()
+	if err != nil {
+		t.Fatalf("Error while creating configuration: %s", err)
+	}
+
+	sessionManager.reloadConfig <- config
+	testCheckConfigFile(t)
+}
+
+func TestLoggingConfigurationOverrideByEnvironmentVar(t *testing.T) {
+	testSetup(t)
+
+	orig := os.Getenv("FRR_LOGGING_LEVEL")
+	os.Setenv("FRR_LOGGING_LEVEL", "alerts")
+	t.Cleanup(func() { os.Setenv("FRR_LOGGING_LEVEL", orig) })
+
+	l := log.NewNopLogger()
+	sessionManager := NewSessionManager(l, logging.LevelDebug)
+	defer close(sessionManager.reloadConfig)
+
+	config, err := sessionManager.createConfig()
+	if err != nil {
+		t.Fatalf("Error while creating configuration: %s", err)
+	}
+
+	sessionManager.reloadConfig <- config
+	testCheckConfigFile(t)
+}
diff --git a/internal/bgp/frr/testdata/TestLoggingConfiguration.golden b/internal/bgp/frr/testdata/TestLoggingConfiguration.golden
new file mode 100644
index 00000000..71fa3bdb
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestLoggingConfiguration.golden
@@ -0,0 +1,7 @@
+
+log file /etc/frr/frr.log warnings
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+
diff --git a/internal/bgp/frr/testdata/TestLoggingConfigurationDebug.golden b/internal/bgp/frr/testdata/TestLoggingConfigurationDebug.golden
new file mode 100644
index 00000000..9ea9b0eb
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestLoggingConfigurationDebug.golden
@@ -0,0 +1,20 @@
+
+log file /etc/frr/frr.log debugging
+log timestamp precision 3
+debug zebra events
+debug zebra nht
+debug zebra kernel
+debug zebra rib
+debug zebra nexthop
+debug bgp neighbor-events
+debug bgp updates
+debug bgp keepalives
+debug bgp nht
+debug bgp zebra
+debug bfd network
+debug bfd peer
+debug bfd zebra
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+
diff --git a/internal/bgp/frr/testdata/TestLoggingConfigurationOverrideByEnvironmentVar.golden b/internal/bgp/frr/testdata/TestLoggingConfigurationOverrideByEnvironmentVar.golden
new file mode 100644
index 00000000..deeac983
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestLoggingConfigurationOverrideByEnvironmentVar.golden
@@ -0,0 +1,7 @@
+
+log file /etc/frr/frr.log alerts
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 7f7cc6c9..e2a052eb 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -14,6 +14,7 @@ import (
 	"go.universe.tf/metallb/internal/bgp"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/k8s"
+	"go.universe.tf/metallb/internal/logging"
 
 	"github.com/go-kit/kit/log"
 	"github.com/google/go-cmp/cmp"
@@ -95,7 +96,7 @@ type fakeBGP struct {
 	sessionManager fakeBGPSessionManager
 }
 
-func (f *fakeBGP) NewSessionManager(_ bgpImplementation, _ log.Logger) bgp.SessionManager {
+func (f *fakeBGP) NewSessionManager(_ bgpImplementation, _ log.Logger, _ logging.Level) bgp.SessionManager {
 	f.sessionManager.t = f.t
 	f.sessionManager.gotAds = make(map[string][]*bgp.Advertisement)