instruktai-python-logger 0.1.0__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.
Files changed (15) hide show
  1. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/PKG-INFO +12 -2
  2. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/README.md +11 -1
  3. instruktai_python_logger-0.1.1/instrukt_ai_logging/__init__.py +19 -0
  4. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instrukt_ai_logging/logging.py +86 -16
  5. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/PKG-INFO +12 -2
  6. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/pyproject.toml +1 -1
  7. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/tests/test_configure_logging.py +7 -29
  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.1}/instrukt_ai_logging/cli.py +0 -0
  10. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
  11. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
  12. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
  13. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/requires.txt +0 -0
  14. {instruktai_python_logger-0.1.0 → instruktai_python_logger-0.1.1}/instruktai_python_logger.egg-info/top_level.txt +0 -0
  15. {instruktai_python_logger-0.1.0 → 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.0
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
@@ -34,9 +34,19 @@ 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
+ from instrukt_ai_logging import configure_logging, get_logger
44
+
45
+ configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
46
+ logger = get_logger("teleclaude.core")
47
+ logger.info("job_started", job_id="abc123")
48
+ ```
49
+
40
50
  ## Environment variables (contract)
41
51
 
42
52
  Per-app prefix model (example uses `TELECLAUDE_`):
@@ -24,9 +24,19 @@ 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
+ from instrukt_ai_logging import configure_logging, get_logger
34
+
35
+ configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
36
+ logger = get_logger("teleclaude.core")
37
+ logger.info("job_started", job_id="abc123")
38
+ ```
39
+
30
40
  ## Environment variables (contract)
31
41
 
32
42
  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)
@@ -12,6 +12,30 @@ from pathlib import Path
12
12
  from typing import Any
13
13
 
14
14
 
15
+ def _normalize_env_prefix(name: str) -> str:
16
+ # TELECLAUDE, MY_APP, etc.
17
+ raw = name.strip().upper()
18
+ if not raw:
19
+ raise ValueError("name must be non-empty")
20
+ raw = re.sub(r"[^A-Z0-9]+", "_", raw)
21
+ raw = re.sub(r"_+", "_", raw).strip("_")
22
+ if not raw:
23
+ raise ValueError("name did not produce a valid env prefix")
24
+ return raw
25
+
26
+
27
+ def _normalize_app_name(name: str) -> str:
28
+ # teleclaude, my-app, etc.
29
+ raw = name.strip().lower()
30
+ if not raw:
31
+ raise ValueError("name must be non-empty")
32
+ raw = re.sub(r"[^a-z0-9]+", "-", raw)
33
+ raw = re.sub(r"-+", "-", raw).strip("-")
34
+ if not raw:
35
+ raise ValueError("name did not produce a valid app name")
36
+ return raw
37
+
38
+
15
39
  def _level_name_to_int(level_name: str, default: int) -> int:
16
40
  name = level_name.strip().upper()
17
41
  if not name:
@@ -143,24 +167,60 @@ class LogfmtFormatter(UtcMillisFormatter):
143
167
  return " ".join(parts)
144
168
 
145
169
 
146
- def log_kv(logger: logging.Logger, level: int, data: Mapping[str, Any]) -> None:
147
- """Log a key/value event.
148
-
149
- `data` MUST include `msg` and may include any other serializable values.
150
- """
151
- if "msg" not in data:
152
- raise ValueError('log_kv requires a "msg" key')
153
-
154
- msg = str(data["msg"])
155
- kv: dict[str, object] = {}
156
- for key, value in data.items():
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
157
176
  if key == "msg":
158
177
  continue
159
178
  if not _SAFE_KEY.fullmatch(key):
160
- raise ValueError(f"Invalid key for log_kv: {key!r}")
161
- kv[key] = value
179
+ continue
180
+ safe[key] = value
181
+
182
+ logger.log(level, msg, extra={"kv": safe})
162
183
 
163
- logger.log(level, msg, extra={"kv": kv})
184
+
185
+ class KVLogger:
186
+ """Ergonomic key/value logger.
187
+
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.
192
+ """
193
+
194
+ def __init__(self, logger: logging.Logger) -> None:
195
+ self._logger = logger
196
+
197
+ @property
198
+ def name(self) -> str:
199
+ return self._logger.name
200
+
201
+ def debug(self, msg: str, **kv: Any) -> None:
202
+ _log_kv(self._logger, logging.DEBUG, msg, kv)
203
+
204
+ def info(self, msg: str, **kv: Any) -> None:
205
+ _log_kv(self._logger, logging.INFO, msg, kv)
206
+
207
+ def warning(self, msg: str, **kv: Any) -> None:
208
+ _log_kv(self._logger, logging.WARNING, msg, kv)
209
+
210
+ def error(self, msg: str, **kv: Any) -> None:
211
+ _log_kv(self._logger, logging.ERROR, msg, kv)
212
+
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)}})
216
+
217
+ def log(self, level: int, msg: str, **kv: Any) -> None:
218
+ _log_kv(self._logger, level, msg, kv)
219
+
220
+
221
+ def get_logger(name: str) -> KVLogger:
222
+ """Return a `KVLogger` wrapper for the named logger."""
223
+ return KVLogger(logging.getLogger(name))
164
224
 
