From f8ad1da55c9d3572006334f21730a90e15857b89 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Tue, 10 Dec 2024 16:09:24 +0100 Subject: [PATCH] feat(logging)_: enable runtime logs configuration Add endpoints for log level and namespaces configuration to `status.go` and deprecate equivalents from service. Endpoints are defined in `status.go`, as it has access to `statusBackend`, the only entity capable of manipulating node configuration without requiring a restart. --- api/geth_backend.go | 32 ++++++++++++--- logutils/core.go | 3 +- mobile/status.go | 40 ++++++++++++++++++- params/config.go | 9 ++++- protocol/messenger_settings.go | 2 + tests-functional/tests/test_logging.py | 54 +++++++++++++------------- 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/api/geth_backend.go b/api/geth_backend.go index b0580a0deac..6873ad77da1 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -2079,12 +2079,6 @@ func (b *GethStatusBackend) loadNodeConfig(inputNodeCfg *params.NodeConfig) erro } } - if len(conf.LogDir) == 0 { - conf.LogFile = filepath.Join(b.rootDataDir, conf.LogFile) - } else { - conf.LogFile = filepath.Join(conf.LogDir, conf.LogFile) - } - b.config = conf if inputNodeCfg != nil && inputNodeCfg.RuntimeLogLevel != "" { @@ -2859,3 +2853,29 @@ func (b *GethStatusBackend) TogglePanicReporting(enabled bool) error { } return b.DisablePanicReporting() } + +func (b *GethStatusBackend) SetLogLevel(level string) error { + b.mu.Lock() + defer b.mu.Unlock() + + err := nodecfg.SetLogLevel(b.appDB, level) + if err != nil { + return err + } + b.config.LogLevel = level + + return logutils.OverrideRootLoggerWithConfig(b.config.LogSettings()) +} + +func (b *GethStatusBackend) SetLogNamespaces(namespaces string) error { + b.mu.Lock() + defer b.mu.Unlock() + + err := nodecfg.SetLogNamespaces(b.appDB, namespaces) + if err != nil { + return err + } + b.config.LogNamespaces = namespaces + + return logutils.OverrideRootLoggerWithConfig(b.config.LogSettings()) +} diff --git a/logutils/core.go b/logutils/core.go index b6e153cbfcf..87916517b55 100644 --- a/logutils/core.go +++ b/logutils/core.go @@ -107,7 +107,8 @@ func (core *Core) Sync() error { } func (core *Core) UpdateSyncer(newSyncer zapcore.WriteSyncer) { - core.syncer.Store(writeSyncerWrapper{WriteSyncer: newSyncer}) + oldSyncer := core.syncer.Swap(writeSyncerWrapper{WriteSyncer: newSyncer}) + _ = oldSyncer.(zapcore.WriteSyncer).Sync() // may fail but doesn't impact syncer update } func (core *Core) UpdateEncoder(newEncoder zapcore.Encoder) { diff --git a/mobile/status.go b/mobile/status.go index 1587f435339..7f6c2f46481 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -1234,7 +1234,7 @@ func exportNodeLogs() string { if config == nil { return makeJSONResponse(errors.New("config and log file are not available")) } - data, err := json.Marshal(exportlogs.ExportFromBaseFile(config.LogFile)) + data, err := json.Marshal(exportlogs.ExportFromBaseFile(config.LogFilePath())) if err != nil { return makeJSONResponse(fmt.Errorf("error marshalling to json: %v", err)) } @@ -2306,6 +2306,44 @@ func addCentralizedMetric(requestJSON string) string { return metric.ID } +func SetLogLevel(requestJSON string) string { + return callWithResponse(setLogLevel, requestJSON) +} + +func setLogLevel(requestJSON string) string { + var request requests.SetLogLevel + err := json.Unmarshal([]byte(requestJSON), &request) + if err != nil { + return makeJSONResponse(err) + } + + err = request.Validate() + if err != nil { + return makeJSONResponse(err) + } + + return makeJSONResponse(statusBackend.SetLogLevel(request.LogLevel)) +} + +func SetLogNamespaces(requestJSON string) string { + return callWithResponse(setLogNamespaces, requestJSON) +} + +func setLogNamespaces(requestJSON string) string { + var request requests.SetLogNamespaces + err := json.Unmarshal([]byte(requestJSON), &request) + if err != nil { + return makeJSONResponse(err) + } + + err = request.Validate() + if err != nil { + return makeJSONResponse(err) + } + + return makeJSONResponse(statusBackend.SetLogNamespaces(request.LogNamespaces)) +} + func IntendedPanic(message string) string { type intendedPanic struct { error diff --git a/params/config.go b/params/config.go index ca3ee5bf972..1f019a81d9f 100644 --- a/params/config.go +++ b/params/config.go @@ -1196,12 +1196,19 @@ func LesTopic(netid int) string { } } +func (c *NodeConfig) LogFilePath() string { + if c.LogDir == "" { + return filepath.Join(c.RootDataDir, c.LogFile) + } + return filepath.Join(c.LogDir, c.LogFile) +} + func (c *NodeConfig) LogSettings() logutils.LogSettings { return logutils.LogSettings{ Enabled: c.LogEnabled, Level: c.LogLevel, Namespaces: c.LogNamespaces, - File: c.LogFile, + File: c.LogFilePath(), MaxSize: c.LogMaxSize, MaxBackups: c.LogMaxBackups, CompressRotated: c.LogCompressRotated, diff --git a/protocol/messenger_settings.go b/protocol/messenger_settings.go index bb3f7f350e0..8f3da3425aa 100644 --- a/protocol/messenger_settings.go +++ b/protocol/messenger_settings.go @@ -30,6 +30,7 @@ func (m *Messenger) SetSyncingOnMobileNetwork(request *requests.SetSyncingOnMobi return nil } +// Deprecated: Use SetLogLevel from status.go instead. func (m *Messenger) SetLogLevel(request *requests.SetLogLevel) error { if err := request.Validate(); err != nil { return err @@ -38,6 +39,7 @@ func (m *Messenger) SetLogLevel(request *requests.SetLogLevel) error { return nodecfg.SetLogLevel(m.database, request.LogLevel) } +// Deprecated: Use SetLogNamespaces from status.go instead. func (m *Messenger) SetLogNamespaces(request *requests.SetLogNamespaces) error { if err := request.Validate(); err != nil { return err diff --git a/tests-functional/tests/test_logging.py b/tests-functional/tests/test_logging.py index 98d9e7826f7..1964151c918 100644 --- a/tests-functional/tests/test_logging.py +++ b/tests-functional/tests/test_logging.py @@ -1,10 +1,11 @@ +from resources.constants import USER_DIR import re from test_cases import StatusBackend import pytest +import os @pytest.mark.rpc -@pytest.mark.skip("waiting for status-backend to be executed on the same host/container") class TestLogging: @pytest.mark.init @@ -20,46 +21,45 @@ def test_logging(self, tmp_path): assert backend_client is not None # Init and login - backend_client.init_status_backend(data_dir=str(tmp_path)) - backend_client.create_account_and_login(data_dir=str(tmp_path)) + backend_client.init_status_backend() + backend_client.create_account_and_login() key_uid = self.ensure_logged_in(backend_client) # Configure logging - backend_client.rpc_valid_request("wakuext_setLogLevel", [{"logLevel": "ERROR"}]) - backend_client.rpc_valid_request( - "wakuext_setLogNamespaces", - [{"logNamespaces": "test1.test2:debug,test1.test2.test3:info"}], - ) + backend_client.api_valid_request("SetLogLevel", {"logLevel": "ERROR"}) + backend_client.api_valid_request("SetLogNamespaces", {"logNamespaces": "test1.test2:debug,test1.test2.test3:info"}) - # Re-login (logging settings take effect after re-login) + log_pattern = [ + r"DEBUG\s+test1\.test2\s+", + r"INFO\s+test1\.test2\s+", + r"INFO\s+test1\.test2\.test3\s+", + r"WARN\s+test1\.test2\s+", + r"WARN\s+test1\.test2\.test3\s+", + r"ERROR\s+test1\s+", + r"ERROR\s+test1\.test2\s+", + r"ERROR\s+test1\.test2\.test3\s+", + ] + + # Ensure changes take effect at runtime + backend_client.rpc_valid_request("wakuext_logTest") + geth_log = backend_client.extract_data(os.path.join(USER_DIR, "geth.log")) + self.expect_logs(geth_log, "test message", log_pattern, count=1) + + # Ensure changes are persisted after re-login backend_client.logout() backend_client.login(str(key_uid)) self.ensure_logged_in(backend_client) - - # Test logging backend_client.rpc_valid_request("wakuext_logTest") - self.expect_logs( - tmp_path / "geth.log", - "test message", - [ - r"DEBUG\s+test1\.test2", - r"INFO\s+test1\.test2", - r"INFO\s+test1\.test2\.test3", - r"WARN\s+test1\.test2", - r"WARN\s+test1\.test2\.test3", - r"ERROR\s+test1", - r"ERROR\s+test1\.test2", - r"ERROR\s+test1\.test2\.test3", - ], - ) + geth_log = backend_client.extract_data(os.path.join(USER_DIR, "geth.log")) + self.expect_logs(geth_log, "test message", log_pattern, count=2) - def expect_logs(self, log_file, filter_keyword, expected_logs): + def expect_logs(self, log_file, filter_keyword, expected_logs, count): with open(log_file, "r") as f: log_content = f.read() filtered_logs = [line for line in log_content.splitlines() if filter_keyword in line] for expected_log in expected_logs: - assert any(re.search(expected_log, log) for log in filtered_logs), f"Log entry not found: {expected_log}" + assert sum(1 for log in filtered_logs if re.search(expected_log, log)) == count, f"Log entry not found or count mismatch: {expected_log}" def ensure_logged_in(self, backend_client): login_response = backend_client.wait_for_signal("node.login")