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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JSONL-LOGGER
3
- Version: 1.2.3
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,6 +2,7 @@ JSONL_LOGGER.py
2
2
  LICENSE
3
3
  README.md
4
4
  pyproject.toml
5
+ setup.cfg
5
6
  JSONL_LOGGER.egg-info/PKG-INFO
6
7
  JSONL_LOGGER.egg-info/SOURCES.txt
7
8
  JSONL_LOGGER.egg-info/dependency_links.txt
@@ -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
- assert result.returncode != 0, (
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 as e:
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 as e:
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) — timezone lookup can fail for invalid
517
- # zone names; falling back to UTC keeps logging functional.
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) -> list[str] | None:
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 None # Success — nothing to re-buffer
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 [] # Signals caller that all attempts failed
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 None ─────────────────────────────
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 None, f"Expected None on success, got {result!r}"
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 None ────────
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 None, (
1183
- f"Expected None after transient recovery, got {result!r}"
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 empty list (re-buffer sentinel) ──────────
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 == [], f"Expected [] on exhaustion, got {result!r}"
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
- failed = _with_file_retry(write_fn, log_file)
1418
+ success = _with_file_retry(write_fn, log_file)
1419
1419
 
1420
- if failed is not None:
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=None):
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.get_event_loop()
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
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.3"
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