Repository: irods/irods
Pull request #8488: [#7229] Increase rodsLog verbosity (main)
Author: login: korydraughn
Created: 2025-05-08T13:23:31Z | Merged: 2025-05-09T15:50:30Z

Required by irods/irods_capability_indexing#172.


Diff:
diff --git a/lib/core/include/irods/irods_logger.hpp b/lib/core/include/irods/irods_logger.hpp
index 2883b10a95..8fbac7b0a5 100644
--- a/lib/core/include/irods/irods_logger.hpp
+++ b/lib/core/include/irods/irods_logger.hpp
@@ -725,7 +725,7 @@ namespace irods::experimental::log
                 object[tag::server::timestamp] = utc_timestamp();
                 object[tag::server::zone] = get_server_zone();
 
-                return object.dump();
+                return object.dump(-1, ' ', false, json::error_handler_t::replace);
             } // to_json_string
 
             template <
diff --git a/lib/core/include/irods/rodsLog.h b/lib/core/include/irods/rodsLog.h
index 296f034f5d..6d4a6c4131 100644
--- a/lib/core/include/irods/rodsLog.h
+++ b/lib/core/include/irods/rodsLog.h
@@ -60,6 +60,19 @@
 #ifdef __cplusplus
 extern "C" {
 #endif
+
+/* Converts the log level for the legacy log category in server_config.json
+ * into an appropriate log level (e.g. LOG_DEBUG) for the rodsLog API.
+ *
+ * This function is designed to initialize the verbosity level for the
+ * rodsLog API.
+ *
+ * \warning For server-side use only.
+ *
+ * \since 5.0.0
+ */
+int rodsLog_derive_verbosity_level_from_legacy_log_level();
+
 void rodsLog( int level, const char *formatStr, ... );
 void rodsLogAndErrorMsg( int level, rError_t *myError, int status,
                          const char *formatStr, ... );
diff --git a/lib/core/src/rodsLog.cpp b/lib/core/src/rodsLog.cpp
index 3b7581a1f9..8b51defdd2 100644
--- a/lib/core/src/rodsLog.cpp
+++ b/lib/core/src/rodsLog.cpp
@@ -27,6 +27,7 @@ const static std::map<const int, const std::string> irods_error_map = irods_erro
 #include <unistd.h>
 #include "irods/irods_exception.hpp"
 #include "irods/irods_logger.hpp"
+#include "irods/irods_configuration_keywords.hpp"
 
 #define BIG_STRING_LEN MAX_NAME_LEN+300
 #include <cstdarg>
@@ -69,6 +70,36 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    // clang-format off
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using enum log_ns::level;
+        case trace:     return LOG_SQL;
+        case debug:     return LOG_DEBUG7;
+        case info:      return LOG_NOTICE;
+        case warn:      return LOG_WARNING;
+        case error:     return LOG_ERROR;
+        case critical:  return LOG_SYS_FATAL;
+    }
+    // clang-format on
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    //
+    // Even though this code path is effectively unreachable, we've decided to return
+    // LOG_SQL just in case. By returning LOG_SQL, we guarantee that all messages are
+    // forwarded to the new logger. The only downside is that the new logger could
+    // choose to discard the message resulting in wasted effort by the rodsLog API.
+    return LOG_SQL;
+} // rodsLog_derive_verbosity_level_from_legacy_log_level
+
 void forward_to_syslog(int log_level, const std::string& msg)
 {
     if (CLIENT_PT == ::ProcessType) {
diff --git a/scripts/irods/test/test_rule_engine_plugin_passthrough.py b/scripts/irods/test/test_rule_engine_plugin_passthrough.py
index 9cc0eb5391..899b822aca 100644
--- a/scripts/irods/test/test_rule_engine_plugin_passthrough.py
+++ b/scripts/irods/test/test_rule_engine_plugin_passthrough.py
@@ -1,12 +1,8 @@
-from __future__ import print_function
+import json
+import mmap
 import os
 import sys
-import mmap
-
-if sys.version_info < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
+import unittest
 
 from . import session
 from .. import test
@@ -30,32 +26,32 @@ def tearDown(self):
     def test_repf_continuation_using_passthrough_rep__issues_4147_4148_4179(self):
         config = IrodsConfig()
 
-        with lib.file_backed_up(config.server_config_path):
-            pep_name = 'pep_database_open_pre'
-            RULE_ENGINE_CONTINUE = 5000000
+        try:
+            with lib.file_backed_up(config.server_config_path):
+                pep_name = 'pep_database_open_pre'
+                RULE_ENGINE_CONTINUE = 5000000
 
-            # Enable the Passthrough REP (make it the first REP in the list).
-            # Configure the Passthrough REP to return 'RULE_ENGINE_CONTINUE' to the REPF.
-            # Set the log level to 'trace' for the rule engine and legacy logger categories.
-            config.server_config['log_level']['rule_engine'] = 'trace'
-            config.server_config['log_level']['legacy'] = 'trace'
-            config.server_config['plugin_configuration']['rule_engines'].insert(0, {
-                'instance_name': 'irods_rule_engine_plugin-passthrough-instance',
-                'plugin_name': 'irods_rule_engine_plugin-passthrough',
-                'plugin_specific_configuration': {
-                    'return_codes_for_peps': [
-                        {
-                            'regex': '^' + pep_name + '$',
-                            'code': RULE_ENGINE_CONTINUE
-                        }
-                    ]
-                }
-            })
-            lib.update_json_file_from_dict(config.server_config_path, config.server_config)
+                # Enable the Passthrough REP (make it the first REP in the list).
+                # Configure the Passthrough REP to return 'RULE_ENGINE_CONTINUE' to the REPF.
+                # Set the log level to 'trace' for the rule engine and legacy logger categories.
+                config.server_config['log_level']['rule_engine'] = 'trace'
+                config.server_config['log_level']['legacy'] = 'trace'
+                config.server_config['plugin_configuration']['rule_engines'].insert(0, {
+                    'instance_name': 'irods_rule_engine_plugin-passthrough-instance',
+                    'plugin_name': 'irods_rule_engine_plugin-passthrough',
+                    'plugin_specific_configuration': {
+                        'return_codes_for_peps': [
+                            {
+                                'regex': '^' + pep_name + '$',
+                                'code': RULE_ENGINE_CONTINUE
+                            }
+                        ]
+                    }
+                })
+                lib.update_json_file_from_dict(config.server_config_path, config.server_config)
 
-            core_re_path = os.path.join(config.core_re_directory, 'core.re')
+                core_re_path = os.path.join(config.core_re_directory, 'core.re')
 
-            try:
                 with lib.file_backed_up(core_re_path):
                     second_msg = 'This should appear after the first message!'
 
@@ -77,5 +73,5 @@ def test_repf_continuation_using_passthrough_rep__issues_4147_4148_4179(self):
                         self.assertTrue(mm.find(second_msg.encode(), index) != -1)
                         mm.close()
 
-            finally:
-                IrodsController(config).reload_configuration()
+        finally:
+            IrodsController(config).reload_configuration()
diff --git a/server/main_server/src/agent_main.cpp b/server/main_server/src/agent_main.cpp
index ff50d6420d..f90bbed6e4 100644
--- a/server/main_server/src/agent_main.cpp
+++ b/server/main_server/src/agent_main.cpp
@@ -230,7 +230,7 @@ auto main(int _argc, char* _argv[]) -> int
 
         // Configure the legacy rodsLog API so messages are written to the legacy log category
         // provided by the new logging API.
-        rodsLogLevel(LOG_NOTICE);
+        rodsLogLevel(rodsLog_derive_verbosity_level_from_legacy_log_level());
         rodsLogSqlReq(0);
 
         init_logger(getppid(), write_to_stdout, enable_test_mode);
diff --git a/server/main_server/src/main.cpp b/server/main_server/src/main.cpp
index 5f938d939b..c72d85a4e5 100644
--- a/server/main_server/src/main.cpp
+++ b/server/main_server/src/main.cpp
@@ -23,6 +23,7 @@
 #include "irods/rcMisc.h"
 #include "irods/rodsClient.h"
 #include "irods/rodsErrorTable.h"
+#include "irods/rodsLog.h"
 #include "irods/update_replica_access_time.h"
 #include "irods/set_delay_server_migration_info.h"
 
@@ -244,7 +245,7 @@ auto main(int _argc, char* _argv[]) -> int
 
         // Configure the legacy rodsLog API so messages are written to the legacy log category
         // provided by the new logging API.
-        rodsLogLevel(LOG_NOTICE);
+        rodsLogLevel(rodsLog_derive_verbosity_level_from_legacy_log_level());
         rodsLogSqlReq(0);
 
         init_logger(getpid(), write_to_stdout, enable_test_mode);


Reviews:
user: login: alanking
body: Seems fine to me. I don't quite understand the changes so I created https://github.com/irods/irods/issues/8489 to update the docs since I think we forgot to do that.

Will review companion PR and come back to this one.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:44:41Z
user: login: alanking
body: If it works as expected, # it
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:54:51Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:21:49Z
user: login: alanking
body: One little suggestion, but otherwise looks good.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:22:08Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:29:56Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:38:10Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:44:05Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:15:10Z
user: login: alanking
body: Looks good. Squash em
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:16:41Z
user: login: alanking
body: Good. Please squash to taste and apply issue numbers, no #s.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:26:46Z
user: login: alanking
body: \# em
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:41:04Z

Review comments:
diff_hunk: @@ -69,6 +70,30 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using log_ns::level;
+
+        case level::trace:      return LOG_SQL;
+        case level::debug:      return LOG_DEBUG7;
+        case level::info:       return LOG_NOTICE;
+        case level::warn:       return LOG_WARNING;
+        case level::error:      return LOG_ERROR;
+        case level::critical:   return LOG_SYS_FATAL;
+    }
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    return LOG_SQL;
path: lib/core/src/rodsLog.cpp
user: login: alanking
body: Would it make more sense to return `LOG_NOTICE` here, based on the comment?
created_at: 2025-05-08T18:21:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,30 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using log_ns::level;
+
+        case level::trace:      return LOG_SQL;
+        case level::debug:      return LOG_DEBUG7;
+        case level::info:       return LOG_NOTICE;
+        case level::warn:       return LOG_WARNING;
+        case level::error:      return LOG_ERROR;
+        case level::critical:   return LOG_SYS_FATAL;
+    }
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    return LOG_SQL;
path: lib/core/src/rodsLog.cpp
user: login: korydraughn
body: Returning `LOG_NOTICE` can result in messages being discarded. By defaulting to `LOG_SQL`, we guarantee that all messages are forwarded to the new logger. The only downside is that the new logger could also choose to discard the message resulting in wasted effort by the rodsLog API.

