instruktai-python-logger 0.1.0__tar.gz → 0.1.0.post1__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 (15) hide show
  1. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/PKG-INFO +13 -2
  2. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/README.md +12 -1
  3. instruktai_python_logger-0.1.0.post1/instrukt_ai_logging/__init__.py +19 -0
  4. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instrukt_ai_logging/logging.py +118 -19
  5. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/PKG-INFO +13 -2
  6. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/pyproject.toml +1 -1
  7. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/tests/test_configure_logging.py +6 -38
  8. instruktai_python_logger-0.1.0/instrukt_ai_logging/__init__.py +0 -14
  9. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instrukt_ai_logging/cli.py +0 -0
  10. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
  11. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
  12. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
  13. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/requires.txt +0 -0
  14. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/instruktai_python_logger.egg-info/top_level.txt +0 -0
  15. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.0.post1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instruktai-python-logger
3
- Version: 0.1.0
3
+ Version: 0.1.0.post1
4
4
  Summary: Centralized logging utilities for InstruktAI Python services.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -34,9 +34,20 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
34
34
  ## API
35
35
 
36
36
  - Python entrypoint: `instrukt_ai_logging.configure_logging(...)`
37
- - Structured logging helper: `instrukt_ai_logging.log_kv(logger, level, {"msg": "...", ...})`
37
+ - Logger helper: `instrukt_ai_logging.get_logger(name)` (named `**kv` logging)
38
38
  - CLI entrypoint: `instrukt-ai-logs` (reads recent log lines)
39
39
 
40
+ Example:
41
+
42
+ ```py
43
+ import logging
44
+ from instrukt_ai_logging import configure_logging
45
+
46
+ configure_logging("teleclaude")
47
+ logger = logging.getLogger("teleclaude.core")
48
+ logger.info("job_started", job_id="abc123", user_id=123)
49
+ ```
50
+
40
51
  ## Environment variables (contract)
41
52
 
42
53
  Per-app prefix model (example uses `TELECLAUDE_`):
@@ -24,9 +24,20 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
24
24
  ## API
25
25
 
26
26
  - Python entrypoint: `instrukt_ai_logging.configure_logging(...)`
27
- - Structured logging helper: `instrukt_ai_logging.log_kv(logger, level, {"msg": "...", ...})`
27
+ - Logger helper: `instrukt_ai_logging.get_logger(name)` (named `**kv` logging)
28
28
  - CLI entrypoint: `instrukt-ai-logs` (reads recent log lines)
29
29
 
30
+ Example:
31
+
32
+ ```py
33
+ import logging
34
+ from instrukt_ai_logging import configure_logging
35
+
36
+ configure_logging("teleclaude")
37
+ logger = logging.getLogger("teleclaude.core")
38
+ logger.info("job_started", job_id="abc123", user_id=123)
39
+ ```
40
+
30
41
  ## Environment variables (contract)
31
42
 
32
43
  Per-app prefix model (example uses `TELECLAUDE_`):
@@ -0,0 +1,19 @@
1
+ """InstruktAI logging standard library.
2
+
3
+ See `README.md` for usage and `docs/design.md` for design intent.
4
+ """
5
+
6
+ __all__ = [
7
+ "__version__",
8
+ "configure_logging",
9
+ "get_logger",
10
+ ]
11
+
12
+ try:
13
+ from importlib.metadata import version as _pkg_version
14
+
15
+ __version__ = _pkg_version("instruktai-python-logger")
16
+ except Exception: # pragma: no cover
17
+ __version__ = "0.0.0"
18
+
19
+ from instrukt_ai_logging.logging import configure_logging, get_logger # noqa: E402 (intentional re-export)
@@ -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
@@ -12,6 +11,42 @@ from pathlib import Path
12
11
  from typing import Any
13
12
 
14
13
 
