Skip to content

Commit 4547e91

Browse files
authored
cli: add log level param (apache#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 apache#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. -->
1 parent 7b84d10 commit 4547e91

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

mkdocs/docs/contributing.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,27 @@ Which will warn:
258258
Deprecated in 0.1.0, will be removed in 0.2.0. The old_property is deprecated. Please use the something_else property instead.
259259
```
260260

261+
### Logging
262+
263+
PyIceberg uses Python's standard logging module. You can control the logging level using either:
264+
265+
**CLI option:**
266+
267+
```bash
268+
pyiceberg --log-level DEBUG describe my_table
269+
```
270+
271+
**Environment variable:**
272+
273+
```bash
274+
export PYICEBERG_LOG_LEVEL=DEBUG
275+
pyiceberg describe my_table
276+
```
277+
278+
Valid log levels are: `DEBUG`, `INFO`, `WARNING` (default), `ERROR`, `CRITICAL`.
279+
280+
Debug logging is particularly useful for troubleshooting issues with FileIO implementations, catalog connections, and other integration points.
281+
261282
### Type annotations
262283

263284
For the type annotation the types from the `Typing` package are used.

pyiceberg/cli/console.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
# pylint: disable=broad-except,redefined-builtin,redefined-outer-name
18+
import logging
1819
from collections.abc import Callable
1920
from functools import wraps
2021
from typing import (
@@ -55,6 +56,13 @@ def wrapper(*args: Any, **kwargs: Any): # type: ignore
5556
@click.option("--catalog")
5657
@click.option("--verbose", type=click.BOOL)
5758
@click.option("--output", type=click.Choice(["text", "json"]), default="text")
59+
@click.option(
60+
"--log-level",
61+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False),
62+
default="WARNING",
63+
envvar="PYICEBERG_LOG_LEVEL",
64+
help="Set the logging level",
65+
)
5866
@click.option("--ugi")
5967
@click.option("--uri")
6068
@click.option("--credential")
@@ -64,10 +72,16 @@ def run(
6472
catalog: str | None,
6573
verbose: bool,
6674
output: str,
75+
log_level: str,
6776
ugi: str | None,
6877
uri: str | None,
6978
credential: str | None,
7079
) -> None:
80+
logging.basicConfig(
81+
level=getattr(logging, log_level.upper()),
82+
format="%(asctime)s:%(levelname)s:%(name)s:%(message)s",
83+
)
84+
7185
properties = {}
7286
if ugi:
7387
properties["ugi"] = ugi

tests/cli/test_console.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,3 +967,65 @@ def test_json_properties_remove_table_does_not_exist(catalog: InMemoryCatalog) -
967967
result = runner.invoke(run, ["--output=json", "properties", "remove", "table", "default.doesnotexist", "location"])
968968
assert result.exit_code == 1
969969
assert result.output == """{"type": "NoSuchTableError", "message": "Table does not exist: default.doesnotexist"}\n"""
970+
971+
972+
def test_log_level_cli_option(mocker: MockFixture) -> None:
973+
mock_basicConfig = mocker.patch("logging.basicConfig")
974+
975+
runner = CliRunner()
976+
runner.invoke(run, ["--log-level", "DEBUG", "list"])
977+
978+
# Verify logging.basicConfig was called with DEBUG level
979+
import logging
980+
981+
mock_basicConfig.assert_called_once()
982+
call_kwargs = mock_basicConfig.call_args[1]
983+
assert call_kwargs["level"] == logging.DEBUG
984+
985+
986+
def test_log_level_env_variable(mocker: MockFixture) -> None:
987+
mock_basicConfig = mocker.patch("logging.basicConfig")
988+
mocker.patch.dict(os.environ, {"PYICEBERG_LOG_LEVEL": "INFO"})
989+
990+
runner = CliRunner()
991+
runner.invoke(run, ["list"])
992+
993+
# Verify logging.basicConfig was called with INFO level
994+
import logging
995+
996+
mock_basicConfig.assert_called_once()
997+
call_kwargs = mock_basicConfig.call_args[1]
998+
assert call_kwargs["level"] == logging.INFO
999+
1000+
1001+
def test_log_level_default_warning(mocker: MockFixture) -> None:
1002+
mock_basicConfig = mocker.patch("logging.basicConfig")
1003+
# Ensure PYICEBERG_LOG_LEVEL is not set
1004+
mocker.patch.dict(os.environ, {}, clear=False)
1005+
if "PYICEBERG_LOG_LEVEL" in os.environ:
1006+
del os.environ["PYICEBERG_LOG_LEVEL"]
1007+
1008+
runner = CliRunner()
1009+
runner.invoke(run, ["list"])
1010+
1011+
# Verify logging.basicConfig was called with WARNING level (default)
1012+
import logging
1013+
1014+
mock_basicConfig.assert_called_once()
1015+
call_kwargs = mock_basicConfig.call_args[1]
1016+
assert call_kwargs["level"] == logging.WARNING
1017+
1018+
1019+
def test_log_level_cli_overrides_env(mocker: MockFixture) -> None:
1020+
mock_basicConfig = mocker.patch("logging.basicConfig")
1021+
mocker.patch.dict(os.environ, {"PYICEBERG_LOG_LEVEL": "INFO"})
1022+
1023+
runner = CliRunner()
1024+
runner.invoke(run, ["--log-level", "ERROR", "list"])
1025+
1026+
# Verify CLI option overrides environment variable
1027+
import logging
1028+
1029+
mock_basicConfig.assert_called_once()
1030+
call_kwargs = mock_basicConfig.call_args[1]
1031+
assert call_kwargs["level"] == logging.ERROR

0 commit comments

Comments
 (0)