This can be viewed as - "hey rodsLog, let the new logger see that message".

This also aligns with the original solution that was approved.
created_at: 2025-05-08T18:29:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,30 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using log_ns::level;
+
+        case level::trace:      return LOG_SQL;
+        case level::debug:      return LOG_DEBUG7;
+        case level::info:       return LOG_NOTICE;
+        case level::warn:       return LOG_WARNING;
+        case level::error:      return LOG_ERROR;
+        case level::critical:   return LOG_SYS_FATAL;
+    }
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    return LOG_SQL;
path: lib/core/src/rodsLog.cpp
user: login: alanking
body: Okay. The comment kind of suggested that the default would result in `LOG_NOTICE`, which is why I said that. Perhaps we can add to the comment to explain why `LOG_SQL` is returned instead? What you've written here is good enough explanation, IMO.
created_at: 2025-05-08T18:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,30 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using log_ns::level;
+
+        case level::trace:      return LOG_SQL;
+        case level::debug:      return LOG_DEBUG7;
+        case level::info:       return LOG_NOTICE;
+        case level::warn:       return LOG_WARNING;
+        case level::error:      return LOG_ERROR;
+        case level::critical:   return LOG_SYS_FATAL;
+    }
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    return LOG_SQL;
path: lib/core/src/rodsLog.cpp
user: login: korydraughn
body: Will do.
created_at: 2025-05-08T18:44:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,30 @@ std::string create_log_error_prefix() {
     return ret;
 }
 