14
+ def _normalize_env_prefix(name: str) -> str:
15
+ # TELECLAUDE, MY_APP, etc.
16
+ raw = name.strip().upper()
17
+ if not raw:
18
+ raise ValueError("name must be non-empty")
19
+ raw = re.sub(r"[^A-Z0-9]+", "_", raw)
20
+ raw = re.sub(r"_+", "_", raw).strip("_")
21
+ if not raw:
22
+ raise ValueError("name did not produce a valid env prefix")
23
+ return raw
24
+
25
+
26
+ def _normalize_app_name(name: str) -> str:
27
+ # teleclaude, my-app, etc.
28
+ raw = name.strip().lower()
29
+ if not raw:
30
+ raise ValueError("name must be non-empty")
31
+ raw = re.sub(r"[^a-z0-9]+", "-", raw)
32
+ raw = re.sub(r"-+", "-", raw).strip("-")
33
+ if not raw:
34
+ raise ValueError("name did not produce a valid app name")
35
+ return raw
36
+
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
+
15
50
  def _level_name_to_int(level_name: str, default: int) -> int:
16
51
  name = level_name.strip().upper()
17
52
  if not name:
@@ -143,24 +178,79 @@ class LogfmtFormatter(UtcMillisFormatter):
143
178
  return " ".join(parts)
144
179
 
145
180
 
146
- def log_kv(logger: logging.Logger, level: int, data: Mapping[str, Any]) -> None:
147
- """Log a key/value event.
181
+ class InstruktLogger(logging.Logger):
182
+ """Logger that accepts arbitrary `**kv` fields.
183
+
184
+ This lets callers use normal logger methods with named key/value pairs:
185
+ logging.getLogger("...").info("event_name", job_id=..., user_id=...)
148
186
 
149
- `data` MUST include `msg` and may include any other serializable values.
187
+ All `**kv` values are serialized to text by the formatter.
150
188
  """
151
- if "msg" not in data:
152
- raise ValueError('log_kv requires a "msg" key')
153
189
 
154
- msg = str(data["msg"])
155
- kv: dict[str, object] = {}
156
- for key, value in data.items():
157
- if key == "msg":
158
- continue
159
- if not _SAFE_KEY.fullmatch(key):
160
- raise ValueError(f"Invalid key for log_kv: {key!r}")
161
- kv[key] = value
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
+ )
218
+
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)
222
+
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)
226
+
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)
230
+
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)
162
234
 
163
- logger.log(level, msg, extra={"kv": kv})
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)
238
+
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)
243
+
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)
249
+
250
+
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)
164
254
 
165
255
 
166
256
  class _ThirdPartySelectorFilter(logging.Filter):
@@ -225,10 +315,8 @@ def _ensure_log_dir(log_dir: Path) -> None:
225
315
 
226
316
 
227
317
  def configure_logging(
318
+ name: str,
228
319
  *,
229
- env_prefix: str,
230
- app_logger_prefix: str,
231
- app_name: str,
232
320
  log_filename: str | None = None,
233
321
  max_message_chars: int = 4000,
234
322
  ) -> Path:
@@ -236,12 +324,23 @@ def configure_logging(
236
324
 
237
325
  Returns the resolved log file path in use.
238
326
  """
327
+ env_prefix = _normalize_env_prefix(name)
328
+ app_logger_prefix = _normalize_logger_prefix(name)
329
+ app_name = app_logger_prefix
330
+ log_filename = log_filename or f"{app_logger_prefix}.log"
331
+
239
332
  contract = LoggingContract(
240
333
  env_prefix=env_prefix,
241
334
  app_logger_prefix=app_logger_prefix,
242
335
  app_name=app_name,
243
336
  )
244
337
 
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
+
245
344
  our_level_name = (os.getenv(contract.env_log_level) or "INFO").upper()
246
345
  third_party_level_name = (os.getenv(contract.env_third_party_level) or "WARNING").upper()
247
346
  spotlight = _parse_csv(os.getenv(contract.env_third_party_loggers, ""))
