Skip to content

Commit 49cfde5

Browse files
parasebadcherian
authored andcommitted
Support changing log levels and setting them in Python (#982)
This introduces a new function `icechunk.set_logs_filter(str)` that allows setting and changing the logs Icechunk will produce. On module import, we still initialize logs to the value of ICECHUNK_LOG environment variable (or error level if not present).
1 parent 7955f1a commit 49cfde5

File tree

6 files changed

+158
-13
lines changed

6 files changed

+158
-13
lines changed

docs/docs/icechunk-python/faq.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,12 @@ Icechunk is different from normal Zarr stores because it is stateful. In a distr
77
**Does `icechunk-python` include logging?**
88

99
Yes! Set the environment variable `ICECHUNK_LOG=icechunk=debug` to print debug logs to stdout. Available "levels" in order of increasing verbosity are `error`, `warn`, `info`, `debug`, `trace`. The default level is `error`. The Rust library uses `tracing-subscriber` crate. The `ICECHUNK_LOG` variable can be used to filter logging following that crate's [documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). For example, `ICECHUNK_LOG=trace` will set both icechunk and it's dependencies' log levels to `trace` while `ICECHUNK_LOG=icechunk=trace` will enable the `trace` level for icechunk only. For more complex control `ICECHUNK_LOG=debug,icechunk=trace,rustls=info,h2=info,hyper=info` will set `trace` for `icechunk`, `info` for `rustls`,`hyper`, and `h2` crates, and `debug` for every other crate.
10+
11+
You can also use Python's `os.environ` to set or change the value of the variable. If you change the environment variable after `icechunk` was
12+
imported, you will need to call `icechunk.set_logs_filter(None)` for changes to take effect.
13+
14+
This function also accepts the filter directive. If you prefer not to use environment variables, you can do:
15+
16+
```python
17+
icechunk.set_logs_filter("debug,icechunk=trace")
18+
```

icechunk-python/python/icechunk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
VirtualChunkSpec,
4444
__version__,
4545
initialize_logs,
46+
set_logs_filter,
4647
spec_version,
4748
)
4849
from icechunk.credentials import (
@@ -153,6 +154,7 @@
153154
"s3_static_credentials",
154155
"s3_storage",
155156
"s3_store",
157+
"set_logs_filter",
156158
"spec_version",
157159
"tigris_storage",
158160
]

icechunk-python/python/icechunk/_icechunk_python.pyi

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,29 @@ def initialize_logs() -> None:
18451845
"""
18461846
Initialize the logging system for the library.
18471847
1848-
This should be called before any other Icechunk functions are called.
1848+
Reads the value of the environment variable ICECHUNK_LOG to obtain the filters.
1849+
This is autamtically called on `import icechunk`.
1850+
"""
1851+
...
1852+
1853+
def set_logs_filter(log_filter_directive: str | None) -> None:
1854+
"""
1855+
Set filters and log levels for the different modules.
1856+
1857+
Examples:
1858+
- set_logs_filter("trace") # trace level for all modules
1859+
- set_logs_filter("error") # error level for all modules
1860+
- set_logs_filter("icechunk=debug,info") # debug level for icechunk, info for everything else
1861+
1862+
Full spec for the log_filter_directive syntax is documented in
1863+
https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
1864+
1865+
Parameters
1866+
----------
1867+
log_filter_directive: str | None
1868+
The comma separated list of directives for modules and log levels.
1869+
If None, the directive will be read from the environment variable
1870+
ICECHUNK_LOG
18491871
"""
18501872
...
18511873