+int rodsLog_derive_verbosity_level_from_legacy_log_level()
+{
+    if (CLIENT_PT == ::ProcessType) {
+        return verbosityLevel;
+    }
+
+    namespace log_ns = irods::experimental::log;
+
+    switch (log_ns::get_level_from_config(irods::KW_CFG_LOG_LEVEL_CATEGORY_LEGACY)) {
+        using log_ns::level;
+
+        case level::trace:      return LOG_SQL;
+        case level::debug:      return LOG_DEBUG7;
+        case level::info:       return LOG_NOTICE;
+        case level::warn:       return LOG_WARNING;
+        case level::error:      return LOG_ERROR;
+        case level::critical:   return LOG_SYS_FATAL;
+    }
+
+    // We should never reach this line because get_level_from_config() always returns
+    // a valid log level (i.e. it catches all exceptions and defaults to level::info).
+    return LOG_SQL;
path: lib/core/src/rodsLog.cpp
user: login: korydraughn
body: Done.
created_at: 2025-05-08T19:15:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: korydraughn
created_at: 2025-05-08T14:09:29Z
body: I need to tweak that commit message a little bit. Don't like how I've worded that.

> I don't quite understand the changes

I'll try to explain... The rodsLog function performs a check of the incoming log level and the verbosity level _(set by `rodsLogLevel`)_. See the following:

https://github.com/irods/irods/blob/c7386a589df464767dd221528cdbd3bc1f68a79c/lib/core/src/rodsLog.cpp#L117-L121

That condition can be improved to help with understanding.

```cpp
if (_level > verbosityLevel && _level != LOG_SQL) {
    return;
}
```

By raising the verbosity level, we allow all messages to be forwarded to the new logger. This is necessary because iRODS 5 does not allow the rodsLog verbosity level to be modified at runtime through the irods_environment.json file or the environment variable (i.e. `spLogLevel`). The test failure for the indexing plugin was due to the iRODS 5 server discarding `LOG_DEBUG` messages.
author_association: CONTRIBUTOR
user: login: alanking
created_at: 2025-05-08T14:20:34Z
body: Ah, got it. Thanks for the explanation. Maybe that will help future mes and yous too :p
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T18:13:16Z
body: Confirmed the new changes still allow the indexing test to pass.

Going to run the unit and core tests to make sure this PR doesn't break anything.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T18:14:44Z
body: Will handle clang-format after testing.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T18:35:01Z
body: Forgot to force-push so that the latest commit message is visible. No code changes introduced by the force-push.

Unit tests passed.
```
-----
All tests passed! :)
time elapsed: [ 175.4627]seconds ([   0]hours [ 2.9244]minutes)
==== end of test run results ====

2025-05-08 14:21:58,707 - version:[4.90.0]
2025-05-08 14:21:58,707 - sha:[2812b6a450f39799202897e33f428b12657ab482]
```

Running core tests.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T19:23:20Z
body: Waiting for core tests to finish/pass. Will squash as soon as the tests report success.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T19:35:55Z
body: Made one change to using C++20 `using enum log_ns::level` to reduce some noise. Feature was introduced in Clang 13 so we should be good.

I'll confirm that doesn't break anything by doing some manual testing. Shouldn't need to rerun unit or core tests.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-08T21:03:59Z
body: Investigating two test failures:
- `test_rule_engine_plugin_passthrough`
- `test_rule_engine_plugin_framework.Test_Rule_Engine_Plugin_Framework`
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-09T13:43:29Z
body: Fixed the failing tests.

Rerunning core tests to make sure the other tests are still ok.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-09T15:23:25Z
body: All core tests passed.
```
-----
All tests passed! :)
time elapsed: [5721.4147]seconds ([   1]hours [35.3569]minutes)
==== end of test run results ====

2025-05-09 11:10:36,873 - version:[4.90.0]
2025-05-09 11:10:36,873 - sha:[2176c0949245f1bd9ef1b589418de862f1b6d2f2]
```

All unit tests passed.
```
-----
All tests passed! :)
time elapsed: [ 174.1432]seconds ([   0]hours [ 2.9024]minutes)
==== end of test run results ====

2025-05-09 11:22:45,934 - version:[4.90.0]
2025-05-09 11:22:45,935 - sha:[2176c0949245f1bd9ef1b589418de862f1b6d2f2]
```
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-09T15:32:58Z
body: Squashed.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-05-09T15:48:34Z
body: Added pounds.
author_association: CONTRIBUTOR