scistacklog 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scistacklog/__init__.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""scistacklog — the shared logging facade for every scistack layer.
|
|
2
|
+
|
|
3
|
+
Dual-sink design:
|
|
4
|
+
|
|
5
|
+
- **console** (stderr): concise pipeline narrative for the person running the
|
|
6
|
+
pipeline. Time-only timestamps, level shown only at WARN and above.
|
|
7
|
+
- **file** (``scidb.log`` next to the database, set via ``Log.set_path``):
|
|
8
|
+
the run document. Date + millisecond timestamps, level, and originating
|
|
9
|
+
layer on every line; one record per line.
|
|
10
|
+
|
|
11
|
+
Both sinks default to INFO. DEBUG detail (per-iteration lines, timing phase
|
|
12
|
+
tables, framework internals) is opt-in per sink::
|
|
13
|
+
|
|
14
|
+
Log.set_level("DEBUG", sink="file") # full detail in the file only
|
|
15
|
+
Log.set_level("DEBUG") # both sinks
|
|
16
|
+
|
|
17
|
+
or via the ``SCIDB_LOG_LEVEL`` environment variable (applied to both sinks).
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
from scistacklog import Log
|
|
22
|
+
|
|
23
|
+
Log.info("for_each(compute_psd) — 120 iterations", layer="scifor")
|
|
24
|
+
Log.debug("combo detail: %s", combo, layer="scifor")
|
|
25
|
+
Log.error("save failed", layer="scidb", exc_info=True)
|
|
26
|
+
|
|
27
|
+
Every emit goes through a stdlib logger named after the layer (``scidb``,
|
|
28
|
+
``scifor``, …), so existing module loggers such as ``scidb.discover`` or
|
|
29
|
+
``scilineage.hashing`` are covered automatically as children. Level
|
|
30
|
+
filtering happens on the two handlers, never on the loggers: records always
|
|
31
|
+
propagate to the root logger, so pytest's ``caplog`` captures them
|
|
32
|
+
regardless of sink levels.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
import threading
|
|
41
|
+
import time
|
|
42
|
+
import warnings
|
|
43
|
+
from contextlib import contextmanager
|
|
44
|
+
|
|
45
|
+
__all__ = ["Log", "LAYERS"]
|
|
46
|
+
|
|
47
|
+
#: Top-level logger name for each scistack layer. A record's ``[layer]`` tag
|
|
48
|
+
#: in the log line is the first dotted component of its logger name.
|
|
49
|
+
LAYERS = (
|
|
50
|
+
"scidb",
|
|
51
|
+
"scifor",
|
|
52
|
+
"sciduck",
|
|
53
|
+
"scihist",
|
|
54
|
+
"scilineage",
|
|
55
|
+
"scistack",
|
|
56
|
+
"scistack_gui",
|
|
57
|
+
"matlab",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# scidb's historical 0-3 level scale — the MATLAB-facing numeric contract.
|
|
61
|
+
_SCIDB_TO_PY = {
|
|
62
|
+
0: logging.DEBUG,
|
|
63
|
+
1: logging.INFO,
|
|
64
|
+
2: logging.WARNING,
|
|
65
|
+
3: logging.ERROR,
|
|
66
|
+
}
|
|
67
|
+
_NAME_TO_SCIDB = {"DEBUG": 0, "INFO": 1, "WARN": 2, "WARNING": 2, "ERROR": 3}
|
|
68
|
+
_SHORT_NAMES = {
|
|
69
|
+
logging.DEBUG: "DEBUG",
|
|
70
|
+
logging.INFO: "INFO",
|
|
71
|
+
logging.WARNING: "WARN",
|
|
72
|
+
logging.ERROR: "ERROR",
|
|
73
|
+
logging.CRITICAL: "ERROR",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_FILE_FORMAT = "%(asctime)s.%(msecs)03d %(levelshort)-5s [%(layer)s] %(message)s"
|
|
77
|
+
_FILE_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
|
78
|
+
_CONSOLE_DATEFMT = "%H:%M:%S"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _RecordTagger(logging.Filter):
|
|
82
|
+
"""Adds the fields the formatters need: layer and short level name."""
|
|
83
|
+
|
|
84
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
85
|
+
record.layer = record.name.split(".", 1)[0]
|
|
86
|
+
record.levelshort = _SHORT_NAMES.get(record.levelno, record.levelname)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _ConsoleFormatter(logging.Formatter):
|
|
91
|
+
"""Time-only timestamps; level prefix only at WARN and above."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
super().__init__(
|
|
95
|
+
"%(asctime)s [%(layer)s] %(message)s", datefmt=_CONSOLE_DATEFMT
|
|
96
|
+
)
|
|
97
|
+
self._alert = logging.Formatter(
|
|
98
|
+
"%(asctime)s [%(layer)s] %(levelshort)s: %(message)s",
|
|
99
|
+
datefmt=_CONSOLE_DATEFMT,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
103
|
+
if record.levelno >= logging.WARNING:
|
|
104
|
+
return self._alert.format(record)
|
|
105
|
+
return super().format(record)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _StderrHandler(logging.StreamHandler):
|
|
109
|
+
"""Console handler that resolves ``sys.stderr`` at emit time.
|
|
110
|
+
|
|
111
|
+
Looking the stream up dynamically keeps the handler correct when the
|
|
112
|
+
hosting process swaps stderr (pytest's capsys, MATLAB, GUI harnesses).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self) -> None:
|
|
116
|
+
super().__init__(sys.stderr)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def stream(self):
|
|
120
|
+
return sys.stderr
|
|
121
|
+
|
|
122
|
+
@stream.setter
|
|
123
|
+
def stream(self, value):
|
|
124
|
+
pass # always dynamic; StreamHandler.__init__/setStream assignments are no-ops
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _Timer:
|
|
128
|
+
"""Collects named phase durations for :meth:`Log.timer`."""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self.phases: "list[tuple[str, float]]" = []
|
|
132
|
+
|
|
133
|
+
@contextmanager
|
|
134
|
+
def phase(self, name: str):
|
|
135
|
+
t0 = time.perf_counter()
|
|
136
|
+
try:
|
|
137
|
+
yield
|
|
138
|
+
finally:
|
|
139
|
+
self.phases.append((name, time.perf_counter() - t0))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Log:
|
|
143
|
+
"""Framework-wide logging facade (classmethod API, MATLAB-bridged).
|
|
144
|
+
|
|
145
|
+
Backed by stdlib logging: one stderr console handler and one file
|
|
146
|
+
handler, both attached to every layer logger in :data:`LAYERS`.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
DEBUG = 0
|
|
150
|
+
INFO = 1
|
|
151
|
+
WARN = 2
|
|
152
|
+
ERROR = 3
|
|
153
|
+
|
|
154
|
+
_lock = threading.RLock()
|
|
155
|
+
_attached = False
|
|
156
|
+
_tagger = _RecordTagger()
|
|
157
|
+
_console_handler: "logging.Handler | None" = None
|
|
158
|
+
_file_handler: "logging.FileHandler | None" = None
|
|
159
|
+
_path: "str | None" = None
|
|
160
|
+
_console_py_level: int = logging.INFO
|
|
161
|
+
_file_py_level: int = logging.INFO
|
|
162
|
+
_unknown_layers: "set[str]" = set()
|
|
163
|
+
|
|
164
|
+
# -- setup ------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def attach(cls) -> None:
|
|
168
|
+
"""Create the sinks and attach them to every layer logger.
|
|
169
|
+
|
|
170
|
+
Idempotent; called lazily by every other method, so nothing happens
|
|
171
|
+
at import time. Reads ``SCIDB_LOG_LEVEL`` (both sinks) on first call.
|
|
172
|
+
"""
|
|
173
|
+
with cls._lock:
|
|
174
|
+
if cls._attached:
|
|
175
|
+
return
|
|
176
|
+
env_level = os.environ.get("SCIDB_LOG_LEVEL")
|
|
177
|
+
if env_level:
|
|
178
|
+
py_level = cls._to_py_level(env_level)
|
|
179
|
+
cls._console_py_level = py_level
|
|
180
|
+
cls._file_py_level = py_level
|
|
181
|
+
handler = _StderrHandler()
|
|
182
|
+
handler.setLevel(cls._console_py_level)
|
|
183
|
+
handler.setFormatter(_ConsoleFormatter())
|
|
184
|
+
handler.addFilter(cls._tagger)
|
|
185
|
+
cls._console_handler = handler
|
|
186
|
+
for name in LAYERS:
|
|
187
|
+
layer_logger = logging.getLogger(name)
|
|
188
|
+
# Filtering happens on the handlers; the logger passes
|
|
189
|
+
# everything so caplog/root handlers see all records.
|
|
190
|
+
layer_logger.setLevel(logging.DEBUG)
|
|
191
|
+
layer_logger.propagate = True
|
|
192
|
+
if handler not in layer_logger.handlers:
|
|
193
|
+
layer_logger.addHandler(handler)
|
|
194
|
+
cls._attached = True
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def bridge_python_logging(cls) -> None:
|
|
198
|
+
"""Deprecated alias for :meth:`attach`.
|
|
199
|
+
|
|
200
|
+
The old implementation installed a bridge handler on a hardcoded
|
|
201
|
+
subset of logger names; the layer-logger hierarchy replaces it.
|
|
202
|
+
"""
|
|
203
|
+
cls.attach()
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def set_path(cls, log_path: "str | None") -> None:
|
|
207
|
+
"""Point the file sink at ``log_path`` (``None`` detaches it).
|
|
208
|
+
|
|
209
|
+
Called automatically by ``scidb.configure_database()``.
|
|
210
|
+
"""
|
|
211
|
+
cls.attach()
|
|
212
|
+
with cls._lock:
|
|
213
|
+
if cls._file_handler is not None:
|
|
214
|
+
for name in LAYERS:
|
|
215
|
+
logging.getLogger(name).removeHandler(cls._file_handler)
|
|
216
|
+
cls._file_handler.close()
|
|
217
|
+
cls._file_handler = None
|
|
218
|
+
cls._path = None if log_path is None else str(log_path)
|
|
219
|
+
if cls._path is None:
|
|
220
|
+
return
|
|
221
|
+
handler = logging.FileHandler(
|
|
222
|
+
cls._path, mode="a", encoding="utf-8", delay=True
|
|
223
|
+
)
|
|
224
|
+
handler.setLevel(cls._file_py_level)
|
|
225
|
+
handler.setFormatter(
|
|
226
|
+
logging.Formatter(_FILE_FORMAT, datefmt=_FILE_DATEFMT)
|
|
227
|
+
)
|
|
228
|
+
handler.addFilter(cls._tagger)
|
|
229
|
+
cls._file_handler = handler
|
|
230
|
+
for name in LAYERS:
|
|
231
|
+
logging.getLogger(name).addHandler(handler)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def get_path(cls) -> "str | None":
|
|
235
|
+
"""Current log file path (``None`` if the file sink is detached)."""
|
|
236
|
+
return cls._path
|
|
237
|
+
|
|
238
|
+
# -- levels -----------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def set_level(cls, level: "int | str", sink: str = "both") -> None:
|
|
242
|
+
"""Set the level of one or both sinks.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
level: ``'DEBUG'``/``'INFO'``/``'WARN'``/``'ERROR'`` or numeric 0-3.
|
|
246
|
+
sink: ``'console'``, ``'file'``, or ``'both'``.
|
|
247
|
+
"""
|
|
248
|
+
if sink not in ("console", "file", "both"):
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Unknown sink '{sink}' (expected 'console', 'file', or 'both')"
|
|
251
|
+
)
|
|
252
|
+
cls.attach()
|
|
253
|
+
py_level = cls._to_py_level(level)
|
|
254
|
+
with cls._lock:
|
|
255
|
+
if sink in ("console", "both"):
|
|
256
|
+
cls._console_py_level = py_level
|
|
257
|
+
if cls._console_handler is not None:
|
|
258
|
+
cls._console_handler.setLevel(py_level)
|
|
259
|
+
if sink in ("file", "both"):
|
|
260
|
+
cls._file_py_level = py_level
|
|
261
|
+
if cls._file_handler is not None:
|
|
262
|
+
cls._file_handler.setLevel(py_level)
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def get_level(cls, sink: "str | None" = None) -> int:
|
|
266
|
+
"""Level of a sink on the 0-3 scale; min of both sinks when ``None``.
|
|
267
|
+
|
|
268
|
+
The ``None`` form is what MATLAB caches to gate calls client-side:
|
|
269
|
+
a message suppressed by both sinks never needs to cross the bridge.
|
|
270
|
+
"""
|
|
271
|
+
if sink == "console":
|
|
272
|
+
return cls._from_py_level(cls._console_py_level)
|
|
273
|
+
if sink == "file":
|
|
274
|
+
return cls._from_py_level(cls._file_py_level)
|
|
275
|
+
return cls._from_py_level(
|
|
276
|
+
min(cls._console_py_level, cls._file_py_level)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# -- emission ---------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def debug(cls, msg: str, *args, layer: str = "scidb",
|
|
283
|
+
exc_info: bool = False) -> None:
|
|
284
|
+
"""Log a message at DEBUG level."""
|
|
285
|
+
cls._log(logging.DEBUG, msg, args, layer, exc_info)
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def info(cls, msg: str, *args, layer: str = "scidb",
|
|
289
|
+
exc_info: bool = False) -> None:
|
|
290
|
+
"""Log a message at INFO level."""
|
|
291
|
+
cls._log(logging.INFO, msg, args, layer, exc_info)
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def warn(cls, msg: str, *args, layer: str = "scidb",
|
|
295
|
+
exc_info: bool = False) -> None:
|
|
296
|
+
"""Log a message at WARN level."""
|
|
297
|
+
cls._log(logging.WARNING, msg, args, layer, exc_info)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def error(cls, msg: str, *args, layer: str = "scidb",
|
|
301
|
+
exc_info: bool = False) -> None:
|
|
302
|
+
"""Log a message at ERROR level."""
|
|
303
|
+
cls._log(logging.ERROR, msg, args, layer, exc_info)
|
|
304
|
+
|
|
305
|
+
# -- instrumentation ----------------------------------------------------
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
@contextmanager
|
|
309
|
+
def step(cls, name: str, *, layer: str = "scidb"):
|
|
310
|
+
"""Trace one named operation: entry/exit at DEBUG, failure at ERROR.
|
|
311
|
+
|
|
312
|
+
Names are snake_case operations (``resolve_empty_lists``,
|
|
313
|
+
``delegate_to_scifor``) — never numbered steps; ordering is conveyed
|
|
314
|
+
by the log's own sequence. At the default (INFO) level these are
|
|
315
|
+
silent; at DEBUG the file reads as a nested execution trace with
|
|
316
|
+
per-step durations. An exception is logged with its traceback and
|
|
317
|
+
duration, then re-raised — so a failed run's log always ends with
|
|
318
|
+
the error in context.
|
|
319
|
+
|
|
320
|
+
Usage::
|
|
321
|
+
|
|
322
|
+
with Log.step("save_batch(PSD)", layer="scidb"):
|
|
323
|
+
...
|
|
324
|
+
"""
|
|
325
|
+
cls.debug(f"→ {name}", layer=layer)
|
|
326
|
+
t0 = time.perf_counter()
|
|
327
|
+
try:
|
|
328
|
+
yield
|
|
329
|
+
except Exception as e:
|
|
330
|
+
cls.error(
|
|
331
|
+
f"✗ {name} failed after {time.perf_counter() - t0:.3f}s: "
|
|
332
|
+
f"{type(e).__name__}: {e}",
|
|
333
|
+
layer=layer, exc_info=True,
|
|
334
|
+
)
|
|
335
|
+
raise
|
|
336
|
+
cls.debug(f"← {name} done in {time.perf_counter() - t0:.3f}s",
|
|
337
|
+
layer=layer)
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
@contextmanager
|
|
341
|
+
def timer(cls, name: str, *, layer: str = "scidb",
|
|
342
|
+
extra: "str | None" = None):
|
|
343
|
+
"""Phase timing for a hot operation, emitted under the ``[timing]`` tag.
|
|
344
|
+
|
|
345
|
+
Yields a :class:`_Timer`; wrap sub-phases with ``t.phase("name")``
|
|
346
|
+
(snake_case, never numbered). On exit emits one INFO summary line —
|
|
347
|
+
``[timing] name: TOTAL=…s (phase=…s, …)`` — plus one DEBUG table
|
|
348
|
+
line per phase. The ``[timing]`` prefix is a stable grep target
|
|
349
|
+
(MATLAB timing-test archives rely on it).
|
|
350
|
+
|
|
351
|
+
Usage::
|
|
352
|
+
|
|
353
|
+
with Log.timer("save_batch(PSD)", extra="114 items") as t:
|
|
354
|
+
with t.phase("canonical_hash"): ...
|
|
355
|
+
with t.phase("commit"): ...
|
|
356
|
+
"""
|
|
357
|
+
t = _Timer()
|
|
358
|
+
t0 = time.perf_counter()
|
|
359
|
+
try:
|
|
360
|
+
yield t
|
|
361
|
+
finally:
|
|
362
|
+
total = time.perf_counter() - t0
|
|
363
|
+
parts = ", ".join(f"{p}={s:.3f}s" for p, s in t.phases)
|
|
364
|
+
detail = f" ({parts})" if parts else ""
|
|
365
|
+
prefix = f"{extra}, " if extra else ""
|
|
366
|
+
cls.info(f"[timing] {name}: {prefix}TOTAL={total:.3f}s{detail}",
|
|
367
|
+
layer=layer)
|
|
368
|
+
for p, s in t.phases:
|
|
369
|
+
cls.debug(f" {name} {p:<30s} {s:.3f}s", layer=layer)
|
|
370
|
+
|
|
371
|
+
# -- internals ---------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def _log(cls, py_level: int, msg: str, args: tuple, layer: str,
|
|
375
|
+
exc_info: bool) -> None:
|
|
376
|
+
cls.attach()
|
|
377
|
+
name = str(layer).split(".", 1)[0]
|
|
378
|
+
if name not in LAYERS:
|
|
379
|
+
with cls._lock:
|
|
380
|
+
if name not in cls._unknown_layers:
|
|
381
|
+
cls._unknown_layers.add(name)
|
|
382
|
+
warnings.warn(
|
|
383
|
+
f"Unknown scistack layer '{layer}', logging as 'scidb'.",
|
|
384
|
+
UserWarning,
|
|
385
|
+
)
|
|
386
|
+
name = "scidb"
|
|
387
|
+
logging.getLogger(name).log(py_level, msg, *args, exc_info=exc_info)
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def _to_py_level(cls, level: "int | str") -> int:
|
|
391
|
+
if isinstance(level, str):
|
|
392
|
+
name = level.upper()
|
|
393
|
+
if name not in _NAME_TO_SCIDB:
|
|
394
|
+
warnings.warn(
|
|
395
|
+
f"Unknown log level '{name}', defaulting to INFO.",
|
|
396
|
+
UserWarning,
|
|
397
|
+
)
|
|
398
|
+
return logging.INFO
|
|
399
|
+
return _SCIDB_TO_PY[_NAME_TO_SCIDB[name]]
|
|
400
|
+
try:
|
|
401
|
+
numeric = int(level)
|
|
402
|
+
except (TypeError, ValueError):
|
|
403
|
+
warnings.warn(
|
|
404
|
+
f"Unknown log level {level!r}, defaulting to INFO.", UserWarning
|
|
405
|
+
)
|
|
406
|
+
return logging.INFO
|
|
407
|
+
if numeric not in _SCIDB_TO_PY:
|
|
408
|
+
warnings.warn(
|
|
409
|
+
f"Unknown log level {level!r}, defaulting to INFO.", UserWarning
|
|
410
|
+
)
|
|
411
|
+
return logging.INFO
|
|
412
|
+
return _SCIDB_TO_PY[numeric]
|
|
413
|
+
|
|
414
|
+
@staticmethod
|
|
415
|
+
def _from_py_level(py_level: int) -> int:
|
|
416
|
+
if py_level <= logging.DEBUG:
|
|
417
|
+
return Log.DEBUG
|
|
418
|
+
if py_level <= logging.INFO:
|
|
419
|
+
return Log.INFO
|
|
420
|
+
if py_level <= logging.WARNING:
|
|
421
|
+
return Log.WARN
|
|
422
|
+
return Log.ERROR
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
def _reset_for_tests(cls) -> None:
|
|
426
|
+
"""Detach all handlers and reset state. Test isolation only."""
|
|
427
|
+
with cls._lock:
|
|
428
|
+
for handler in (cls._console_handler, cls._file_handler):
|
|
429
|
+
if handler is not None:
|
|
430
|
+
for name in LAYERS:
|
|
431
|
+
logging.getLogger(name).removeHandler(handler)
|
|
432
|
+
handler.close()
|
|
433
|
+
cls._console_handler = None
|
|
434
|
+
cls._file_handler = None
|
|
435
|
+
cls._path = None
|
|
436
|
+
cls._attached = False
|
|
437
|
+
cls._console_py_level = logging.INFO
|
|
438
|
+
cls._file_py_level = logging.INFO
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scistacklog
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Shared logging facade for the scistack framework (dual-sink console/file logging with layer tags)
|
|
5
|
+
Author: SciStack Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: data-science,logging,pipeline
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Classifier: Topic :: System :: Logging
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# scistacklog
|
|
26
|
+
|
|
27
|
+
The shared logging facade for every scistack layer. This is the lowest-level
|
|
28
|
+
package in the stack — it has no dependencies, and every other layer (scifor,
|
|
29
|
+
scidb, sciduckdb, …) emits through it or through a plain
|
|
30
|
+
`logging.getLogger("<layer>")` child logger that it covers.
|
|
31
|
+
|
|
32
|
+
## Design
|
|
33
|
+
|
|
34
|
+
One `Log` call, two destinations with independent levels:
|
|
35
|
+
|
|
36
|
+
- **console** (stderr) — a concise pipeline narrative for the person running
|
|
37
|
+
the pipeline. Time-only timestamps; level prefix only at WARN and above.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
14:32:06 [scifor] for_each(compute_psd) — 120 iterations: subject=12 values [s01,…,s12]
|
|
41
|
+
14:33:15 [scifor] WARN: iteration failed: subject=s03, session=2 — ValueError: bad channel count
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **file** (`scidb.log` next to the database, pointed via `Log.set_path`,
|
|
45
|
+
done automatically by `scidb.configure_database()`) — the run document.
|
|
46
|
+
One record per line: date + millisecond timestamp, level, originating layer.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
2026-07-07 14:32:06.001 INFO [scifor] for_each(compute_psd) — 120 iterations: subject=12 values [s01,…,s12]
|
|
50
|
+
2026-07-07 14:33:20.100 INFO [scidb] [timing] save_batch(PSD): 114 items, 12 schemas, 0.412s
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both sinks default to INFO. DEBUG detail (per-iteration lines, timing phase
|
|
54
|
+
tables, framework internals) is opt-in:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from scistacklog import Log
|
|
58
|
+
|
|
59
|
+
Log.set_level("DEBUG", sink="file") # full detail in the file only
|
|
60
|
+
Log.set_level("DEBUG") # both sinks
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
or set the `SCIDB_LOG_LEVEL` environment variable (applies to both sinks),
|
|
64
|
+
or pass `-v` to the `scidb` CLI.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from scistacklog import Log
|
|
70
|
+
|
|
71
|
+
Log.info("for_each(compute_psd) — 120 iterations", layer="scifor")
|
|
72
|
+
Log.debug("combo detail: %s", combo, layer="scifor")
|
|
73
|
+
Log.error("save failed", layer="scidb", exc_info=True)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`layer` must be one of `scistacklog.LAYERS`
|
|
77
|
+
(`scidb`, `scifor`, `sciduck`, `scihist`, `scilineage`, `scistack`,
|
|
78
|
+
`scistack_gui`, `matlab`).
|
|
79
|
+
|
|
80
|
+
## Contracts
|
|
81
|
+
|
|
82
|
+
- **caplog**: level filtering happens on the two handlers, never on the
|
|
83
|
+
loggers — every record propagates to the root logger, so pytest's `caplog`
|
|
84
|
+
captures everything regardless of sink levels. `Log.*(...)` with the
|
|
85
|
+
default layer emits on logger `"scidb"`, unchanged from the historical
|
|
86
|
+
implementation.
|
|
87
|
+
- **MATLAB**: `scimatlab`'s `+scidb/Log.m` delegates to this class via
|
|
88
|
+
`py.scidb.log.Log.*` (a re-export shim in scidb). The numeric level scale
|
|
89
|
+
(`DEBUG=0 … ERROR=3`) and positional call shapes are frozen for that
|
|
90
|
+
bridge.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
scistacklog/__init__.py,sha256=D45CLXYajVdTdEAJEA3D8_umBFhnVDqYW2K8mImSr1k,15584
|
|
2
|
+
scistacklog-0.1.1.dist-info/METADATA,sha256=Zlk-uq_5iCZ5-W56MvCSUBucgCNK73Q3nBV5XJqW404,3427
|
|
3
|
+
scistacklog-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
scistacklog-0.1.1.dist-info/RECORD,,
|