@@ -259,7 +358,7 @@ def configure_logging(
259
358
  log_dir = _fallback_log_root(app_name)
260
359
  _ensure_log_dir(log_dir)
261
360
 
262
- log_file = log_dir / (log_filename or f"{app_name}.log")
361
+ log_file = log_dir / log_filename
263
362
 
264
363
  formatter = LogfmtFormatter(max_message_chars=max_message_chars)
265
364
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instruktai-python-logger
3
- Version: 0.1.0
3
+ Version: 0.1.0.post1
4
4
  Summary: Centralized logging utilities for InstruktAI Python services.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -34,9 +34,20 @@ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
34
34
  ## API
35
35
 
36
36
  - Python entrypoint: `instrukt_ai_logging.configure_logging(...)`
37
- - Structured logging helper: `instrukt_ai_logging.log_kv(logger, level, {"msg": "...", ...})`
37
+ - Logger helper: `instrukt_ai_logging.get_logger(name)` (named `**kv` logging)
38
38
  - CLI entrypoint: `instrukt-ai-logs` (reads recent log lines)
39
39
 
40
+ Example:
41
+
42
+ ```py
43
+ import logging
44
+ from instrukt_ai_logging import configure_logging
45
+
46
+ configure_logging("teleclaude")
47
+ logger = logging.getLogger("teleclaude.core")
48
+ logger.info("job_started", job_id="abc123", user_id=123)
49
+ ```
50
+
40
51
  ## Environment variables (contract)
41
52
 
42
53
  Per-app prefix model (example uses `TELECLAUDE_`):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "instruktai-python-logger"
7
- version = "0.1.0"
7
+ version = "0.1.0.post1"
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, log_kv
7
+ from instrukt_ai_logging import configure_logging
8
8
 
9
9
 
10
10
  @pytest.fixture()
@@ -35,11 +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
- env_prefix="TELECLAUDE",
40
- app_logger_prefix="teleclaude",
41
- app_name="teleclaude",
42
- )
38
+ log_path = configure_logging("teleclaude")
43
39
 
44
40
  logging.getLogger("teleclaude.core").debug("hello from ours")
45
41
  logging.getLogger("httpcore.http11").info("hello from third-party")
@@ -57,11 +53,7 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
57
53
  monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "INFO")
58
54
  monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOGGERS", "httpcore")
59
55
 
60
- log_path = configure_logging(
61
- env_prefix="TELECLAUDE",
62
- app_logger_prefix="teleclaude",
63
- app_name="teleclaude",
64
- )
56
+ log_path = configure_logging("teleclaude")
65
57
 
66
58
  # Ensure records are actually created even though root is WARNING in spotlight mode.
67
59
  httpcore_logger = logging.getLogger("httpcore")
@@ -85,39 +77,15 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
85
77
  assert "telegram info" not in content
86
78
 
87
79
 
88
- def test_log_kv_requires_msg_key(isolated_logging, monkeypatch):
80
+ def test_named_kv_logger_emits_pairs(isolated_logging, monkeypatch):
89
81
  with TemporaryDirectory() as tmp:
90
82
  monkeypatch.setenv("INSTRUKT_AI_LOG_ROOT", tmp)
91
83
  monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
92
84
  monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
93
85
 
94
- configure_logging(
95
- env_prefix="TELECLAUDE",
96
- app_logger_prefix="teleclaude",
97
- app_name="teleclaude",
98
- )
86
+ log_path = configure_logging("teleclaude")
99
87
 
100
- with pytest.raises(ValueError):
101
- log_kv(logging.getLogger("teleclaude.core"), logging.INFO, {"session": "abc"})
102
-
103
-
104
- def test_log_kv_emits_pairs(isolated_logging, monkeypatch):
105
- with TemporaryDirectory() as tmp:
106
- monkeypatch.setenv("INSTRUKT_AI_LOG_ROOT", tmp)
107
- monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
108
- monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
109
-
110
- log_path = configure_logging(
111
- env_prefix="TELECLAUDE",
112
- app_logger_prefix="teleclaude",
113
- app_name="teleclaude",
114
- )
115
-
116
- log_kv(
117
- logging.getLogger("teleclaude.core"),
118
- logging.INFO,
119
- {"msg": "hello", "session": "abc123", "n": 1},
120
- )
88
+ logging.getLogger("teleclaude.core").info("hello", session="abc123", n=1)
121
89
 
122
90
  content = _read_text(log_path)
123
91
  assert 'msg="hello"' in content
@@ -1,14 +0,0 @@
1
- """InstruktAI logging standard library.
2
-
3
- See `README.md` for usage and `docs/design.md` for design intent.
4
- """
5
-
6
- __all__ = [
7
- "__version__",
8
- "configure_logging",
9
- "log_kv",
10
- ]
11
-
12
- __version__ = "0.0.0"
13
-
14
- from instrukt_ai_logging.logging import configure_logging, log_kv # noqa: E402 (intentional re-export)