Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions mkdocs/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions pyiceberg/cli/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -55,6 +56,13 @@ def wrapper(*args: Any, **kwargs: Any): # type: ignore
@click.option("--catalog")
@click.option("--verbose", type=click.BOOL)
@click.option("--output", type=click.Choice(["text", "json"]), default="text")
@click.option(
"--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")
Expand All @@ -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="%(levelname)s:%(name)s:%(message)s",

Choose a reason for hiding this comment

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

Could you add a timestamp, line no to this?
format="%(asctime)s:%(levelname)s:%(name)s:%(lineno)d:%(message)s" something like this?
Or better yet structured logging - not sure if that requires a larger change?

)

properties = {}
if ugi:
properties["ugi"] = ugi
Expand Down
62 changes: 62 additions & 0 deletions tests/cli/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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