instruktai-python-logger 0.1.0.post1__tar.gz → 0.1.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.1.0.post1 → instruktai_python_logger-0.1.1}/PKG-INFO +5 -6
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/README.md +4 -5
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instrukt_ai_logging/logging.py +55 -84
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/PKG-INFO +5 -6
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/pyproject.toml +1 -1
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/tests/test_configure_logging.py +15 -5
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instrukt_ai_logging/__init__.py +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instrukt_ai_logging/cli.py +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/requires.txt +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/top_level.txt +0 -0
- {instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/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.1
|
|
4
4
|
Summary: Centralized logging utilities for InstruktAI Python services.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -40,12 +40,11 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
40
40
|
Example:
|
|
41
41
|
|
|
42
42
|
```py
|
|
43
|
-
import
|
|
44
|
-
from instrukt_ai_logging import configure_logging
|
|
43
|
+
from instrukt_ai_logging import configure_logging, get_logger
|
|
45
44
|
|
|
46
|
-
configure_logging("teleclaude")
|
|
47
|
-
logger =
|
|
48
|
-
logger.info("job_started", job_id="abc123"
|
|
45
|
+
configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
|
|
46
|
+
logger = get_logger("teleclaude.core")
|
|
47
|
+
logger.info("job_started", job_id="abc123")
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
## Environment variables (contract)
|
|
@@ -30,12 +30,11 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
30
30
|
Example:
|
|
31
31
|
|
|
32
32
|
```py
|
|
33
|
-
import
|
|
34
|
-
from instrukt_ai_logging import configure_logging
|
|
33
|
+
from instrukt_ai_logging import configure_logging, get_logger
|
|
35
34
|
|
|
36
|
-
configure_logging("teleclaude")
|
|
37
|
-
logger =
|
|
38
|
-
logger.info("job_started", job_id="abc123"
|
|
35
|
+
configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
|
|
36
|
+
logger = get_logger("teleclaude.core")
|
|
37
|
+
logger.info("job_started", job_id="abc123")
|
|
39
38
|
```
|
|
40
39
|
|
|
41
40
|
## Environment variables (contract)
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
+
from collections.abc import Mapping
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from datetime import datetime, timedelta, timezone
|
|
9
10
|
from logging.handlers import WatchedFileHandler
|
|
@@ -35,18 +36,6 @@ def _normalize_app_name(name: str) -> str:
|
|
|
35
36
|
return raw
|
|
36
37
|
|
|
37
38
|
|
|
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
|
-
|
|
50
39
|
def _level_name_to_int(level_name: str, default: int) -> int:
|
|
51
40
|
name = level_name.strip().upper()
|
|
52
41
|
if not name:
|
|
@@ -178,79 +167,60 @@ class LogfmtFormatter(UtcMillisFormatter):
|
|
|
178
167
|
return " ".join(parts)
|
|
179
168
|
|
|
180
169
|
|
|
181
|
-
|
|
182
|
-
"""
|
|
170
|
+
def _log_kv(logger: logging.Logger, level: int, msg: str, kv: Mapping[str, Any]) -> None:
|
|
171
|
+
"""Internal helper: attach `kv` pairs without forcing callers to use `extra`."""
|
|
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.
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
Callers use `KVLogger` like a normal logger:
|
|
189
|
+
logger.info("event_name", job_id=..., user_id=...)
|
|
186
190
|
|
|
187
|
-
|
|
191
|
+
This keeps call sites human-friendly while still producing strict logfmt pairs.
|
|
188
192
|
"""
|
|
189
193
|
|
|
190
|
-
def
|
|
191
|
-
|
|
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
|
-
)
|
|
194
|
+
def __init__(self, logger: logging.Logger) -> None:
|
|
195
|
+
self._logger = logger
|
|
218
196
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
197
|
+
@property
|
|
198
|
+
def name(self) -> str:
|
|
199
|
+
return self._logger.name
|
|
222
200
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
self._log_with_kv(logging.INFO, msg, args, **kwargs)
|
|
201
|
+
def debug(self, msg: str, **kv: Any) -> None:
|
|
202
|
+
_log_kv(self._logger, logging.DEBUG, msg, kv)
|
|
226
203
|
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
self._log_with_kv(logging.WARNING, msg, args, **kwargs)
|
|
204
|
+
def info(self, msg: str, **kv: Any) -> None:
|
|
205
|
+
_log_kv(self._logger, logging.INFO, msg, kv)
|
|
230
206
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
self._log_with_kv(logging.ERROR, msg, args, **kwargs)
|
|
207
|
+
def warning(self, msg: str, **kv: Any) -> None:
|
|
208
|
+
_log_kv(self._logger, logging.WARNING, msg, kv)
|
|
234
209
|
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
self._log_with_kv(logging.CRITICAL, msg, args, **kwargs)
|
|
210
|
+
def error(self, msg: str, **kv: Any) -> None:
|
|
211
|
+
_log_kv(self._logger, logging.ERROR, msg, kv)
|
|
238
212
|
|
|
239
|
-
def exception(self, msg:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
self._log_with_kv(logging.ERROR, msg, args, **kwargs)
|
|
213
|
+
def exception(self, msg: str, **kv: Any) -> None:
|
|
214
|
+
# Match logging.exception behavior (includes exc_info=True).
|
|
215
|
+
self._logger.exception(msg, extra={"kv": {k: v for k, v in kv.items() if _SAFE_KEY.fullmatch(k)}})
|
|
243
216
|
|
|
244
|
-
def log(self, level: int, msg:
|
|
245
|
-
|
|
246
|
-
raise TypeError("level must be an int")
|
|
247
|
-
if self.isEnabledFor(level):
|
|
248
|
-
self._log_with_kv(level, msg, args, **kwargs)
|
|
217
|
+
def log(self, level: int, msg: str, **kv: Any) -> None:
|
|
218
|
+
_log_kv(self._logger, level, msg, kv)
|
|
249
219
|
|
|
250
220
|
|
|
251
|
-
def get_logger(name: str) ->
|
|
252
|
-
"""
|
|
253
|
-
return logging.getLogger(name)
|
|
221
|
+
def get_logger(name: str) -> KVLogger:
|
|
222
|
+
"""Return a `KVLogger` wrapper for the named logger."""
|
|
223
|
+
return KVLogger(logging.getLogger(name))
|
|
254
224
|
|
|
255
225
|
|
|
256
226
|
class _ThirdPartySelectorFilter(logging.Filter):
|
|
@@ -315,8 +285,11 @@ def _ensure_log_dir(log_dir: Path) -> None:
|
|
|
315
285
|
|
|
316
286
|
|
|
317
287
|
def configure_logging(
|
|
318
|
-
name: str,
|
|
319
288
|
*,
|
|
289
|
+
app_logger_prefix: str,
|
|
290
|
+
name: str | None = None,
|
|
291
|
+
env_prefix: str | None = None,
|
|
292
|
+
app_name: str | None = None,
|
|
320
293
|
log_filename: str | None = None,
|
|
321
294
|
max_message_chars: int = 4000,
|
|
322
295
|
) -> Path:
|
|
@@ -324,10 +297,14 @@ def configure_logging(
|
|
|
324
297
|
|
|
325
298
|
Returns the resolved log file path in use.
|
|
326
299
|
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
300
|
+
if name is not None:
|
|
301
|
+
if env_prefix is not None or app_name is not None:
|
|
302
|
+
raise ValueError("Pass either name= OR env_prefix/app_name (not both)")
|
|
303
|
+
env_prefix = _normalize_env_prefix(name)
|
|
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=")
|
|
331
308
|
|
|
332
309
|
contract = LoggingContract(
|
|
333
310
|
env_prefix=env_prefix,
|
|
@@ -335,12 +312,6 @@ def configure_logging(
|
|
|
335
312
|
app_name=app_name,
|
|
336
313
|
)
|
|
337
314
|
|
|
338
|
-
# Ensure all loggers accept arbitrary `**kv` (no wrapper at call sites).
|
|
339
|
-
logging.setLoggerClass(InstruktLogger)
|
|
340
|
-
for obj in logging.root.manager.loggerDict.values():
|
|
341
|
-
if isinstance(obj, logging.Logger) and not isinstance(obj, InstruktLogger):
|
|
342
|
-
obj.__class__ = InstruktLogger
|
|
343
|
-
|
|
344
315
|
our_level_name = (os.getenv(contract.env_log_level) or "INFO").upper()
|
|
345
316
|
third_party_level_name = (os.getenv(contract.env_third_party_level) or "WARNING").upper()
|
|
346
317
|
spotlight = _parse_csv(os.getenv(contract.env_third_party_loggers, ""))
|
|
@@ -358,7 +329,7 @@ def configure_logging(
|
|
|
358
329
|
log_dir = _fallback_log_root(app_name)
|
|
359
330
|
_ensure_log_dir(log_dir)
|
|
360
331
|
|
|
361
|
-
log_file = log_dir / log_filename
|
|
332
|
+
log_file = log_dir / (log_filename or f"{app_name}.log")
|
|
362
333
|
|
|
363
334
|
formatter = LogfmtFormatter(max_message_chars=max_message_chars)
|
|
364
335
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: instruktai-python-logger
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Centralized logging utilities for InstruktAI Python services.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -40,12 +40,11 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
|
|
|
40
40
|
Example:
|
|
41
41
|
|
|
42
42
|
```py
|
|
43
|
-
import
|
|
44
|
-
from instrukt_ai_logging import configure_logging
|
|
43
|
+
from instrukt_ai_logging import configure_logging, get_logger
|
|
45
44
|
|
|
46
|
-
configure_logging("teleclaude")
|
|
47
|
-
logger =
|
|
48
|
-
logger.info("job_started", job_id="abc123"
|
|
45
|
+
configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
|
|
46
|
+
logger = get_logger("teleclaude.core")
|
|
47
|
+
logger.info("job_started", job_id="abc123")
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
## Environment variables (contract)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "instruktai-python-logger"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Centralized logging utilities for InstruktAI Python services."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -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, get_logger
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.fixture()
|
|
@@ -35,7 +35,10 @@ 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(
|
|
38
|
+
log_path = configure_logging(
|
|
39
|
+
app_logger_prefix="teleclaude",
|
|
40
|
+
name="teleclaude",
|
|
41
|
+
)
|
|
39
42
|
|
|
40
43
|
logging.getLogger("teleclaude.core").debug("hello from ours")
|
|
41
44
|
logging.getLogger("httpcore.http11").info("hello from third-party")
|
|
@@ -53,7 +56,10 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
|
|
|
53
56
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "INFO")
|
|
54
57
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOGGERS", "httpcore")
|
|
55
58
|
|
|
56
|
-
log_path = configure_logging(
|
|
59
|
+
log_path = configure_logging(
|
|
60
|
+
app_logger_prefix="teleclaude",
|
|
61
|
+
name="teleclaude",
|
|
62
|
+
)
|
|
57
63
|
|
|
58
64
|
# Ensure records are actually created even though root is WARNING in spotlight mode.
|
|
59
65
|
httpcore_logger = logging.getLogger("httpcore")
|
|
@@ -83,9 +89,13 @@ def test_named_kv_logger_emits_pairs(isolated_logging, monkeypatch):
|
|
|
83
89
|
monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
|
|
84
90
|
monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
|
|
85
91
|
|
|
86
|
-
log_path = configure_logging(
|
|
92
|
+
log_path = configure_logging(
|
|
93
|
+
app_logger_prefix="teleclaude",
|
|
94
|
+
name="teleclaude",
|
|
95
|
+
)
|
|
87
96
|
|
|
88
|
-
|
|
97
|
+
logger = get_logger("teleclaude.core")
|
|
98
|
+
logger.info("hello", session="abc123", n=1)
|
|
89
99
|
|
|
90
100
|
content = _read_text(log_path)
|
|
91
101
|
assert 'msg="hello"' in content
|
|
File without changes
|
{instruktai_python_logger-0.1.0.post1 → instruktai_python_logger-0.1.1}/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
|