instruktai-python-logger 0.1.5__tar.gz → 0.4.0__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 (24) hide show
  1. instruktai_python_logger-0.4.0/PKG-INFO +111 -0
  2. instruktai_python_logger-0.4.0/README.md +101 -0
  3. instruktai_python_logger-0.4.0/instrukt_ai_logging/__init__.py +33 -0
  4. instruktai_python_logger-0.4.0/instrukt_ai_logging/cli.py +224 -0
  5. instruktai_python_logger-0.4.0/instrukt_ai_logging/install.py +59 -0
  6. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instrukt_ai_logging/logging.py +168 -40
  7. instruktai_python_logger-0.4.0/instrukt_ai_logging/py.typed +1 -0
  8. instruktai_python_logger-0.4.0/instruktai_python_logger.egg-info/PKG-INFO +111 -0
  9. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instruktai_python_logger.egg-info/SOURCES.txt +6 -1
  10. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instruktai_python_logger.egg-info/entry_points.txt +1 -0
  11. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/pyproject.toml +6 -2
  12. instruktai_python_logger-0.4.0/tests/test_cli_follow.py +46 -0
  13. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/tests/test_configure_logging.py +44 -1
  14. instruktai_python_logger-0.4.0/tests/test_log_aggregation.py +132 -0
  15. instruktai_python_logger-0.4.0/tests/test_trace_logging.py +69 -0
  16. instruktai_python_logger-0.1.5/PKG-INFO +0 -69
  17. instruktai_python_logger-0.1.5/README.md +0 -59
  18. instruktai_python_logger-0.1.5/instrukt_ai_logging/__init__.py +0 -19
  19. instruktai_python_logger-0.1.5/instrukt_ai_logging/cli.py +0 -52
  20. instruktai_python_logger-0.1.5/instruktai_python_logger.egg-info/PKG-INFO +0 -69
  21. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
  22. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instruktai_python_logger.egg-info/requires.txt +0 -0
  23. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/instruktai_python_logger.egg-info/top_level.txt +0 -0
  24. {instruktai_python_logger-0.1.5 → instruktai_python_logger-0.4.0}/setup.cfg +0 -0
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: instruktai-python-logger
3
+ Version: 0.4.0
4
+ Summary: Centralized logging utilities for Python services.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8; extra == "dev"
9
+ Requires-Dist: ruff>=0.8; extra == "dev"
10
+
11
+ # InstruktAI Python Logger
12
+
13
+ Centralized logging utilities for Python services.
14
+
15
+ This repo provides a shared, consistent logging contract (env vars + output format + log location) intended to keep logs highly queryable (including by AIs that only read a tail window).
16
+
17
+ Background and rationale live in `docs/design.md`.
18
+ Publishing notes live in `docs/publishing.md`.
19
+
20
+ ## Install (editable, local workspace)
21
+
22
+ From PyPI (recommended):
23
+
24
+ ```bash
25
+ pip install instrukt-ai-logger
26
+ ```
27
+
28
+ From GitHub:
29
+
30
+ ```bash
31
+ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
32
+ ```
33
+
34
+ ## API
35
+
36
+ - Python entrypoint: `instrukt_ai_logging.configure_logging(...)`
37
+ - Logger helper: `instrukt_ai_logging.get_logger(name)` (named `**kv` logging)
38
+ - CLI entrypoint: `instrukt-ai-logs` (reads recent log lines)
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
+
51
+ ### Per-process log files (multi-producer apps)
52
+
53
+ When several processes belong to the same app (a daemon plus a watcher plus a
54
+ cron, for example), each one can own its own file under the same app directory
55
+ by passing `source=`:
56
+
57
+ ```py
58
+ configure_logging("teleclaude") # → teleclaude.log (default)
59
+ configure_logging("teleclaude", source="docs-watch") # → docs-watch.log
60
+ configure_logging("teleclaude", source="cron") # → cron.log
61
+ ```
62
+
63
+ The `.log` extension is always appended; pass the bare stem. Files all live
64
+ under `/var/log/instrukt-ai/{app}/`. The CLI merges them transparently
65
+ (see below).
66
+
67
+ ## Environment variables (contract)
68
+
69
+ Per-app prefix model (example uses `TELECLAUDE_`):
70
+
71
+ - `TELECLAUDE_LOG_LEVEL`
72
+ - `TELECLAUDE_THIRD_PARTY_LOG_LEVEL`
73
+ - `TELECLAUDE_THIRD_PARTY_LOGGERS` (comma-separated logger prefixes to spotlight, e.g. `httpcore,telegram`)
74
+ - `TELECLAUDE_MUTED_LOGGERS` (comma-separated logger prefixes to force to WARNING+, e.g. `teleclaude.cli.tui`)
75
+
76
+ Global:
77
+
78
+ - `INSTRUKT_AI_LOG_ROOT` (optional log root override)
79
+
80
+ ## Log location (contract)
81
+
82
+ Default target:
83
+
84
+ - `/var/log/instrukt-ai/{app}/{app}.log`
85
+
86
+ The installer for each service is expected to create the directory and set write permissions for the daemon user. If the default location is not writable, the implementation will fall back to a user-writable directory and/or require `INSTRUKT_AI_LOG_ROOT`.
87
+
88
+ ## CLI
89
+
90
+ ```bash
91
+ # Last 10 minutes (default), follows
92
+ instrukt-ai-logs teleclaude -f
93
+
94
+ # Last 2 hours
95
+ instrukt-ai-logs teleclaude --since 2h
96
+
97
+ # Filter with regex
98
+ instrukt-ai-logs teleclaude --since 10m --grep 'logger=teleclaude'
99
+
100
+ # Follow (tail -f style) after printing the --since window
101
+ instrukt-ai-logs teleclaude --since 10m --follow
102
+
103
+ # Restrict to specific source streams (per-process files)
104
+ instrukt-ai-logs teleclaude --since 1h --logs=cron,docs-watch
105
+ ```
106
+
107
+ Without `--logs`, every `*.log*` file in the app directory is merged by
108
+ timestamp (rotation suffixes included). With `--logs=stem1,stem2`, only files
109
+ whose stem matches an entry are read — exact stem match, so `--logs=cron`
110
+ picks `cron.log`, `cron.log.0`, `cron.log.1` and never `cron-backup.log`.
111
+ Follow mode reads each selected file with one polling thread per file.
@@ -0,0 +1,101 @@
1
+ # InstruktAI Python Logger
2
+
3
+ Centralized logging utilities for Python services.
4
+
5
+ This repo provides a shared, consistent logging contract (env vars + output format + log location) intended to keep logs highly queryable (including by AIs that only read a tail window).
6
+
7
+ Background and rationale live in `docs/design.md`.
8
+ Publishing notes live in `docs/publishing.md`.
9
+
10
+ ## Install (editable, local workspace)
11
+
12
+ From PyPI (recommended):
13
+
14
+ ```bash
15
+ pip install instrukt-ai-logger
16
+ ```
17
+
18
+ From GitHub:
19
+
20
+ ```bash
21
+ pip install git+ssh://git@github.com/InstruktAI/python-logger.git
22
+ ```
23
+
24
+ ## API
25
+
26
+ - Python entrypoint: `instrukt_ai_logging.configure_logging(...)`
27
+ - Logger helper: `instrukt_ai_logging.get_logger(name)` (named `**kv` logging)
28
+ - CLI entrypoint: `instrukt-ai-logs` (reads recent log lines)
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
+
41
+ ### Per-process log files (multi-producer apps)
42
+
43
+ When several processes belong to the same app (a daemon plus a watcher plus a
44
+ cron, for example), each one can own its own file under the same app directory
45
+ by passing `source=`:
46
+
47
+ ```py
48
+ configure_logging("teleclaude") # → teleclaude.log (default)
49
+ configure_logging("teleclaude", source="docs-watch") # → docs-watch.log
50
+ configure_logging("teleclaude", source="cron") # → cron.log
51
+ ```
52
+
53
+ The `.log` extension is always appended; pass the bare stem. Files all live
54
+ under `/var/log/instrukt-ai/{app}/`. The CLI merges them transparently
55
+ (see below).
56
+
57
+ ## Environment variables (contract)
58
+
59
+ Per-app prefix model (example uses `TELECLAUDE_`):
60
+
61
+ - `TELECLAUDE_LOG_LEVEL`
62
+ - `TELECLAUDE_THIRD_PARTY_LOG_LEVEL`
63
+ - `TELECLAUDE_THIRD_PARTY_LOGGERS` (comma-separated logger prefixes to spotlight, e.g. `httpcore,telegram`)
64
+ - `TELECLAUDE_MUTED_LOGGERS` (comma-separated logger prefixes to force to WARNING+, e.g. `teleclaude.cli.tui`)
65
+
66
+ Global:
67
+
68
+ - `INSTRUKT_AI_LOG_ROOT` (optional log root override)
69
+
70
+ ## Log location (contract)
71
+
72
+ Default target:
73
+
74
+ - `/var/log/instrukt-ai/{app}/{app}.log`
75
+
76
+ The installer for each service is expected to create the directory and set write permissions for the daemon user. If the default location is not writable, the implementation will fall back to a user-writable directory and/or require `INSTRUKT_AI_LOG_ROOT`.
77
+
78
+ ## CLI
79
+
80
+ ```bash
81
+ # Last 10 minutes (default), follows
82
+ instrukt-ai-logs teleclaude -f
83
+
84
+ # Last 2 hours
85
+ instrukt-ai-logs teleclaude --since 2h
86
+
87
+ # Filter with regex
88
+ instrukt-ai-logs teleclaude --since 10m --grep 'logger=teleclaude'
89
+
90
+ # Follow (tail -f style) after printing the --since window
91
+ instrukt-ai-logs teleclaude --since 10m --follow
92
+
93
+ # Restrict to specific source streams (per-process files)
94
+ instrukt-ai-logs teleclaude --since 1h --logs=cron,docs-watch
95
+ ```
96
+
97
+ Without `--logs`, every `*.log*` file in the app directory is merged by
98
+ timestamp (rotation suffixes included). With `--logs=stem1,stem2`, only files
99
+ whose stem matches an entry are read — exact stem match, so `--logs=cron`
100
+ picks `cron.log`, `cron.log.0`, `cron.log.1` and never `cron-backup.log`.
101
+ Follow mode reads each selected file with one polling thread per file.
@@ -0,0 +1,33 @@
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
+ "InstruktAILogger",
11
+ "InstruktAILoggerProtocol",
12
+ "resolve_log_file",
13
+ "resolve_log_files",
14
+ "TRACE",
15
+ ]
16
+
17
+ try:
18
+ from importlib.metadata import packages_distributions, version as _pkg_version
19
+
20
+ _dists = packages_distributions().get(__name__, [])
21
+ __version__ = _pkg_version(_dists[0]) if _dists else "0.0.0"
22
+ except Exception: # pragma: no cover
23
+ __version__ = "0.0.0"
24
+
25
+ from instrukt_ai_logging.logging import ( # noqa: E402 (intentional re-export)
26
+ configure_logging,
27
+ get_logger,
28
+ InstruktAILogger,
29
+ InstruktAILoggerProtocol,
30
+ resolve_log_file,
31
+ resolve_log_files,
32
+ TRACE,
33
+ )
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import re
6
+ import sys
7
+ import threading
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Iterator
11
+
12
+ from instrukt_ai_logging.logging import (
13
+ iter_recent_log_lines_merged,
14
+ parse_since,
15
+ resolve_log_files,
16
+ )
17
+
18
+
19
+ def iter_follow_lines(
20
+ log_file: Path,
21
+ *,
22
+ poll_interval_s: float = 0.25,
23
+ start_at_end: bool = True,
24
+ max_lines: int | None = None,
25
+ max_seconds: float | None = None,
26
+ ) -> Iterator[str]:
27
+ """Yield new lines appended to `log_file`, similar to `tail -f`.
28
+
29
+ - Detects rotation (inode change) and truncation.
30
+ - Waits for file creation if it doesn't exist yet.
31
+ - `max_lines`/`max_seconds` are mainly for tests.
32
+ """
33
+ deadline = None if max_seconds is None else (time.monotonic() + max_seconds)
34
+ emitted = 0
35
+
36
+ f = None
37
+ inode = None
38
+ start_at_end_for_next_open = start_at_end
39
+
40
+ try:
41
+ while True:
42
+ if deadline is not None and time.monotonic() >= deadline:
43
+ return
44
+
45
+ if f is None:
46
+ try:
47
+ f = log_file.open("r", encoding="utf-8", errors="replace")
48
+ except FileNotFoundError:
49
+ time.sleep(poll_interval_s)
50
+ continue
51
+
52
+ try:
53
+ inode = os.fstat(f.fileno()).st_ino
54
+ except OSError:
55
+ inode = None
56
+
57
+ if start_at_end_for_next_open:
58
+ f.seek(0, os.SEEK_END)
59
+ else:
60
+ f.seek(0, os.SEEK_SET)
61
+
62
+ # Only the first open may start at end; after rotation we read from start.
63
+ start_at_end_for_next_open = False
64
+
65
+ line = f.readline()
66
+ if line:
67
+ yield line
68
+ emitted += 1
69
+ if max_lines is not None and emitted >= max_lines:
70
+ return
71
+ continue
72
+
73
+ # No new data: detect rotation/truncation and wait.
74
+ try:
75
+ st = log_file.stat()
76
+ except FileNotFoundError:
77
+ f.close()
78
+ f = None
79
+ inode = None
80
+ time.sleep(poll_interval_s)
81
+ continue
82
+
83
+ if inode is not None and getattr(st, "st_ino", None) is not None and st.st_ino != inode:
84
+ f.close()
85
+ f = None
86
+ inode = None
87
+ continue
88
+
89
+ if f.tell() > st.st_size:
90
+ f.seek(0, os.SEEK_SET)
91
+
92
+ time.sleep(poll_interval_s)
93
+ finally:
94
+ if f is not None:
95
+ try:
96
+ f.close()
97
+ except OSError:
98
+ pass
99
+
100
+
101
+ def _parse_stems(value: str) -> list[str]:
102
+ return [item.strip() for item in value.split(",") if item.strip()]
103
+
104
+
105
+ def _stem_of(path: Path) -> str:
106
+ """Return the stem before the first `.log` suffix (e.g. `cron.log.1` → `cron`)."""
107
+ name = path.name
108
+ idx = name.find(".log")
109
+ return name[:idx] if idx > 0 else name
110
+
111
+
112
+ def _is_rotation_suffix(path: Path) -> bool:
113
+ """True for `<stem>.log.<n>` rotation files (and friends), false for `<stem>.log`."""
114
+ name = path.name
115
+ idx = name.find(".log")
116
+ return idx > 0 and name[idx + len(".log") :] != ""
117
+
118
+
119
+ def main() -> None:
120
+ from instrukt_ai_logging import __version__
121
+
122
+ parser = argparse.ArgumentParser(prog="instrukt-ai-logs", add_help=True)
123
+ parser.add_argument(
124
+ "--version",
125
+ action="version",
126
+ version=f"%(prog)s {__version__}",
127
+ )
128
+ parser.add_argument("app", help="App/service name (folder under /var/log/instrukt-ai/)")
129
+ parser.add_argument(
130
+ "--since", default="10m", help="Time window (e.g. 10m, 2h, 1d). Default: 10m"
131
+ )
132
+ parser.add_argument(
133
+ "-f",
134
+ "--follow",
135
+ action="store_true",
136
+ help="Follow the log (tail -f style) after printing the --since window",
137
+ )
138
+ parser.add_argument("--grep", default="", help="Regex to filter lines (optional)")
139
+ parser.add_argument(
140
+ "--logs",
141
+ default="",
142
+ help=(
143
+ "Comma-separated list of stems to include (e.g. 'cron,docs-watch'). "
144
+ "Default: all log files in the app's directory."
145
+ ),
146
+ )
147
+ args = parser.parse_args()
148
+
149
+ try:
150
+ since = parse_since(args.since)
151
+ except ValueError as e:
152
+ raise SystemExit(str(e)) from e
153
+
154
+ pattern = re.compile(args.grep) if args.grep else None
155
+ stems = _parse_stems(args.logs) or None
156
+
157
+ files = resolve_log_files(args.app, stems=stems)
158
+ if not files:
159
+ if stems:
160
+ available = sorted({_stem_of(p) for p in resolve_log_files(args.app)})
161
+ available_str = ", ".join(available) if available else "(none)"
162
+ raise SystemExit(
163
+ f"No log files matched stems {stems} in app '{args.app}'. "
164
+ f"Available: {available_str}"
165
+ )
166
+ raise SystemExit(f"No log files found for app '{args.app}'")
167
+
168
+ for line in iter_recent_log_lines_merged(files, since):
169
+ if pattern and not pattern.search(line):
170
+ continue
171
+ sys.stdout.write(line)
172
+
173
+ if not args.follow:
174
+ return
175
+
176
+ sys.stdout.flush()
177
+
178
+ # Follow each selected file concurrently. New lines from any file are
179
+ # written to stdout as they arrive — no cross-file timestamp merge in
180
+ # follow mode, since real-time arrival ordering is its own merge.
181
+ # Filter to files that actually exist for the canonical (default) glob —
182
+ # for the explicit `--logs=` case, follow each named stem's canonical
183
+ # file (the unsuffixed `<stem>.log`) so we don't follow rotated history.
184
+ if stems is None:
185
+ follow_files = [p for p in files if not _is_rotation_suffix(p)]
186
+ else:
187
+ follow_files = []
188
+ for stem in stems:
189
+ for p in files:
190
+ if p.name == f"{stem}.log":
191
+ follow_files.append(p)
192
+ break
193
+
194
+ if not follow_files:
195
+ return
196
+
197
+ stop_event = threading.Event()
198
+
199
+ def _follow_one(path: Path) -> None:
200
+ try:
201
+ for line in iter_follow_lines(path, start_at_end=True):
202
+ if stop_event.is_set():
203
+ return
204
+ if pattern and not pattern.search(line):
205
+ continue
206
+ sys.stdout.write(line)
207
+ sys.stdout.flush()
208
+ except OSError:
209
+ return
210
+
211
+ threads = [
212
+ threading.Thread(target=_follow_one, args=(path,), daemon=True, name=f"follow-{path.name}")
213
+ for path in follow_files
214
+ ]
215
+ for t in threads:
216
+ t.start()
217
+
218
+ try:
219
+ while any(t.is_alive() for t in threads):
220
+ for t in threads:
221
+ t.join(timeout=0.25)
222
+ except KeyboardInterrupt:
223
+ stop_event.set()
224
+ return
@@ -0,0 +1,59 @@
1
+ """Install log rotation for InstruktAI logs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def _write_file(path: Path, content: str, *, force: bool) -> None:
12
+ if path.exists() and not force:
13
+ raise SystemExit(f"Refusing to overwrite existing file: {path}. Use --force.")
14
+ path.parent.mkdir(parents=True, exist_ok=True)
15
+ path.write_text(content, encoding="utf-8")
16
+
17
+
18
+ def _install_newsyslog(log_root: Path, *, force: bool) -> Path:
19
+ conf_path = Path("/etc/newsyslog.d/instrukt-ai.conf")
20
+ line = f"{log_root}/" + "*/*.log 640 10 100000 * N\n"
21
+ _write_file(conf_path, line, force=force)
22
+ return conf_path
23
+
24
+
25
+ def _install_logrotate(log_root: Path, *, force: bool) -> Path:
26
+ conf_path = Path("/etc/logrotate.d/instrukt-ai")
27
+ content = f"""{log_root}/*/*.log {{
28
+ size 100M
29
+ rotate 10
30
+ missingok
31
+ notifempty
32
+ copytruncate
33
+ }}
34
+ """
35
+ _write_file(conf_path, content, force=force)
36
+ return conf_path
37
+
38
+
39
+ def main() -> None:
40
+ parser = argparse.ArgumentParser(prog="instrukt-ai-log-setup", add_help=True)
41
+ parser.add_argument(
42
+ "--log-root",
43
+ default=os.environ.get("INSTRUKT_AI_LOG_ROOT", "/var/log/instrukt-ai"),
44
+ help="Base log root (default: /var/log/instrukt-ai or INSTRUKT_AI_LOG_ROOT)",
45
+ )
46
+ parser.add_argument("--force", action="store_true", help="Overwrite existing config")
47
+ args = parser.parse_args()
48
+
49
+ log_root = Path(args.log_root)
50
+ if sys.platform == "darwin":
51
+ conf = _install_newsyslog(log_root, force=args.force)
52
+ else:
53
+ conf = _install_logrotate(log_root, force=args.force)
54
+
55
+ sys.stdout.write(f"installed: {conf}\n")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()