instruktai-python-logger 0.5.0__tar.gz → 0.5.1__tar.gz
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.
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/PKG-INFO +1 -1
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/logging.py +48 -3
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instruktai_python_logger.egg-info/PKG-INFO +1 -1
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/pyproject.toml +1 -1
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_log_aggregation.py +56 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/README.md +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/__init__.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/cli.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/install.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/py.typed +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instruktai_python_logger.egg-info/top_level.txt +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/setup.cfg +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_cli_follow.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_configure_logging.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_install.py +0 -0
- {instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_trace_logging.py +0 -0
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/logging.py
RENAMED
|
@@ -548,6 +548,46 @@ def iter_recent_log_lines(log_file: Path, since: timedelta) -> list[str]:
|
|
|
548
548
|
return lines
|
|
549
549
|
|
|
550
550
|
|
|
551
|
+
# Initial backward-probe size for locating the start of the --since window in a
|
|
552
|
+
# large file. A typical multi-minute window fits in one or two probes; the probe
|
|
553
|
+
# doubles when the window is larger, so worst case is still O(window), not O(file).
|
|
554
|
+
_WINDOW_PROBE_BYTES = 262144 # 256 KiB
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _window_start_offset(path: Path, cutoff: datetime) -> int:
|
|
558
|
+
"""Return a byte offset at or before the first line newer than `cutoff`.
|
|
559
|
+
|
|
560
|
+
A log file is appended in timestamp order, so the window's start can be found
|
|
561
|
+
by probing backward from EOF in exponentially growing chunks instead of
|
|
562
|
+
scanning from byte 0: once a probe's earliest timestamped line is already
|
|
563
|
+
older than the cutoff, the window begins within that probe and everything
|
|
564
|
+
before it is skippable. Returns 0 when the window spans the whole file (or the
|
|
565
|
+
file is smaller than a single probe), which reproduces a full forward scan.
|
|
566
|
+
"""
|
|
567
|
+
try:
|
|
568
|
+
size = path.stat().st_size
|
|
569
|
+
except OSError:
|
|
570
|
+
return 0
|
|
571
|
+
if size <= _WINDOW_PROBE_BYTES:
|
|
572
|
+
return 0
|
|
573
|
+
|
|
574
|
+
probe = _WINDOW_PROBE_BYTES
|
|
575
|
+
with path.open("rb") as fb:
|
|
576
|
+
while probe < size:
|
|
577
|
+
start = size - probe
|
|
578
|
+
fb.seek(start)
|
|
579
|
+
fb.readline() # discard the partial line spanning the probe boundary
|
|
580
|
+
first_ts: datetime | None = None
|
|
581
|
+
for raw in fb:
|
|
582
|
+
first_ts = parse_log_timestamp(raw.decode("utf-8", errors="replace"))
|
|
583
|
+
if first_ts is not None:
|
|
584
|
+
break
|
|
585
|
+
if first_ts is not None and first_ts < cutoff:
|
|
586
|
+
return start
|
|
587
|
+
probe *= 2
|
|
588
|
+
return 0
|
|
589
|
+
|
|
590
|
+
|
|
551
591
|
def iter_recent_log_lines_merged(files: Iterable[Path], since: timedelta) -> Iterator[str]:
|
|
552
592
|
"""Yield lines newer than now-`since` merged across multiple files by timestamp.
|
|
553
593
|
|
|
@@ -567,12 +607,17 @@ def iter_recent_log_lines_merged(files: Iterable[Path], since: timedelta) -> Ite
|
|
|
567
607
|
|
|
568
608
|
def _file_stream(path: Path) -> Iterator[tuple[datetime, str]]:
|
|
569
609
|
try:
|
|
570
|
-
|
|
610
|
+
fb = path.open("rb")
|
|
571
611
|
except FileNotFoundError:
|
|
572
612
|
return
|
|
573
613
|
try:
|
|
614
|
+
offset = _window_start_offset(path, cutoff)
|
|
615
|
+
if offset:
|
|
616
|
+
fb.seek(offset)
|
|
617
|
+
fb.readline() # discard the partial line at the seek boundary
|
|
574
618
|
last_ts: datetime | None = None
|
|
575
|
-
for
|
|
619
|
+
for raw in fb:
|
|
620
|
+
line = raw.decode("utf-8", errors="replace")
|
|
576
621
|
ts = parse_log_timestamp(line)
|
|
577
622
|
if ts is None:
|
|
578
623
|
if last_ts is None or last_ts < cutoff:
|
|
@@ -584,7 +629,7 @@ def iter_recent_log_lines_merged(files: Iterable[Path], since: timedelta) -> Ite
|
|
|
584
629
|
continue
|
|
585
630
|
yield (ts, line)
|
|
586
631
|
finally:
|
|
587
|
-
|
|
632
|
+
fb.close()
|
|
588
633
|
|
|
589
634
|
eligible: list[Path] = []
|
|
590
635
|
for path in files:
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_log_aggregation.py
RENAMED
|
@@ -34,6 +34,9 @@ _MSG_GIT_ERROR = "git-error"
|
|
|
34
34
|
_MSG_INFO = "info-line"
|
|
35
35
|
_MSG_CRON = "cron-line"
|
|
36
36
|
_MSG_DAEMON = "daemon-line"
|
|
37
|
+
_MSG_OLD_PREFIX = "msg=old-"
|
|
38
|
+
_MSG_LINE_PREFIX = "msg=line-"
|
|
39
|
+
_LARGE_LINE_COUNT = 20000
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
@pytest.fixture()
|
|
@@ -176,6 +179,59 @@ def test_iter_recent_log_lines_merged_keeps_continuation_lines_with_parent(
|
|
|
176
179
|
assert _MSG_AFTER in lines[3]
|
|
177
180
|
|
|
178
181
|
|
|
182
|
+
def test_iter_recent_log_lines_merged_reads_tail_window_of_large_file(
|
|
183
|
+
app_log_dir: Path,
|
|
184
|
+
) -> None:
|
|
185
|
+
# A large body of old lines (well past the 256 KiB backward probe) followed by
|
|
186
|
+
# a recent window that includes a multi-line entry. The reader must return only
|
|
187
|
+
# the recent lines — proving it locates the window near EOF rather than scanning
|
|
188
|
+
# the whole file — and must keep the continuation line with its parent across
|
|
189
|
+
# the seek boundary.
|
|
190
|
+
daemon = app_log_dir / "demo-app.log"
|
|
191
|
+
old_block = "".join(
|
|
192
|
+
f"{_now_iso(-30)} level=INFO logger=demo.daemon {_MSG_OLD_PREFIX}{i}\n" for i in range(_LARGE_LINE_COUNT)
|
|
193
|
+
)
|
|
194
|
+
recent_block = (
|
|
195
|
+
f"{_now_iso(-1)} level=ERROR logger=demo.daemon msg={_MSG_BOOM}\n"
|
|
196
|
+
f"{_TRACEBACK_LINE}\n"
|
|
197
|
+
f"{_now_iso(0)} level=INFO logger=demo.daemon msg={_MSG_AFTER}\n"
|
|
198
|
+
)
|
|
199
|
+
daemon.write_text(old_block + recent_block, encoding="utf-8")
|
|
200
|
+
assert daemon.stat().st_size > 262144 # forces the backward-probe path (offset > 0)
|
|
201
|
+
|
|
202
|
+
files = resolve_log_files("demo-app")
|
|
203
|
+
lines = list(iter_recent_log_lines_merged(files, since=timedelta(minutes=5)))
|
|
204
|
+
assert len(lines) == 3
|
|
205
|
+
assert _MSG_BOOM in lines[0]
|
|
206
|
+
assert lines[1].startswith(_TRACEBACK_LINE)
|
|
207
|
+
assert _MSG_AFTER in lines[2]
|
|
208
|
+
assert all(_MSG_OLD_PREFIX not in line for line in lines)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_iter_recent_log_lines_merged_returns_whole_large_file_within_window(
|
|
212
|
+
app_log_dir: Path,
|
|
213
|
+
) -> None:
|
|
214
|
+
# Every line is inside the window but the file exceeds one probe: the backward
|
|
215
|
+
# probe must grow to the file start (offset 0) and return all lines, not just
|
|
216
|
+
# the final probe's worth.
|
|
217
|
+
daemon = app_log_dir / "demo-app.log"
|
|
218
|
+
daemon.write_text(
|
|
219
|
+
"".join(
|
|
220
|
+
f"{_now_iso(-1)} level=INFO logger=demo.daemon {_MSG_LINE_PREFIX}{i}\n" for i in range(_LARGE_LINE_COUNT)
|
|
221
|
+
),
|
|
222
|
+
encoding="utf-8",
|
|
223
|
+
)
|
|
224
|
+
assert daemon.stat().st_size > 262144
|
|
225
|
+
|
|
226
|
+
files = resolve_log_files("demo-app")
|
|
227
|
+
lines = list(iter_recent_log_lines_merged(files, since=timedelta(minutes=5)))
|
|
228
|
+
assert len(lines) == _LARGE_LINE_COUNT
|
|
229
|
+
first_marker = f"{_MSG_LINE_PREFIX}0"
|
|
230
|
+
last_marker = f"{_MSG_LINE_PREFIX}{_LARGE_LINE_COUNT - 1}"
|
|
231
|
+
assert first_marker in lines[0]
|
|
232
|
+
assert last_marker in lines[-1]
|
|
233
|
+
|
|
234
|
+
|
|
179
235
|
def test_cli_exclude_drops_matching_lines(
|
|
180
236
|
app_log_dir: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
181
237
|
) -> None:
|
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/__init__.py
RENAMED
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/cli.py
RENAMED
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/install.py
RENAMED
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/instrukt_ai_logging/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_configure_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{instruktai_python_logger-0.5.0 → instruktai_python_logger-0.5.1}/tests/test_trace_logging.py
RENAMED
|
File without changes
|