instruktai-python-logger 0.1.1__tar.gz → 0.1.2__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.
Files changed (14) hide show
  1. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/PKG-INFO +5 -4
  2. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/README.md +4 -3
  3. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instrukt_ai_logging/logging.py +66 -42
  4. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/PKG-INFO +5 -4
  5. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/pyproject.toml +1 -1
  6. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/tests/test_configure_logging.py +2 -3
  7. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instrukt_ai_logging/__init__.py +0 -0
  8. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instrukt_ai_logging/cli.py +0 -0
  9. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
  10. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
  11. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
  12. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/requires.txt +0 -0
  13. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/instruktai_python_logger.egg-info/top_level.txt +0 -0
  14. {instruktai_python_logger-0.1.1 → instruktai_python_logger-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instruktai-python-logger
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
- from instrukt_ai_logging import configure_logging, get_logger
43
+ import logging
44
+ from instrukt_ai_logging import configure_logging
44
45
 
45
46
  configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
46
- logger = get_logger("teleclaude.core")
47
- logger.info("job_started", job_id="abc123")
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
- from instrukt_ai_logging import configure_logging, get_logger
33
+ import logging
34
+ from instrukt_ai_logging import configure_logging
34
35
 
35
36
  configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
36
- logger = get_logger("teleclaude.core")
37
- logger.info("job_started", job_id="abc123")
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)
@@ -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
@@ -167,60 +166,79 @@ class LogfmtFormatter(UtcMillisFormatter):
167
166
  return " ".join(parts)
168
167
 
169
168
 
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
-
169
+ class InstruktLogger(logging.Logger):
170
+ """Logger that accepts arbitrary `**kv` fields.
184
171
 
185
- class KVLogger:
186
- """Ergonomic key/value logger.
172
+ This lets callers use normal logger methods with named key/value pairs:
173
+ logging.getLogger("...").info("event_name", job_id=..., user_id=...)
187
174
 
