Skip to content

RSDK-10329: Apply log_configuration level on reconfigure for modular resources#1173

Open
claude[bot] wants to merge 1 commit intomainfrom
claude/RSDK-10329-fix-debug-logs-modular-resources
Open

RSDK-10329: Apply log_configuration level on reconfigure for modular resources#1173
claude[bot] wants to merge 1 commit intomainfrom
claude/RSDK-10329-fix-debug-logs-modular-resources

Conversation

@claude
Copy link
Contributor

@claude claude bot commented Mar 19, 2026

Summary

  • Fix reconfigure_resource() in Module to apply log_configuration.level when reconfiguring a Reconfigurable resource
  • Previously, clicking "Enable debug logs" on a modular resource in the UI added "log_configuration": {"level": "debug"} to the resource config, but the log level was never applied during reconfigure — only during initial add_resource
  • Add tests verifying log level is applied on both add_resource and reconfigure_resource

DRI

@allisonschiang is the responsible engineer for this PR.

Test plan

  • test_add_resource_applies_log_level — verifies log level is set when adding a resource with log_configuration
  • test_reconfigure_resource_applies_log_level — verifies log level is updated when reconfiguring a Reconfigurable resource with log_configuration
  • All existing tests continue to pass

🤖 Generated with Claude Code

…resources

When a Reconfigurable resource was reconfigured, the log_configuration.level
from the new config was not applied to the resource's logger. Only the
non-Reconfigurable path (which re-adds the resource) applied the log level.

This adds update_log_level() after reconfigure() in the Reconfigurable branch
of reconfigure_resource(), so that clicking "Enable debug logs" in the UI
actually takes effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude claude bot requested a review from a team as a code owner March 19, 2026 02:47
@claude claude bot requested review from lia-viam and stuqdog March 19, 2026 02:47
Copy link
Contributor Author

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary

The fix is correct and minimal. The Reconfigurable branch of reconfigure_resource() was the only path missing a call to update_log_level. The non-Reconfigurable branch already worked because it delegates to add_resource, which already had the call. The added line exactly mirrors the existing call in add_resource and is in the right place (after reconfigure() so the new config is applied before the log level is updated).

update_log_level gracefully handles the default case — when log_configuration is absent, config.log_configuration.level returns "", "".upper() stays "", and update_log_level falls back to the global LOG_LEVEL. Behavior is therefore consistent with add_resource.

gen/ was not modified.


Observations (not blocking)

tests/test_module.py:172assert gizmo.logger.level != logging.DEBUG

This is a weak negative assertion. It passes if the level is WARNING, ERROR, NOTSET, or any value other than 10. A positive assertion like assert gizmo.logger.level == logging.INFO would more precisely document the expected initial state and catch any future regression where the default silently shifts to a different level.

tests/test_module.py:158–188 — Missing edge case: debug → (no log_configuration)

There is no test verifying that reconfiguring a resource from level="debug" to a config with no log_configuration resets the level back to INFO. The update_log_level empty-string path handles this correctly in the implementation, but it is not exercised. Adding this case would complete the coverage of the three transitions: none→debug, debug→debug, and debug→none.

@allisonschiang
Copy link
Member

@claude Why does reconfiguring a resource with removing log_configuration set it back to info? how are debug logs handled differently than other logs?

@claude
Copy link
Contributor Author

claude bot commented Mar 19, 2026

Claude finished @allisonschiang's task in 1m 6s —— View job


  • Read relevant files to understand log level handling
  • Answer the question

Why removing log_configuration resets the log level to INFO

The answer is in update_log_level in src/viam/logging.py:156–161:

def update_log_level(logger: logging.Logger, level: Union[int, str]):
    if level == "":
        level = LOG_LEVEL          # ← falls back to the process-wide default
    logger.setLevel(level)
    for handler in logger.handlers:
        handler.setLevel(level)

And the call site in reconfigure_resource (line 244):

update_log_level(resource.logger, config.log_configuration.level.upper())

The chain when log_configuration is absent:

  1. config.log_configuration accesses a protobuf sub-message that was not set in the config.
  2. Protobuf returns a default LogConfiguration with level = "" (the zero value for a string field).
  3. "".upper() → still "".
  4. Inside update_log_level, level == "" is True, so level is replaced with the module-level global LOG_LEVEL.
  5. LOG_LEVEL is defined at logging.py:18 as INFO, and is only changed by setLevel() — e.g. when the module is started with --log-level=debug.
  6. The logger and all its handlers are set to INFO, suppressing debug messages.

So "resets to INFO" is more precisely "resets to the process-wide default log level" — it's INFO unless the module was launched with --log-level=debug, in which case the fallback would be DEBUG.


How debug logs are handled differently

Python's logging system requires a record to pass two level filters before it is emitted: the logger's own level, and each handler's level. update_log_level keeps them in sync by setting both. Here is what happens end-to-end:

