JSONL-LOGGER 1.2.3__tar.gz → 1.2.4__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.
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.egg-info/PKG-INFO +2 -6
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.egg-info/SOURCES.txt +1 -0
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.py +38 -33
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/PKG-INFO +2 -6
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/README.md +1 -5
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/pyproject.toml +1 -1
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.egg-info/dependency_links.txt +0 -0
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.egg-info/requires.txt +0 -0
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/JSONL_LOGGER.egg-info/top_level.txt +0 -0
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/LICENSE +0 -0
- {jsonl_logger-1.2.3 → jsonl_logger-1.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: JSONL-LOGGER
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Async queue-based structured JSONL logging with thread-safe performance and auto-detected module names
|
|
5
5
|
Author-email: rocky <rocky@null.net>
|
|
6
6
|
License: MIT
|
|
@@ -59,7 +59,7 @@ log_info("Order placed", order_id=123)
|
|
|
59
59
|
1. `LOGGER_FILE_NAME` env var in .env
|
|
60
60
|
2. `os.environ["LOGGER_FILE_NAME"]` in your module
|
|
61
61
|
3. `logfile_name=` parameter in function call
|
|
62
|
-
4. Falls back to "LOGS" if none set
|
|
62
|
+
4. `Falls back` to "LOGS" if none set
|
|
63
63
|
|
|
64
64
|
- **module_name**: The Python module name. Auto-detected from caller's `__name__`. Can be overridden via `module_name=` parameter.
|
|
65
65
|
|
|
@@ -256,7 +256,3 @@ python3 -m pytest JSONL_LOGGER.py -v
|
|
|
256
256
|
| `_warn_non_primitive_fields()` | 3 | 5 | list warning, dict warning, datetime warning, primitives silent, caller module name |
|
|
257
257
|
| `ColoredFormatter.format()` | 3 | 6 | INFO green, WARNING yellow, ERROR red, METR emoji, message preserved, RESET present |
|
|
258
258
|
| `UniformLevelFormatter.format()` | 3 | 9 | valid JSON, required keys, compact separators, WARN level, METR level, extra fields, no reserved leaks, source absent/present, non-serialisable stringified |
|
|
259
|
-
|
|
260
|
-
## Version
|
|
261
|
-
|
|
262
|
-
Current: 1.0.0
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# PURPOSE: Custom logging system with JSONL file output
|
|
3
3
|
# GOAL: Provide structured logging to JSONL files
|
|
4
4
|
# RUNS TO: log_info() / log_warn() / log_error() / log_metric() / send_notification() / send_notification_async()
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
# ┌────────────────────────────────────────────────────────────────────────────────┐
|
|
7
7
|
# │ PERFORMANCE TEST RESULTS (10 RUN AVERAGE) │
|
|
8
8
|
# ├───────────────────┬──────────┬──────────┬────────────┬─────────────────────────┤
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
# 10. Module-Level Notification Log
|
|
85
85
|
# - send_notification() writes to main_logger.jsonl
|
|
86
86
|
# - WHY: Uniform JSONL format — same parser, same tooling, same retention
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
89
89
|
# SECTION MAP:
|
|
90
90
|
# SECTION 1 · CONSTANTS & CONFIG → LOGGING_CONFIG, module-level state
|
|
@@ -171,13 +171,7 @@ if pytest is not None:
|
|
|
171
171
|
assert "PROJECT_DIRECTORY" in result.stderr, (
|
|
172
172
|
f"Expected 'PROJECT_DIRECTORY' in stderr, got: {result.stderr}"
|
|
173
173
|
)
|
|
174
|
-
|
|
175
|
-
f"Expected non-zero returncode, got {result.returncode}"
|
|
176
|
-
)
|
|
177
|
-
assert "PROJECT_DIRECTORY" in result.stderr, (
|
|
178
|
-
f"Expected 'PROJECT_DIRECTORY' in stderr, got: {result.stderr}"
|
|
179
|
-
)
|
|
180
|
-
|
|
174
|
+
|
|
181
175
|
|
|
182
176
|
# ── SECTION 1 · CONSTANTS & CONFIG ───────────────────────────────────────────
|
|
183
177
|
# Goal: Define all configuration values and module-level state in one place
|
|
@@ -392,7 +386,7 @@ def _get_caller_module() -> str:
|
|
|
392
386
|
module_name = frame.f_globals.get("__name__", "unknown_module")
|
|
393
387
|
return module_name.split(".")[-1]
|
|
394
388
|
|
|
395
|
-
except Exception
|
|
389
|
+
except Exception:
|
|
396
390
|
# WHY: Exception (not bare except) — frame inspection can fail under
|
|
397
391
|
# restricted interpreters; returning a sentinel is safer than crashing.
|
|
398
392
|
return "unknown_module"
|
|
@@ -437,7 +431,7 @@ def _get_actual_source_file() -> str:
|
|
|
437
431
|
|
|
438
432
|
return Path(__file__).stem # __main__ fallback
|
|
439
433
|
|
|
440
|
-
except Exception
|
|
434
|
+
except Exception:
|
|
441
435
|
# WHY: Exception (not bare except) — frame inspection can fail under
|
|
442
436
|
# restricted interpreters; returning a sentinel is safer than crashing.
|
|
443
437
|
return "unknown_source"
|
|
@@ -510,11 +504,15 @@ def _get_timestamp() -> dict[str, str]:
|
|
|
510
504
|
local_str = local_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[
|
|
511
505
|
:-3
|
|
512
506
|
] + local_time.strftime("%z")
|
|
513
|
-
except ValueError:
|
|
514
|
-
raise
|
|
515
507
|
except Exception as e:
|
|
516
|
-
# WHY: Exception (not bare except) —
|
|
517
|
-
#
|
|
508
|
+
# WHY: Exception (not bare except) — both empty and invalid timezone
|
|
509
|
+
# names fall back to UTC with a warning, keeping logging functional.
|
|
510
|
+
# A misconfigured TZ should never crash the entire application.
|
|
511
|
+
print(
|
|
512
|
+
f"[LOGGER WARN] Invalid LOGS_LOCAL_TIMEZONE={LOGS_LOCAL_TIMEZONE!r}: {e}. "
|
|
513
|
+
"Falling back to UTC.",
|
|
514
|
+
file=sys.stderr,
|
|
515
|
+
)
|
|
518
516
|
local_str = utc_str
|
|
519
517
|
|
|
520
518
|
return {"utc": utc_str, "local": local_str}
|
|
@@ -1122,16 +1120,18 @@ if pytest is not None:
|
|
|
1122
1120
|
# Goal: Isolate retry mechanics so business functions stay free of retry loops
|
|
1123
1121
|
|
|
1124
1122
|
|
|
1125
|
-
def _with_file_retry(write_fn: Callable[[], None], log_file: str) ->
|
|
1123
|
+
def _with_file_retry(write_fn: Callable[[], None], log_file: str) -> bool:
|
|
1126
1124
|
"""[TIER 2] Attempt write_fn() up to RETRY_MAX_ATTEMPTS times with exponential backoff.
|
|
1127
1125
|
|
|
1126
|
+
Returns True on success, False if all attempts failed.
|
|
1127
|
+
|
|
1128
1128
|
WHY: Retry logic lives here — not in _flush_buffer — per RULE retry-mechanics.
|
|
1129
1129
|
Business functions must not contain retry loops.
|
|
1130
1130
|
"""
|
|
1131
1131
|
for attempt in range(RETRY_MAX_ATTEMPTS):
|
|
1132
1132
|
try:
|
|
1133
1133
|
write_fn()
|
|
1134
|
-
return
|
|
1134
|
+
return True
|
|
1135
1135
|
except Exception as e:
|
|
1136
1136
|
# WHY: Exception (not bare except) — all non-system-exit errors are
|
|
1137
1137
|
# treated as transient for file I/O per DOMAIN_RULES retry policy.
|
|
@@ -1147,7 +1147,7 @@ def _with_file_retry(write_fn: Callable[[], None], log_file: str) -> list[str] |
|
|
|
1147
1147
|
f"Failed to write logs to {log_file} after "
|
|
1148
1148
|
f"{RETRY_MAX_ATTEMPTS} attempts: {e}"
|
|
1149
1149
|
)
|
|
1150
|
-
return
|
|
1150
|
+
return False
|
|
1151
1151
|
|
|
1152
1152
|
|
|
1153
1153
|
# ── TESTS ───────────────────────────────────────────────────────────────────
|
|
@@ -1159,17 +1159,17 @@ if pytest is not None:
|
|
|
1159
1159
|
|
|
1160
1160
|
def test_with_file_retry(self) -> None:
|
|
1161
1161
|
assert pytest is not None # Type guard for pyright
|
|
1162
|
-
# ── success on first attempt returns
|
|
1162
|
+
# ── success on first attempt returns True ─────────────────────────────
|
|
1163
1163
|
calls = []
|
|
1164
1164
|
|
|
1165
1165
|
def write_fn() -> None:
|
|
1166
1166
|
calls.append(1)
|
|
1167
1167
|
|
|
1168
1168
|
result = _with_file_retry(write_fn, "test.jsonl")
|
|
1169
|
-
assert result is
|
|
1169
|
+
assert result is True, f"Expected True on success, got {result!r}"
|
|
1170
1170
|
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
|
|
1171
1171
|
|
|
1172
|
-
# ── success on third attempt (transient failures) returns
|
|
1172
|
+
# ── success on third attempt (transient failures) returns True ────────
|
|
1173
1173
|
attempt_count = [0]
|
|
1174
1174
|
|
|
1175
1175
|
def write_fn_transient() -> None:
|
|
@@ -1179,17 +1179,17 @@ if pytest is not None:
|
|
|
1179
1179
|
|
|
1180
1180
|
with patch("time.sleep"):
|
|
1181
1181
|
result = _with_file_retry(write_fn_transient, "test.jsonl")
|
|
1182
|
-
assert result is
|
|
1183
|
-
f"Expected
|
|
1182
|
+
assert result is True, (
|
|
1183
|
+
f"Expected True after transient recovery, got {result!r}"
|
|
1184
1184
|
)
|
|
1185
1185
|
assert attempt_count[0] == 3, f"Expected 3 attempts, got {attempt_count[0]}"
|
|
1186
1186
|
|
|
1187
|
-
# ── total exhaustion returns
|
|
1187
|
+
# ── total exhaustion returns False (re-buffer sentinel) ──────────
|
|
1188
1188
|
with patch("time.sleep"):
|
|
1189
1189
|
result = _with_file_retry(
|
|
1190
1190
|
lambda: (_ for _ in ()).throw(OSError("disk full")), "test.jsonl"
|
|
1191
1191
|
)
|
|
1192
|
-
assert result
|
|
1192
|
+
assert result is False, f"Expected False on exhaustion, got {result!r}"
|
|
1193
1193
|
|
|
1194
1194
|
# ── any exception type is retried up to max attempts ──────────────────
|
|
1195
1195
|
perm_count = [0]
|
|
@@ -1415,9 +1415,9 @@ def _flush_buffer(log_file: str) -> None:
|
|
|
1415
1415
|
for line in to_write:
|
|
1416
1416
|
f.write(line + "\n")
|
|
1417
1417
|
|
|
1418
|
-
|
|
1418
|
+
success = _with_file_retry(write_fn, log_file)
|
|
1419
1419
|
|
|
1420
|
-
if
|
|
1420
|
+
if not success:
|
|
1421
1421
|
with _buffer_lock:
|
|
1422
1422
|
combined = to_write + _buffers.get(log_file, [])
|
|
1423
1423
|
if len(combined) > LOGGER_MAX_BUFFER_SIZE:
|
|
@@ -1462,7 +1462,7 @@ if pytest is not None:
|
|
|
1462
1462
|
mock_open.return_value
|
|
1463
1463
|
)
|
|
1464
1464
|
mock_open.return_value.__exit__ = lambda self, *args: None
|
|
1465
|
-
with patch("JSONL_LOGGER._with_file_retry", return_value=
|
|
1465
|
+
with patch("JSONL_LOGGER._with_file_retry", return_value=True):
|
|
1466
1466
|
_flush_buffer("fake.jsonl")
|
|
1467
1467
|
assert mod._buffers.get("fake.jsonl", []) == [], (
|
|
1468
1468
|
"Buffer should be empty after successful flush"
|
|
@@ -1474,7 +1474,7 @@ if pytest is not None:
|
|
|
1474
1474
|
original = mod._buffers.copy()
|
|
1475
1475
|
mod._buffers["fake.jsonl"] = ["line1", "line2"]
|
|
1476
1476
|
try:
|
|
1477
|
-
with patch("JSONL_LOGGER._with_file_retry", return_value=
|
|
1477
|
+
with patch("JSONL_LOGGER._with_file_retry", return_value=False):
|
|
1478
1478
|
_flush_buffer("fake.jsonl")
|
|
1479
1479
|
assert len(mod._buffers.get("fake.jsonl", [])) == 2, (
|
|
1480
1480
|
"Lines should be re-buffered on retry exhaustion"
|
|
@@ -1489,7 +1489,7 @@ if pytest is not None:
|
|
|
1489
1489
|
try:
|
|
1490
1490
|
buf = io.StringIO()
|
|
1491
1491
|
with patch("sys.stderr", buf):
|
|
1492
|
-
with patch("JSONL_LOGGER._with_file_retry", return_value=
|
|
1492
|
+
with patch("JSONL_LOGGER._with_file_retry", return_value=False):
|
|
1493
1493
|
mod._buffers["fake.jsonl"] = ["a", "b", "c", "d"]
|
|
1494
1494
|
_flush_buffer("fake.jsonl")
|
|
1495
1495
|
remaining = mod._buffers.get("fake.jsonl", [])
|
|
@@ -1508,7 +1508,7 @@ if pytest is not None:
|
|
|
1508
1508
|
try:
|
|
1509
1509
|
buf = io.StringIO()
|
|
1510
1510
|
with patch("sys.stderr", buf):
|
|
1511
|
-
with patch("JSONL_LOGGER._with_file_retry", return_value=
|
|
1511
|
+
with patch("JSONL_LOGGER._with_file_retry", return_value=False):
|
|
1512
1512
|
_flush_buffer("fake.jsonl")
|
|
1513
1513
|
assert (
|
|
1514
1514
|
"dropped" in buf.getvalue().lower()
|
|
@@ -1626,6 +1626,12 @@ def _init_logger() -> logging.Logger:
|
|
|
1626
1626
|
"""
|
|
1627
1627
|
global _log_queue, _writer_thread
|
|
1628
1628
|
|
|
1629
|
+
# Guard against double-initialization — if the writer thread is already
|
|
1630
|
+
# alive, return the existing logger. Creating a second queue + thread
|
|
1631
|
+
# while the first is running would lose logs enqueued on the old queue.
|
|
1632
|
+
if _writer_thread is not None and _writer_thread.is_alive():
|
|
1633
|
+
return logging.getLogger(__name__)
|
|
1634
|
+
|
|
1629
1635
|
_logger = logging.getLogger(__name__)
|
|
1630
1636
|
_logger.setLevel(logging.DEBUG)
|
|
1631
1637
|
_logger.handlers.clear()
|
|
@@ -2205,7 +2211,7 @@ async def send_notification_async(
|
|
|
2205
2211
|
the call to a thread pool thread.
|
|
2206
2212
|
"""
|
|
2207
2213
|
source_file = _get_actual_source_file()
|
|
2208
|
-
loop = asyncio.
|
|
2214
|
+
loop = asyncio.get_running_loop()
|
|
2209
2215
|
await loop.run_in_executor(
|
|
2210
2216
|
None, send_notification, message, logfile_name, module_name, source_file
|
|
2211
2217
|
)
|
|
@@ -2413,7 +2419,6 @@ def run_performance_test(
|
|
|
2413
2419
|
|
|
2414
2420
|
elapsed_multi = time.time() - start
|
|
2415
2421
|
_flush_logs()
|
|
2416
|
-
elapsed_multi = time.time() - start
|
|
2417
2422
|
throughput_multi = total_logs / elapsed_multi
|
|
2418
2423
|
|
|
2419
2424
|
results["multi_thread"] = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: JSONL-LOGGER
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Async queue-based structured JSONL logging with thread-safe performance and auto-detected module names
|
|
5
5
|
Author-email: rocky <rocky@null.net>
|
|
6
6
|
License: MIT
|
|
@@ -59,7 +59,7 @@ log_info("Order placed", order_id=123)
|
|
|
59
59
|
1. `LOGGER_FILE_NAME` env var in .env
|
|
60
60
|
2. `os.environ["LOGGER_FILE_NAME"]` in your module
|
|
61
61
|
3. `logfile_name=` parameter in function call
|
|
62
|
-
4. Falls back to "LOGS" if none set
|
|
62
|
+
4. `Falls back` to "LOGS" if none set
|
|
63
63
|
|
|
64
64
|
- **module_name**: The Python module name. Auto-detected from caller's `__name__`. Can be overridden via `module_name=` parameter.
|
|
65
65
|
|
|
@@ -256,7 +256,3 @@ python3 -m pytest JSONL_LOGGER.py -v
|
|
|
256
256
|
| `_warn_non_primitive_fields()` | 3 | 5 | list warning, dict warning, datetime warning, primitives silent, caller module name |
|
|
257
257
|
| `ColoredFormatter.format()` | 3 | 6 | INFO green, WARNING yellow, ERROR red, METR emoji, message preserved, RESET present |
|
|
258
258
|
| `UniformLevelFormatter.format()` | 3 | 9 | valid JSON, required keys, compact separators, WARN level, METR level, extra fields, no reserved leaks, source absent/present, non-serialisable stringified |
|
|
259
|
-
|
|
260
|
-
## Version
|
|
261
|
-
|
|
262
|
-
Current: 1.0.0
|
|
@@ -32,7 +32,7 @@ log_info("Order placed", order_id=123)
|
|
|
32
32
|
1. `LOGGER_FILE_NAME` env var in .env
|
|
33
33
|
2. `os.environ["LOGGER_FILE_NAME"]` in your module
|
|
34
34
|
3. `logfile_name=` parameter in function call
|
|
35
|
-
4. Falls back to "LOGS" if none set
|
|
35
|
+
4. `Falls back` to "LOGS" if none set
|
|
36
36
|
|
|
37
37
|
- **module_name**: The Python module name. Auto-detected from caller's `__name__`. Can be overridden via `module_name=` parameter.
|
|
38
38
|
|
|
@@ -229,7 +229,3 @@ python3 -m pytest JSONL_LOGGER.py -v
|
|
|
229
229
|
| `_warn_non_primitive_fields()` | 3 | 5 | list warning, dict warning, datetime warning, primitives silent, caller module name |
|
|
230
230
|
| `ColoredFormatter.format()` | 3 | 6 | INFO green, WARNING yellow, ERROR red, METR emoji, message preserved, RESET present |
|
|
231
231
|
| `UniformLevelFormatter.format()` | 3 | 9 | valid JSON, required keys, compact separators, WARN level, METR level, extra fields, no reserved leaks, source absent/present, non-serialisable stringified |
|
|
232
|
-
|
|
233
|
-
## Version
|
|
234
|
-
|
|
235
|
-
Current: 1.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "JSONL-LOGGER"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.4"
|
|
8
8
|
description = "Async queue-based structured JSONL logging with thread-safe performance and auto-detected module names"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|