icechunk-python/src/lib.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use conflicts::{
2727
use errors::{IcechunkError, PyConflictError, PyRebaseFailedError};
2828
use icechunk::{format::format_constants::SpecVersionBin, initialize_tracing};
2929
use pyo3::prelude::*;
30+
use pyo3::types::PyMapping;
3031
use pyo3::wrap_pyfunction;
3132
use repository::{PyDiff, PyGCSummary, PyManifestFileInfo, PyRepository, PySnapshotInfo};
3233
use session::PySession;
@@ -69,14 +70,31 @@ fn cli_entrypoint(_py: Python) -> PyResult<()> {
6970
Ok(())
7071
}
7172

73+
fn log_filters_from_env(py: Python) -> PyResult<Option<String>> {
74+
let os = py.import("os")?;
75+
let environ = os.getattr("environ")?;
76+
let environ: &Bound<PyMapping> = environ.downcast()?;
77+
let value = environ.get_item("ICECHUNK_LOG").ok().and_then(|v| v.extract().ok());
78+
Ok(value)
79+
}
80+
7281
#[pyfunction]
73-
fn initialize_logs() -> PyResult<()> {
82+
fn initialize_logs(py: Python) -> PyResult<()> {
7483
if env::var("ICECHUNK_NO_LOGS").is_err() {
75-
initialize_tracing()
84+
let log_filter_directive = log_filters_from_env(py)?;
85+
initialize_tracing(log_filter_directive.as_deref())
7686
}
7787
Ok(())
7888
}
7989

90+
#[pyfunction]
91+
fn set_logs_filter(py: Python, log_filter_directive: Option<String>) -> PyResult<()> {
92+
let log_filter_directive =
93+
log_filter_directive.or_else(|| log_filters_from_env(py).ok().flatten());
94+
initialize_tracing(log_filter_directive.as_deref());
95+
Ok(())
96+
}
97+
8098
#[pyfunction]
8199
/// The spec version that this version of the Icechunk library
82100
/// uses to write metadata files
@@ -125,6 +143,7 @@ fn _icechunk_python(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
125143
m.add_class::<PyDiff>()?;
126144
m.add_class::<VirtualChunkSpec>()?;
127145
m.add_function(wrap_pyfunction!(initialize_logs, m)?)?;
146+
m.add_function(wrap_pyfunction!(set_logs_filter, m)?)?;
128147
m.add_function(wrap_pyfunction!(spec_version, m)?)?;
129148
m.add_function(wrap_pyfunction!(cli_entrypoint, m)?)?;
130149

icechunk-python/tests/test_logs.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
from unittest import mock
3+
4+
import icechunk as ic
5+
6+
7+
@mock.patch.dict(os.environ, {"ICECHUNK_LOG": "debug"}, clear=True)
8+
def test_debug_logs_from_environment(capfd):
9+
ic.set_logs_filter(None)
10+
ic.Repository.create(storage=ic.in_memory_storage())
11+
assert "Creating Repository" in capfd.readouterr().out
12+
13+
14+
@mock.patch.dict(os.environ, clear=True)
15+
def test_no_logs_from_environment(capfd):
16+
ic.set_logs_filter(None)
17+
ic.Repository.create(storage=ic.in_memory_storage())
18+
assert capfd.readouterr().out == ""
19+
20+
21+
@mock.patch.dict(os.environ, clear=True)
22+
def test_change_log_levels_from_env(capfd):
23+
# first with logs disabled
24+
ic.set_logs_filter(None)
25+
ic.Repository.create(storage=ic.in_memory_storage())
26+
assert capfd.readouterr().out == ""
27+
28+
# now with logs enabled
29+
with mock.patch.dict(os.environ, {"ICECHUNK_LOG": "debug"}, clear=True):
30+
ic.set_logs_filter(None)
31+
ic.Repository.create(storage=ic.in_memory_storage())
32+
assert "Creating Repository" in capfd.readouterr().out
33+
34+
35+
def test_debug_logs_from_argument(capfd):
36+
ic.set_logs_filter("debug")
37+
ic.Repository.create(storage=ic.in_memory_storage())
38+
assert "Creating Repository" in capfd.readouterr().out
39+
40+
41+
@mock.patch.dict(os.environ, {"ICECHUNK_LOG": "debug"}, clear=True)
42+
def test_no_logs_from_argument(capfd):
43+
ic.set_logs_filter("false")
44+
ic.Repository.create(storage=ic.in_memory_storage())
45+
assert capfd.readouterr().out == ""
46+
47+
48+
def test_change_log_levels_from_argument(capfd):
49+
# first with logs disabled
50+
ic.set_logs_filter("")
51+
ic.Repository.create(storage=ic.in_memory_storage())
52+
assert capfd.readouterr().out == ""
53+
54+
# now with logs enabled
55+
ic.set_logs_filter("debug")
56+
ic.Repository.create(storage=ic.in_memory_storage())
57+
assert "Creating Repository" in capfd.readouterr().out

icechunk/src/lib.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,60 @@ mod private {
4949
}
5050

5151
#[cfg(feature = "logs")]
52-
pub fn initialize_tracing() {
52+
#[allow(clippy::type_complexity)]
53+
static LOG_FILTER: std::sync::LazyLock<
54+
std::sync::Mutex<
55+
Option<
56+
tracing_subscriber::reload::Handle<
57+
tracing_subscriber::EnvFilter,
58+
tracing_subscriber::layer::Layered<
59+
tracing_error::ErrorLayer<tracing_subscriber::Registry>,
60+
tracing_subscriber::Registry,
61+
>,
62+
>,
63+
>,
64+
>,
65+
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
66+
67+
#[cfg(feature = "logs")]
68+
pub fn initialize_tracing(log_filter_directive: Option<&str>) {
5369
use tracing_error::ErrorLayer;
5470
use tracing_subscriber::{
55-
EnvFilter, Layer, Registry, layer::SubscriberExt, util::SubscriberInitExt,
71+
EnvFilter, Layer, Registry, layer::SubscriberExt, reload, util::SubscriberInitExt,
5672
};
5773

5874
// We have two Layers. One keeps track of the spans to feed the ICError instances.
5975
// The other is the one spitting logs to stdout. Filtering only applies to the second Layer.
6076

61-
let stdout_layer = tracing_subscriber::fmt::layer()
62-
.pretty()
63-
.with_filter(EnvFilter::from_env("ICECHUNK_LOG"));
77+
let filter = log_filter_directive
78+
.map(EnvFilter::new)
79+
.unwrap_or_else(|| EnvFilter::from_env("ICECHUNK_LOG"));
80+
match LOG_FILTER.lock() {
81+
Ok(mut guard) => match guard.as_ref() {
82+
Some(handle) => {
83+
if let Err(err) = handle.reload(filter) {
84+
println!("Error reloading log settings: {}", err)
85+
}
86+
}
87+
None => {
88+
let (filter, handle) = reload::Layer::new(filter);
89+
*guard = Some(handle);
90+
let stdout_layer =
91+
tracing_subscriber::fmt::layer().pretty().with_filter(filter);
6492

65-
let error_span_layer = ErrorLayer::default();
93+
let error_span_layer = ErrorLayer::default();
6694

67-
if let Err(err) =
68-
Registry::default().with(error_span_layer).with(stdout_layer).try_init()
69-
{
70-
println!("Warning: {}", err);
95+
if let Err(err) = Registry::default()
96+
.with(error_span_layer)
97+
.with(stdout_layer)
98+
.try_init()
99+
{
100+
println!("Error initializing logs: {}", err);
101+
}
102+
}
103+
},
104+
Err(err) => {
105+
println!("Error setting up logs: {}", err)
106+
}
71107
}
72108
}

0 commit comments

Comments
 (0)