Scenario config.log_configuration.level level passed to update_log_level Logger + handler level set to DEBUG messages emitted?
UI sets "Enable debug logs" "debug" "DEBUG" DEBUG (10) ✅ Yes
log_configuration removed "" (proto default) "" LOG_LEVEL = INFO (20) ❌ No — filtered at both logger and handler
Never set "" "" LOG_LEVEL = INFO ❌ No

Because the logger level is the first gate, a logger.debug("…") call from a resource's code is checked against logger.level. If logger.level == INFO (20) and DEBUG == 10, the record is dropped immediately and never reaches the handlers. That's the core reason debug logs disappear when log_configuration is removed: update_log_level raises the threshold from 10 back to 20.

@allisonschiang
Copy link
Member

@claude makes sense, can you add this edge case tests/test_module.py:158–188 — Missing edge case: debug → (no log_configuration)

@claude
Copy link
Contributor Author

claude bot commented Mar 19, 2026

Claude finished @allisonschiang's task in 1m 35s —— View job


  • Read tests/test_module.py to understand existing test patterns
  • Add edge case test: debug → (no log_configuration) resets log level to INFO
  • Push to branch (blocked — see note below)

Change ready but push blocked

I've written and committed the new test locally, but the push was denied (403 — Permission to viamrobotics/viam-python-sdk.git denied to github-actions[bot]). You'll need to apply the diff manually to tests/test_module.py after test_reconfigure_resource_applies_log_level:

async def test_reconfigure_resource_resets_log_level_to_default(self, module: Module):
    # Add a resource with debug log level
    req = AddResourceRequest(
        config=ComponentConfig(
            name="gizmo_reset_log",
            namespace="acme",
            type="gizmo",
            model="acme:demo:mygizmo",
            attributes=dict_to_struct({"arg1": "arg1", "motor": "motor1"}),
            api="acme:component:gizmo",
            log_configuration=LogConfiguration(level="debug"),
        )
    )
    await module.add_resource(req)
    gizmo = module.server.get_resource(MyGizmo, Gizmo.get_resource_name("gizmo_reset_log"))
    assert gizmo.logger.level == logging.DEBUG

    # Reconfigure without log_configuration — should reset to INFO
    req = ReconfigureResourceRequest(
        config=ComponentConfig(
            name="gizmo_reset_log",
            namespace="acme",
            type="gizmo",
            model="acme:demo:mygizmo",
            attributes=dict_to_struct({"arg1": "arg2", "motor": "motor1"}),
            api="acme:component:gizmo",
        )
    )
    await module.reconfigure_resource(req)
    assert gizmo.my_arg == "arg2"
    assert gizmo.logger.level == logging.INFO

What the test does:

  1. Adds a resource with log_configuration.level = "debug" — asserts logger is at DEBUG.
  2. Reconfigures with no log_configuration — asserts logger resets to logging.INFO (the LOG_LEVEL fallback in update_log_level).

@allisonschiang
Copy link
Member

@claude try pushing your changes again

@claude
Copy link
Contributor Author

claude bot commented Mar 19, 2026

Claude finished @allisonschiang's task in 58s —— View job


  • Read current state of tests/test_module.py
  • Add edge case test (test_reconfigure_resource_resets_log_level_to_default)
  • Push to remote — blocked (403 — github-actions[bot] lacks write access to this repository)

Push still blocked

The commit was created successfully locally, but the push is denied with the same 403 as before:

remote: Permission to viamrobotics/viam-python-sdk.git denied to github-actions[bot].

This is a repository permission issue — the GitHub Actions bot token used to run this job doesn't have write access to viamrobotics/viam-python-sdk. This cannot be resolved by retrying.

To apply the change, please add the following test to tests/test_module.py after test_reconfigure_resource_applies_log_level (after line 188):

async def test_reconfigure_resource_resets_log_level_to_default(self, module: Module):
    # Add a resource with debug log level
    req = AddResourceRequest(
        config=ComponentConfig(
            name="gizmo_reset_log",
            namespace="acme",
            type="gizmo",
            model="acme:demo:mygizmo",
            attributes=dict_to_struct({"arg1": "arg1", "motor": "motor1"}),
            api="acme:component:gizmo",
            log_configuration=LogConfiguration(level="debug"),
        )
    )
    await module.add_resource(req)
    gizmo = module.server.get_resource(MyGizmo, Gizmo.get_resource_name("gizmo_reset_log"))
    assert gizmo.logger.level == logging.DEBUG

    # Reconfigure without log_configuration — should reset to INFO
    req = ReconfigureResourceRequest(
        config=ComponentConfig(
            name="gizmo_reset_log",
            namespace="acme",
            type="gizmo",
            model="acme:demo:mygizmo",
            attributes=dict_to_struct({"arg1": "arg2", "motor": "motor1"}),
            api="acme:component:gizmo",
        )
    )
    await module.reconfigure_resource(req)
    assert gizmo.my_arg == "arg2"
    assert gizmo.logger.level == logging.INFO

You may also want to check the FAQ for information on granting the bot write access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant