Skip to content

Commit 4505f0c

Browse files
committed
add log level to CLI
1 parent b0a7878 commit 4505f0c

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
# pylint: disable=broad-except,redefined-builtin,redefined-outer-name
18+
import logging
19+
import os
1820
from collections.abc import Callable
1921
from functools import wraps
2022
from typing import (
@@ -33,6 +35,8 @@
3335
from pyiceberg.table.refs import SnapshotRef, SnapshotRefType
3436
from pyiceberg.utils.properties import property_as_int
3537

38+
DEFAULT_LOG_LEVEL = "WARNING"
39+
3640

3741
def catch_exception() -> Callable: # type: ignore
3842
def decorator(func: Callable) -> Callable: # type: ignore
@@ -55,6 +59,11 @@ def wrapper(*args: Any, **kwargs: Any): # type: ignore
5559
@click.option("--catalog")
5660
@click.option("--verbose", type=click.BOOL)
5761
@click.option("--output", type=click.Choice(["text", "json"]), default="text")
62+
@click.option(
63+
"--log-level",
64+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False),
65+
help="Set the logging level (also configurable via PYICEBERG_LOG_LEVEL environment variable)",
66+
)
5867
@click.option("--ugi")
5968
@click.option("--uri")
6069
@click.option("--credential")
@@ -64,10 +73,18 @@ def run(
6473
catalog: str | None,
6574
verbose: bool,
6675
output: str,
76+
log_level: str | None,
6777
ugi: str | None,
6878
uri: str | None,
6979
credential: str | None,
7080
) -> None:
81+
# Configure logging level from CLI option or environment variable
82+
level = log_level or os.getenv("PYICEBERG_LOG_LEVEL") or DEFAULT_LOG_LEVEL
83+
logging.basicConfig(
84+
level=getattr(logging, level.upper()),
85+
format="%(levelname)s:%(name)s:%(message)s",
86+
)
87+
7188
properties = {}
7289
if ugi:
7390
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)