diff --git a/api/geth_backend.go b/api/geth_backend.go index 3528a8dbc70..653b332457b 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -2198,12 +2198,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 != "" { @@ -2984,3 +2978,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.DefaultLogSettings()) +} + +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.DefaultLogSettings()) +} 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 1d22598f8a6..4e3c8b1935d 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -1248,7 +1248,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)) } @@ -2323,6 +2323,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 3bbf8094060..6d43e27f9b6 100644 --- a/params/config.go +++ b/params/config.go @@ -1048,12 +1048,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) DefaultLogSettings() 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 9cc534d42a5..c6c4e7c1ed8 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/clients/status_backend.py b/tests-functional/clients/status_backend.py index 8988007b337..6642f96d1f0 100644 --- a/tests-functional/clients/status_backend.py +++ b/tests-functional/clients/status_backend.py @@ -335,6 +335,9 @@ def container_exec(self, command): def find_public_key(self): self.public_key = self.node_login_event.get("event", {}).get("settings", {}).get("public-key") + def find_key_uid(self): + return self.node_login_event.get("event", {}).get("account", {}).get("key-uid") + @retry(stop=stop_after_delay(10), wait=wait_fixed(0.1), reraise=True) def change_container_ip(self, new_ipv4=None, new_ipv6=None): if not self.container: diff --git a/tests-functional/tests/test_logging.py b/tests-functional/tests/test_logging.py index 6ed4fc0af2e..b09c7c15c6a 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 clients.status_backend 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,50 +21,41 @@ 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)) - key_uid = self.ensure_logged_in(backend_client) + backend_client.init_status_backend() + backend_client.create_account_and_login() # 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"}) + + 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+", + ] - # Re-login (logging settings take effect after re-login) - backend_client.logout() - backend_client.login(str(key_uid)) - self.ensure_logged_in(backend_client) + # 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) - # Test logging + # Ensure changes are persisted after re-login + backend_client.logout() + backend_client.login(str(backend_client.find_key_uid())) + backend_client.wait_for_login() 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}" - - def ensure_logged_in(self, backend_client): - login_response = backend_client.wait_for_signal("node.login") - backend_client.verify_json_schema(login_response, "signal_node_login") - key_uid = login_response.get("event", {}).get("account", {}).get("key-uid") - assert key_uid is not None, "key-uid not found in login response" - return key_uid + 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}"