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