165
225
 
166
226
  class _ThirdPartySelectorFilter(logging.Filter):
@@ -226,9 +286,10 @@ def _ensure_log_dir(log_dir: Path) -> None:
226
286
 
227
287
  def configure_logging(
228
288
  *,
229
- env_prefix: str,
230
289
  app_logger_prefix: str,
231
- app_name: str,
290
+ name: str | None = None,
291
+ env_prefix: str | None = None,
292
+ app_name: str | None = None,
232
293
  log_filename: str | None = None,
233
294
  max_message_chars: int = 4000,
234
295
  ) -> Path:
@@ -236,6 +297,15 @@ def configure_logging(
236
297
 
237
298
  Returns the resolved log file path in use.
238
299
  """
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=")
308
+
239
309
  contract = LoggingContract(
240
310
  env_prefix=env_prefix,
241
311
  app_logger_prefix=app_logger_prefix,
@@ -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.1
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,19 @@ 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
+ from instrukt_ai_logging import configure_logging, get_logger
44
+
45
+ configure_logging(name="teleclaude", app_logger_prefix="teleclaude")
46
+ logger = get_logger("teleclaude.core")
47
+ logger.info("job_started", job_id="abc123")
48
+ ```
49
+
40
50
  ## Environment variables (contract)
41
51
 
42
52
  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.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, log_kv
7
+ from instrukt_ai_logging import configure_logging, get_logger
8
8
 
9
9
 
10
10
  @pytest.fixture()
@@ -36,9 +36,8 @@ def test_our_logs_respect_app_level_and_third_party_baseline(isolated_logging, m
36
36
  monkeypatch.delenv("TELECLAUDE_THIRD_PARTY_LOGGERS", raising=False)
37
37
 
38
38
  log_path = configure_logging(
39
- env_prefix="TELECLAUDE",
40
39
  app_logger_prefix="teleclaude",
41
- app_name="teleclaude",
40
+ name="teleclaude",
42
41
  )
43
42
 
44
43
  logging.getLogger("teleclaude.core").debug("hello from ours")
@@ -58,9 +57,8 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
58
57
  monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOGGERS", "httpcore")
59
58
 
60
59
  log_path = configure_logging(
61
- env_prefix="TELECLAUDE",
62
60
  app_logger_prefix="teleclaude",
63
- app_name="teleclaude",
61
+ name="teleclaude",
64
62
  )
65
63
 
66
64
  # Ensure records are actually created even though root is WARNING in spotlight mode.
@@ -85,39 +83,19 @@ def test_spotlight_allows_selected_third_party_only(isolated_logging, monkeypatc
85
83
  assert "telegram info" not in content
86
84
 
87
85
 
88
- def test_log_kv_requires_msg_key(isolated_logging, monkeypatch):
89
- with TemporaryDirectory() as tmp:
90
- monkeypatch.setenv("INSTRUKT_AI_LOG_ROOT", tmp)
91
- monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
92
- monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
93
-
94
- configure_logging(
95
- env_prefix="TELECLAUDE",
96
- app_logger_prefix="teleclaude",
97
- app_name="teleclaude",
98
- )
99
-
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):
86
+ def test_named_kv_logger_emits_pairs(isolated_logging, monkeypatch):
105
87
  with TemporaryDirectory() as tmp:
106
88
  monkeypatch.setenv("INSTRUKT_AI_LOG_ROOT", tmp)
107
89
  monkeypatch.setenv("TELECLAUDE_LOG_LEVEL", "INFO")
108
90
  monkeypatch.setenv("TELECLAUDE_THIRD_PARTY_LOG_LEVEL", "WARNING")
109
91
 
110
92
  log_path = configure_logging(
111
- env_prefix="TELECLAUDE",
112
93
  app_logger_prefix="teleclaude",
113
- app_name="teleclaude",
94
+ name="teleclaude",
114
95
  )
115
96
 
116
- log_kv(
117
- logging.getLogger("teleclaude.core"),
118
- logging.INFO,
119
- {"msg": "hello", "session": "abc123", "n": 1},
120
- )
97
+ logger = get_logger("teleclaude.core")
98
+ logger.info("hello", session="abc123", n=1)
121
99
 
122
100
  content = _read_text(log_path)
123
101
  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)