188
- Callers use `KVLogger` like a normal logger:
189
- logger.info("event_name", job_id=..., user_id=...)
190
-
191
- This keeps call sites human-friendly while still producing strict logfmt pairs.
175
+ All `**kv` values are serialized to text by the formatter.
192
176
  """
193
177
 
194
- def __init__(self, logger: logging.Logger) -> None:
195
- self._logger = logger
178
+ def _log_with_kv(self, level: int, msg: object, args: tuple[object, ...], **kwargs: Any) -> None:
179
+ exc_info = kwargs.pop("exc_info", None)
180
+ stack_info = kwargs.pop("stack_info", False)
181
+ stacklevel = kwargs.pop("stacklevel", 1)
182
+ extra = kwargs.pop("extra", None)
183
+
184
+ # Remaining kwargs are treated as `kv`.
185
+ kv: dict[str, object] = {k: v for k, v in kwargs.items() if _SAFE_KEY.fullmatch(k)}
186
+
187
+ merged_extra: dict[str, object] = {}
188
+ if isinstance(extra, dict):
189
+ merged_extra.update(extra)
190
+
191
+ existing_kv = merged_extra.get("kv")
192
+ if isinstance(existing_kv, dict):
193
+ merged_extra["kv"] = {**existing_kv, **kv}
194
+ else:
195
+ merged_extra["kv"] = kv
196
+
197
+ super()._log(
198
+ level,
199
+ msg,
200
+ args,
201
+ exc_info=exc_info,
202
+ extra=merged_extra,
203
+ stack_info=stack_info,
204
+ stacklevel=stacklevel,
205
+ )
196
206
 
197
- @property
198
- def name(self) -> str:
199
- return self._logger.name
207
+ def debug(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
208
+ if self.isEnabledFor(logging.DEBUG):
209
+ self._log_with_kv(logging.DEBUG, msg, args, **kwargs)
200
210
 
201
- def debug(self, msg: str, **kv: Any) -> None:
202
- _log_kv(self._logger, logging.DEBUG, msg, kv)
211
+ def info(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
212
+ if self.isEnabledFor(logging.INFO):
213
+ self._log_with_kv(logging.INFO, msg, args, **kwargs)
203
214
 
204
- def info(self, msg: str, **kv: Any) -> None:
205
- _log_kv(self._logger, logging.INFO, msg, kv)
215
+ def warning(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
216
+ if self.isEnabledFor(logging.WARNING):
217
+ self._log_with_kv(logging.WARNING, msg, args, **kwargs)
206
218
 
207
- def warning(self, msg: str, **kv: Any) -> None:
208
- _log_kv(self._logger, logging.WARNING, msg, kv)
219
+ def error(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
220
+ if self.isEnabledFor(logging.ERROR):
221
+ self._log_with_kv(logging.ERROR, msg, args, **kwargs)
209
222
 
210
- def error(self, msg: str, **kv: Any) -> None:
211
- _log_kv(self._logger, logging.ERROR, msg, kv)
223
+ def critical(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
224
+ if self.isEnabledFor(logging.CRITICAL):
225
+ self._log_with_kv(logging.CRITICAL, msg, args, **kwargs)
212
226
 
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)}})
227
+ def exception(self, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
228
+ kwargs.setdefault("exc_info", True)
229
+ if self.isEnabledFor(logging.ERROR):
230
+ self._log_with_kv(logging.ERROR, msg, args, **kwargs)
216
231
 
217
- def log(self, level: int, msg: str, **kv: Any) -> None:
218
- _log_kv(self._logger, level, msg, kv)
232
+ def log(self, level: int, msg: object, *args: object, **kwargs: Any) -> None: # type: ignore[override]
233
+ if not isinstance(level, int):
234
+ raise TypeError("level must be an int")
235
+ if self.isEnabledFor(level):
236
+ self._log_with_kv(level, msg, args, **kwargs)
219
237
 
220
238
 
221
- def get_logger(name: str) -> KVLogger:
222
- """Return a `KVLogger` wrapper for the named logger."""
223
- return KVLogger(logging.getLogger(name))
239
+ def get_logger(name: str) -> logging.Logger:
240
+ """Compatibility helper (returns a normal logger, but with `**kv` support after configuration)."""
241
+ return logging.getLogger(name)
224
242
 
225
243
 
226
244
  class _ThirdPartySelectorFilter(logging.Filter):
@@ -312,6 +330,12 @@ def configure_logging(
312
330
  app_name=app_name,
313
331
  )
314
332
 
333
+ # Ensure all loggers accept arbitrary `**kv` (no wrapper at call sites).
334
+ logging.setLoggerClass(InstruktLogger)
335
+ for obj in logging.root.manager.loggerDict.values():
336
+ if isinstance(obj, logging.Logger) and not isinstance(obj, InstruktLogger):
337
+ obj.__class__ = InstruktLogger
338
+
315
339
  our_level_name = (os.getenv(contract.env_log_level) or "INFO").upper()
316
340
  third_party_level_name = (os.getenv(contract.env_third_party_level) or "WARNING").upper()
317
341
  spotlight = _parse_csv(os.getenv(contract.env_third_party_loggers, ""))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instruktai-python-logger
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
- from instrukt_ai_logging import configure_logging, get_logger
43
+ import logging
44
+ from instrukt_ai_logging import configure_logging
44
45
 
45
46
  configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
46
- logger = get_logger("teleclaude.core")
47
- logger.info("job_started", job_id="abc123")
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "instruktai-python-logger"
7
- version = "0.1.1"
7
+ version = "0.1.2"
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, get_logger
7
+ from instrukt_ai_logging import configure_logging
8
8
 
9
9
 
10
10
  @pytest.fixture()
@@ -94,8 +94,7 @@ def test_named_kv_logger_emits_pairs(isolated_logging, monkeypatch):
94
94
  name="teleclaude",
95
95
  )
96
96
 
97
- logger = get_logger("teleclaude.core")
98
- logger.info("hello", session="abc123", n=1)
97
+ logging.getLogger("teleclaude.core").info("hello", session="abc123", n=1)
99
98
 
100
99
  content = _read_text(log_path)
101
100
  assert 'msg="hello"' in content