instruktai-python-logger 0.1.1__tar.gz → 0.1.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.
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/PKG-INFO +6 -5
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/README.md +5 -4
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/logging.py +85 -53
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/PKG-INFO +6 -5
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/pyproject.toml +1 -1
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/tests/test_configure_logging.py +5 -15
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/__init__.py +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/cli.py +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/requires.txt +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instruktai_python_logger.egg-info/top_level.txt +0 -0
- {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: instruktai-python-logger
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Centralized logging utilities for InstruktAI Python services.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -40,11 +40,12 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
40
40
|
Example:
|
|
41
41
|
|
|
42
42
|
```py
|
|
43
|
-
|
|
43
|
+
import logging
|
|
44
|
+
from instrukt_ai_logging import configure_logging
|
|
44
45
|
|
|
45
|
-
configure_logging(
|
|
46
|
-
logger =
|
|
47
|
-
logger.info("job_started", job_id="abc123")
|
|
46
|
+
configure_logging("teleclaude")
|
|
47
|
+
logger = logging.getLogger("teleclaude.core")
|
|
48
|
+
logger.info("job_started", job_id="abc123", user_id=123)
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
## Environment variables (contract)
|
|
@@ -30,11 +30,12 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
30
30
|
Example:
|
|
31
31
|
|
|
32
32
|
```py
|
|
33
|
-
|
|
33
|
+
import logging
|
|
34
|
+
from instrukt_ai_logging import configure_logging
|
|
34
35
|
|
|
35
|
-
configure_logging(
|
|
36
|
-
logger =
|
|
37
|
-
logger.info("job_started", job_id="abc123")
|
|
36
|
+
configure_logging("teleclaude")
|
|
37
|
+
logger = logging.getLogger("teleclaude.core")
|
|
38
|
+
logger.info("job_started", job_id="abc123", user_id=123)
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
## Environment variables (contract)
|
{instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/logging.py
RENAMED
|
@@ -4,7 +4,6 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
-
from collections.abc import Mapping
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
from datetime import datetime, timedelta, timezone
|
|
10
9
|
from logging.handlers import WatchedFileHandler
|
|
@@ -36,6 +35,18 @@ def _normalize_app_name(name: str) -> str:
|
|
|
36
35
|
return raw
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
def _normalize_logger_prefix(name: str) -> str:
|
|
39
|
+
# teleclaude, crypto_ai, etc. (match Python package style)
|
|
40
|
+
raw = name.strip().lower()
|
|
41
|
+
if not raw:
|
|
42
|
+
raise ValueError("name must be non-empty")
|
|
43
|
+
raw = re.sub(r"[^a-z0-9]+", "_", raw)
|
|
44
|
+
raw = re.sub(r"_+", "_", raw).strip("_")
|
|
45
|
+
if not raw:
|
|
46
|
+
raise ValueError("name did not produce a valid logger prefix")
|
|
47
|
+
return raw
|
|
48
|
+
|
|
49
|
+
|
|
39
50
|
def _level_name_to_int(level_name: str, default: int) -> int:
|
|
40
51
|
name = level_name.strip().upper()
|
|
41
52
|
if not name:
|
|
@@ -167,60 +178,79 @@ class LogfmtFormatter(UtcMillisFormatter):
|
|
|
167
178
|
return " ".join(parts)
|
|
168
179
|
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
"""
|
|
172
|
-
safe: dict[str, object] = {}
|
|
173
|
-
for key, value in kv.items():
|
|
174
|
-
if not isinstance(key, str):
|
|
175
|
-
continue
|
|
176
|
-
if key == "msg":
|
|
177
|
-
continue
|
|
178
|
-
if not _SAFE_KEY.fullmatch(key):
|
|
179
|
-
continue
|
|
180
|
-
safe[key] = value
|
|
181
|
-
|
|
182
|
-
logger.log(level, msg, extra={"kv": safe})
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class KVLogger:
|
|
186
|
-
"""Ergonomic key/value logger.
|
|
181
|
+
class InstruktLogger(logging.Logger):
|
|
182
|
+
"""Logger that accepts arbitrary `**kv` fields.
|
|
187
183
|
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
This lets callers use normal logger methods with named key/value pairs:
|
|
185
|
+
logging.getLogger("...").info("event_name", job_id=..., user_id=...)
|
|
190
186
|
|
|
191
|
-
|
|
187
|
+
All `**kv` values are serialized to text by the formatter.
|
|
192
188
|
"""
|
|
193
189
|
|
|
194
|
-
def
|
|
195
|
-
|
|
190
|
+
def _log_with_kv(self, level: int, msg: object, args: tuple[object, ...], **kwargs: Any) -> None:
|
|
191
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
192
|
+
stack_info = kwargs.pop("stack_info", False)
|
|
193
|
+
stacklevel = kwargs.pop("stacklevel", 1)
|
|
194
|
+
extra = kwargs.pop("extra", None)
|
|
195
|
+
|
|
196
|
+
# Remaining kwargs are treated as `kv`.
|
|
197
|
+
kv: dict[str, object] = {k: v for k, v in kwargs.items() if _SAFE_KEY.fullmatch(k)}
|
|
198
|
+
|
|
199
|
+
merged_extra: dict[str, object] = {}
|
|
200
|
+
if isinstance(extra, dict):
|
|
201
|
+
merged_extra.update(extra)
|
|
202
|
+
|
|
203
|
+
existing_kv = merged_extra.get("kv")
|
|
204
|
+
if isinstance(existing_kv, dict):
|
|
205
|
+
merged_extra["kv"] = {**existing_kv, **kv}
|
|
206
|
+
else:
|
|
207
|
+
merged_extra["kv"] = kv
|
|
208
|
+
|
|
209
|
+
super()._log(
|
|
210
|
+
level,
|
|
211
|
+
msg,
|
|
212
|
+
args,
|
|
213
|
+
exc_info=exc_info,
|
|
214
|
+
extra=merged_extra,
|
|
215
|
+
stack_info=stack_info,
|
|
216
|
+
stacklevel=stacklevel,
|
|
217
|
+
)
|
|
196
218
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
def debug(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
220
|
+
if self.isEnabledFor(logging.DEBUG):
|
|
221
|
+
self._log_with_kv(logging.DEBUG, msg, args, **kwargs)
|
|
200
222
|
|
|
201
|
-
def
|
|
202
|
-
|
|
223
|
+
def info(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
224
|
+
if self.isEnabledFor(logging.INFO):
|
|
225
|
+
self._log_with_kv(logging.INFO, msg, args, **kwargs)
|
|
203
226
|
|
|
204
|
-
def
|
|
205
|
-
|
|
227
|
+
def warning(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
228
|
+
if self.isEnabledFor(logging.WARNING):
|
|
229
|
+
self._log_with_kv(logging.WARNING, msg, args, **kwargs)
|
|
206
230
|
|
|
207
|
-
def
|
|
208
|
-
|
|
231
|
+
def error(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
232
|
+
if self.isEnabledFor(logging.ERROR):
|
|
233
|
+
self._log_with_kv(logging.ERROR, msg, args, **kwargs)
|
|
209
234
|
|
|
210
|
-
def
|
|
211
|
-
|
|
235
|
+
def critical(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
236
|
+
if self.isEnabledFor(logging.CRITICAL):
|
|
237
|
+
self._log_with_kv(logging.CRITICAL, msg, args, **kwargs)
|
|
212
238
|
|
|
213
|
-
def exception(self, msg:
|
|
214
|
-
|
|
215
|
-
self.
|
|
239
|
+
def exception(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
240
|
+
kwargs.setdefault("exc_info", True)
|
|
241
|
+
if self.isEnabledFor(logging.ERROR):
|
|
242
|
+
self._log_with_kv(logging.ERROR, msg, args, **kwargs)
|
|
216
243
|
|
|
217
|
-
def log(self, level: int, msg:
|
|
218
|
-
|
|
244
|
+
def log(self, level: int, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
|
|
245
|
+
if not isinstance(level, int):
|
|
246
|
+
raise TypeError("level must be an int")
|
|
247
|
+
if self.isEnabledFor(level):
|
|
248
|
+
self._log_with_kv(level, msg, args, **kwargs)
|
|
219
249
|
|
|
220
250
|
|
|
221
|
-
def get_logger(name: str) ->
|
|
222
|
-
"""
|
|
223
|
-
return
|
|
251
|
+
def get_logger(name: str) -> logging.Logger:
|
|
252
|
+
"""Compatibility helper (returns a normal logger, but with `**kv` support after configuration)."""
|
|
253
|
+
return logging.getLogger(name)
|
|
224
254
|
|
|
225
255
|
|
|
226
256
|
class _ThirdPartySelectorFilter(logging.Filter):
|
|
@@ -285,9 +315,9 @@ def _ensure_log_dir(log_dir: Path) -> None:
|
|
|
285
315
|
|
|
286
316
|
|
|
287
317
|
def configure_logging(
|
|
318
|
+
name: str,
|
|
288
319
|
*,
|
|
289
|
-
app_logger_prefix: str,
|
|
290
|
-
name: str | None = None,
|
|
320
|
+
app_logger_prefix: str | None = None,
|
|
291
321
|
env_prefix: str | None = None,
|
|
292
322
|
app_name: str | None = None,
|
|
293
323
|
log_filename: str | None = None,
|
|
@@ -297,14 +327,10 @@ def configure_logging(
|
|
|
297
327
|
|
|
298
328
|
Returns the resolved log file path in use.
|
|
299
329
|
"""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
app_name = _normalize_app_name(name)
|
|
305
|
-
|
|
306
|
-
if env_prefix is None or app_name is None:
|
|
307
|
-
raise ValueError("Missing required config: name= OR both env_prefix= and app_name=")
|
|
330
|
+
env_prefix = env_prefix or _normalize_env_prefix(name)
|
|
331
|
+
app_logger_prefix = app_logger_prefix or _normalize_logger_prefix(name)
|
|
332
|
+
app_name = app_name or app_logger_prefix
|
|
333
|
+
log_filename = log_filename or f"{app_logger_prefix}.log"
|
|
308
334
|
|
|
309
335
|
contract = LoggingContract(
|
|
310
336
|
env_prefix=env_prefix,
|
|
@@ -312,6 +338,12 @@ def configure_logging(
|
|
|
312
338
|
app_name=app_name,
|
|
313
339
|
)
|
|
314
340
|
|
|
341
|
+
# Ensure all loggers accept arbitrary `**kv` (no wrapper at call sites).
|
|
342
|
+
logging.setLoggerClass(InstruktLogger)
|
|
343
|
+
for obj in logging.root.manager.loggerDict.values():
|
|
344
|
+
if isinstance(obj, logging.Logger) and not isinstance(obj, InstruktLogger):
|
|
345
|
+
obj.__class__ = InstruktLogger
|
|
346
|
+
|
|
315
347
|
our_level_name = (os.getenv(contract.env_log_level) or "INFO").upper()
|
|
316
348
|
third_party_level_name = (os.getenv(contract.env_third_party_level) or "WARNING").upper()
|
|
317
349
|
spotlight = _parse_csv(os.getenv(contract.env_third_party_loggers, ""))
|
|
@@ -329,7 +361,7 @@ def configure_logging(
|
|
|
329
361
|
log_dir = _fallback_log_root(app_name)
|
|
330
362
|
_ensure_log_dir(log_dir)
|
|
331
363
|
|
|
332
|
-
log_file = log_dir /
|
|
364
|
+
log_file = log_dir / log_filename
|
|
333
365
|
|
|
334
366
|
formatter = LogfmtFormatter(max_message_chars=max_message_chars)
|
|
335
367
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: instruktai-python-logger
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Centralized logging utilities for InstruktAI Python services.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -40,11 +40,12 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
40
40
|
Example:
|
|
41
41
|
|
|
42
42
|
```py
|
|
43
|
-
|
|
43
|
+
import logging
|
|
44
|
+
from instrukt_ai_logging import configure_logging
|
|
44
45
|
|
|
45
|
-
configure_logging(
|
|
46
|
-
logger =
|
|
47
|
-
logger.info("job_started", job_id="abc123")
|
|
46
|
+
configure_logging("teleclaude")
|
|
47
|
+
logger = logging.getLogger("teleclaude.core")
|
|
48
|
+
logger.info("job_started", job_id="abc123", user_id=123)
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
## Environment variables (contract)
|
{instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/tests/test_configure_logging.py
RENAMED
|
@@ -4,7 +4,7 @@ from tempfile import TemporaryDirectory
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from instrukt_ai_logging import configure_logging
|
|
7
|
+
from instrukt_ai_logging import configure_logging
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.fixture()
|
|
@@ -35,10 +35,7 @@ def test_our_logs_respect_app_level_and_third_party_baseline(isolated_logging, m
|
|
|
35
35
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
|
|
36
36
|
monkeypatch.delenv("TELECLAUDE_THIRD_PARTY_LOGGERS", raising=False)
|
|
37
37
|
|
|
38
|
-
log_path = configure_logging(
|
|
39
|
-
app_logger_prefix="teleclaude",
|
|
40
|
-
name="teleclaude",
|
|
41
|
-
)
|
|
38
|
+
log_path = configure_logging("teleclaude")
|
|
42
39
|
|
|
43
40
|
logging.getLogger("teleclaude.core").debug("hello from ours")
|
|
44
41
|
logging.getLogger("httpcore.http11").info("hello from third-party")
|
|
@@ -56,10 +53,7 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
|
|
|
56
53
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "INFO")
|
|
57
54
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOGGERS", "httpcore")
|
|
58
55
|
|
|
59
|
-
log_path = configure_logging(
|
|
60
|
-
app_logger_prefix="teleclaude",
|
|
61
|
-
name="teleclaude",
|
|
62
|
-
)
|
|
56
|
+
log_path = configure_logging("teleclaude")
|
|
63
57
|
|
|
64
58
|
# Ensure records are actually created even though root is WARNING in spotlight mode.
|
|
65
59
|
httpcore_logger = logging.getLogger("httpcore")
|
|
@@ -89,13 +83,9 @@ def test_named_kv_logger_emits_pairs(isolated_logging, monkeypatch):
|
|
|
89
83
|
monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
|
|
90
84
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
|
|
91
85
|
|
|
92
|
-
log_path = configure_logging(
|
|
93
|
-
app_logger_prefix="teleclaude",
|
|
94
|
-
name="teleclaude",
|
|
95
|
-
)
|
|
86
|
+
log_path = configure_logging("teleclaude")
|
|
96
87
|
|
|
97
|
-
|
|
98
|
-
logger.info("hello", session="abc123", n=1)
|
|
88
|
+
logging.getLogger("teleclaude.core").info("hello", session="abc123", n=1)
|
|
99
89
|
|
|
100
90
|
content = _read_text(log_path)
|
|
101
91
|
assert 'msg="hello"' in content
|
{instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/__init__.py
RENAMED
|
File without changes
|
{instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.4}/instrukt_ai_logging/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|