This is an automated email from the ASF dual-hosted git repository.

fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 4547e910 cli: add log level param (#2868)
4547e910 is described below

commit 4547e910938d2574567ba4a3af0759bc46cd5dcc
Author: Kevin Liu <[email protected]>
AuthorDate: Tue Jan 6 03:52:09 2026 -0500

    cli: add log level param (#2868)
    
    <!--
    Thanks for opening a pull request!
    -->
    
    <!-- In the case this PR will resolve an issue, please replace
    ${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
    <!-- Closes #${GITHUB_ISSUE_ID} -->
    
    # Rationale for this change
    After #2867, I realized that there's no way to set log level for the
    CLI.
    This PR introduces 2 ways to set log levels, `--log-level` and
    `PYICEBERG_LOG_LEVEL`. Default log level is `WARNING`
    
    ## Are these changes tested?
    Yes
    
    ## Are there any user-facing changes?
    Yes
    
    <!-- In the case of user-facing changes, please add the changelog label.
    -->
---
 mkdocs/docs/contributing.md | 21 +++++++++++++++
 pyiceberg/cli/console.py    | 14 ++++++++++
 tests/cli/test_console.py   | 62 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+)

diff --git a/mkdocs/docs/contributing.md b/mkdocs/docs/contributing.md
index aaecab2c..4f135709 100644
--- a/mkdocs/docs/contributing.md
+++ b/mkdocs/docs/contributing.md
@@ -258,6 +258,27 @@ Which will warn:
 Deprecated in 0.1.0, will be removed in 0.2.0. The old_property is deprecated. 
Please use the something_else property instead.
 ```
 
+### Logging
+
+PyIceberg uses Python's standard logging module. You can control the logging 
level using either:
+
+**CLI option:**
+
+```bash
+pyiceberg --log-level DEBUG describe my_table
+```
+
+**Environment variable:**
+
+```bash
+export PYICEBERG_LOG_LEVEL=DEBUG
+pyiceberg describe my_table
+```
+
+Valid log levels are: `DEBUG`, `INFO`, `WARNING` (default), `ERROR`, 
`CRITICAL`.
+
+Debug logging is particularly useful for troubleshooting issues with FileIO 
implementations, catalog connections, and other integration points.
+
 ### Type annotations
 
 For the type annotation the types from the `Typing` package are used.
diff --git a/pyiceberg/cli/console.py b/pyiceberg/cli/console.py
index 9baa813e..6c14eea0 100644
--- a/pyiceberg/cli/console.py
+++ b/pyiceberg/cli/console.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 # pylint: disable=broad-except,redefined-builtin,redefined-outer-name
+import logging
 from collections.abc import Callable
 from functools import wraps
 from typing import (
@@ -55,6 +56,13 @@ def catch_exception() -> Callable:  # type: ignore
 @click.option("--catalog")
 @click.option("--verbose", type=click.BOOL)
 @click.option("--output", type=click.Choice(["text", "json"]), default="text")
[email protected](
+    "--log-level",
+    type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 
case_sensitive=False),
+    default="WARNING",
+    envvar="PYICEBERG_LOG_LEVEL",
+    help="Set the logging level",
+)
 @click.option("--ugi")
 @click.option("--uri")
 @click.option("--credential")
@@ -64,10 +72,16 @@ def run(
     catalog: str | None,
     verbose: bool,
     output: str,
+    log_level: str,
     ugi: str | None,
     uri: str | None,
     credential: str | None,
 ) -> None:
+    logging.basicConfig(
+        level=getattr(logging, log_level.upper()),
+        format="%(asctime)s:%(levelname)s:%(name)s:%(message)s",
+    )
+
     properties = {}
     if ugi:
         properties["ugi"] = ugi
diff --git a/tests/cli/test_console.py b/tests/cli/test_console.py
index a0e95522..a713975e 100644
--- a/tests/cli/test_console.py
+++ b/tests/cli/test_console.py
@@ -967,3 +967,65 @@ def 
test_json_properties_remove_table_does_not_exist(catalog: InMemoryCatalog) -
     result = runner.invoke(run, ["--output=json", "properties", "remove", 
"table", "default.doesnotexist", "location"])
     assert result.exit_code == 1
     assert result.output == """{"type": "NoSuchTableError", "message": "Table 
does not exist: default.doesnotexist"}\n"""
+
+
+def test_log_level_cli_option(mocker: MockFixture) -> None:
+    mock_basicConfig = mocker.patch("logging.basicConfig")
+
+    runner = CliRunner()
+    runner.invoke(run, ["--log-level", "DEBUG", "list"])
+
+    # Verify logging.basicConfig was called with DEBUG level
+    import logging
+
+    mock_basicConfig.assert_called_once()
+    call_kwargs = mock_basicConfig.call_args[1]
+    assert call_kwargs["level"] == logging.DEBUG
+
+
+def test_log_level_env_variable(mocker: MockFixture) -> None:
+    mock_basicConfig = mocker.patch("logging.basicConfig")
+    mocker.patch.dict(os.environ, {"PYICEBERG_LOG_LEVEL": "INFO"})
+
+    runner = CliRunner()
+    runner.invoke(run, ["list"])
+
+    # Verify logging.basicConfig was called with INFO level
+    import logging
+
+    mock_basicConfig.assert_called_once()
+    call_kwargs = mock_basicConfig.call_args[1]
+    assert call_kwargs["level"] == logging.INFO
+
+
+def test_log_level_default_warning(mocker: MockFixture) -> None:
+    mock_basicConfig = mocker.patch("logging.basicConfig")
+    # Ensure PYICEBERG_LOG_LEVEL is not set
+    mocker.patch.dict(os.environ, {}, clear=False)
+    if "PYICEBERG_LOG_LEVEL" in os.environ:
+        del os.environ["PYICEBERG_LOG_LEVEL"]
+
+    runner = CliRunner()
+    runner.invoke(run, ["list"])
+
+    # Verify logging.basicConfig was called with WARNING level (default)
+    import logging
+
+    mock_basicConfig.assert_called_once()
+    call_kwargs = mock_basicConfig.call_args[1]
+    assert call_kwargs["level"] == logging.WARNING
+
+
+def test_log_level_cli_overrides_env(mocker: MockFixture) -> None:
+    mock_basicConfig = mocker.patch("logging.basicConfig")
+    mocker.patch.dict(os.environ, {"PYICEBERG_LOG_LEVEL": "INFO"})
+
+    runner = CliRunner()
+    runner.invoke(run, ["--log-level", "ERROR", "list"])
+
+    # Verify CLI option overrides environment variable
+    import logging
+
+    mock_basicConfig.assert_called_once()
+    call_kwargs = mock_basicConfig.call_args[1]
+    assert call_kwargs["level"] == logging.ERROR

Reply via email to