Skip to content

Commit 76f835c

Browse files
committed
feature(logging): include keys in logging store logs
1 parent 3b787a4 commit 76f835c

File tree

2 files changed

+69
-16
lines changed

2 files changed

+69
-16
lines changed

src/zarr/storage/logging.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66
from collections import defaultdict
77
from contextlib import contextmanager
8-
from typing import TYPE_CHECKING, Self
8+
from typing import TYPE_CHECKING, Any, Self
99

1010
from zarr.abc.store import AccessMode, ByteRangeRequest, Store
1111
from zarr.core.buffer import Buffer
@@ -57,17 +57,24 @@ def _default_handler(self) -> logging.Handler:
5757
return handler
5858

5959
@contextmanager
60-
def log(self) -> Generator[None, None, None]:
60+
def log(self, hint: Any = "") -> Generator[None, None, None]:
61+
"""Context manager to log method calls
62+
63+
Each call to the wrapped store is logged to the configured logger and added to
64+
the counter dict.
65+
"""
6166
method = inspect.stack()[2].function
6267
op = f"{type(self._store).__name__}.{method}"
68+
if hint:
69+
op += f"({hint})"
6370
self.logger.info(f"Calling {op}")
6471
start_time = time.time()
6572
try:
6673
self.counter[method] += 1
6774
yield
6875
finally:
6976
end_time = time.time()
70-
self.logger.info(f"Finished {op} in {end_time - start_time:.2f} seconds")
77+
self.logger.info(f"Finished {op} [{end_time - start_time:.2f}s]")
7178

7279
@property
7380
def supports_writes(self) -> bool:
@@ -95,10 +102,15 @@ def _mode(self) -> AccessMode: # type: ignore[override]
95102
return self._store._mode
96103

97104
@property
98-
def _is_open(self) -> bool: # type: ignore[override]
105+
def _is_open(self) -> bool:
99106
with self.log():
100107
return self._store._is_open
101108

109+
@_is_open.setter
110+
def _is_open(self, value: bool) -> None:
111+
with self.log(value):
112+
self._store._is_open = value
113+
102114
async def _open(self) -> None:
103115
with self.log():
104116
return await self._store._open()
@@ -122,7 +134,7 @@ def __repr__(self) -> str:
122134
return f"LoggingStore({repr(self._store)!r})"
123135

124136
def __eq__(self, other: object) -> bool:
125-
with self.log():
137+
with self.log(other):
126138
return self._store == other
127139

128140
async def get(
@@ -131,56 +143,69 @@ async def get(
131143
prototype: BufferPrototype,
132144
byte_range: tuple[int | None, int | None] | None = None,
133145
) -> Buffer | None:
134-
with self.log():
146+
# docstring inherited
147+
with self.log(key):
135148
return await self._store.get(key=key, prototype=prototype, byte_range=byte_range)
136149

137150
async def get_partial_values(
138151
self,
139152
prototype: BufferPrototype,
140153
key_ranges: Iterable[tuple[str, ByteRangeRequest]],
141154
) -> list[Buffer | None]:
142-
with self.log():
155+
# docstring inherited
156+
keys = ",".join([k[0] for k in key_ranges])
157+
with self.log(keys):
143158
return await self._store.get_partial_values(prototype=prototype, key_ranges=key_ranges)
144159

145160
async def exists(self, key: str) -> bool:
146-
with self.log():
161+
# docstring inherited
162+
with self.log(key):
147163
return await self._store.exists(key)
148164

149165
async def set(self, key: str, value: Buffer) -> None:
150-
with self.log():
166+
# docstring inherited
167+
with self.log(key):
151168
return await self._store.set(key=key, value=value)
152169

153170
async def set_if_not_exists(self, key: str, value: Buffer) -> None:
154-
with self.log():
171+
# docstring inherited
172+
with self.log(key):
155173
return await self._store.set_if_not_exists(key=key, value=value)
156174

157175
async def delete(self, key: str) -> None:
158-
with self.log():
176+
# docstring inherited
177+
with self.log(key):
159178
return await self._store.delete(key=key)
160179

161180
async def set_partial_values(
162181
self, key_start_values: Iterable[tuple[str, int, bytes | bytearray | memoryview]]
163182
) -> None:
164-
with self.log():
183+
# docstring inherited
184+
keys = ",".join([k[0] for k in key_start_values])
185+
with self.log(keys):
165186
return await self._store.set_partial_values(key_start_values=key_start_values)
166187

167188
async def list(self) -> AsyncGenerator[str, None]:
189+
# docstring inherited
168190
with self.log():
169191
async for key in self._store.list():
170192
yield key
171193

172194
async def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]:
173-
with self.log():
195+
# docstring inherited
196+
with self.log(prefix):
174197
async for key in self._store.list_prefix(prefix=prefix):
175198
yield key
176199

177200
async def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
178-
with self.log():
201+
# docstring inherited
202+
with self.log(prefix):
179203
async for key in self._store.list_dir(prefix=prefix):
180204
yield key
181205

182206
def with_mode(self, mode: AccessModeLiteral) -> Self:
183-
with self.log():
207+
# docstring inherited
208+
with self.log(mode):
184209
return type(self)(
185210
self._store.with_mode(mode),
186211
log_level=self.log_level,

src/zarr/storage/remote.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,35 @@ def from_url(
8282
mode: AccessModeLiteral = "r",
8383
allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS,
8484
) -> RemoteStore:
85-
fs, path = fsspec.url_to_fs(url, **storage_options)
85+
"""
86+
Create a RemoteStore from a URL.
87+
88+
Parameters
89+
----------
90+
url : str
91+
The URL to the root of the store.
92+
storage_options : dict, optional
93+
The options to pass to fsspec when creating the filesystem.
94+
mode : str, optional
95+
The mode of the store. Defaults to "r".
96+
allowed_exceptions : tuple, optional
97+
The exceptions that are allowed to be raised when accessing the
98+
store. Defaults to ALLOWED_EXCEPTIONS.
99+
100+
Returns
101+
-------
102+
RemoteStore
103+
"""
104+
opts = storage_options or {}
105+
opts = {"asynchronous": True, **opts}
106+
107+
fs, path = fsspec.url_to_fs(url, **opts)
108+
109+
# fsspec is not consistent about removing the scheme from the path, so check and strip it here
110+
# https://github.com/fsspec/filesystem_spec/issues/1722
111+
if "://" in path:
112+
_, path = path.split("://", maxsplit=1)
113+
86114
return cls(fs=fs, path=path, mode=mode, allowed_exceptions=allowed_exceptions)
87115

88116
async def clear(self) -> None:

0 commit comments

Comments
 (0)