pulselog 0.1.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.
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulselog
3
+ Version: 0.1.0
4
+ Summary: Real-time browser dashboard for Python logging — zero config, non-blocking
5
+ License: MIT
6
+ Keywords: logging,dashboard,websocket,real-time,monitoring
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: System :: Logging
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: websockets>=11.0
21
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
26
+
27
+ # pulselog
28
+
29
+ A Python logging library that streams every `logger.info()` to a live real-time dashboard in your browser — with zero configuration.
30
+
31
+ ```
32
+ pip install pulselog
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ from pulselog import Logger
39
+
40
+ logger = Logger("my-app")
41
+
42
+ logger.info("training started", epoch=1)
43
+ logger.warning("learning rate too high", lr=0.1)
44
+ logger.save("epoch-1", {"acc": 0.91, "loss": 0.23}, status="DONE", progress=33)
45
+ logger.shutdown()
46
+ ```
47
+
48
+ A browser tab opens automatically at `http://localhost:5678` showing all logs in real time.
49
+
50
+ ## Why pulselog?
51
+
52
+ Standard logging solutions block the main thread on every log call — waiting for a file write, HTTP request, or DB insert. In tight loops (ML training, inference, data pipelines) this kills performance.
53
+
54
+ pulselog uses a **non-blocking in-memory queue + daemon background worker**. The main thread never waits. Log calls cost ~2µs. 1 million calls complete in under 2 seconds.
55
+
56
+ ## Dashboard
57
+
58
+ The dashboard is a single self-contained HTML file served over WebSocket — no build step, no CDN, no framework.
59
+
60
+ **Logs tab:**
61
+ - Colour-coded by level (DEBUG=gray, INFO=blue, WARNING=amber, ERROR/CRITICAL=red)
62
+ - Level filter + full-text search
63
+ - Virtual list rendering — handles 100k+ logs with zero browser lag
64
+ - Auto-scroll with manual scroll override
65
+ - Export all logs as JSON
66
+
67
+ **Checkpoints tab:**
68
+ - Progress bars for each checkpoint
69
+ - Overall progress = average of all checkpoint progress values
70
+ - Expandable JSON data viewer
71
+ - Status icons: DONE ✅, IN_PROGRESS 🟡, FAILED 🔴, SKIPPED ⚫
72
+
73
+ ## API
74
+
75
+ ### Initialisation
76
+
77
+ ```python
78
+ logger = Logger(
79
+ name, # shown in dashboard
80
+ host="localhost", # dashboard bind host
81
+ port=5678, # auto-increments if port is taken
82
+ auto_open=True, # open browser automatically
83
+ dashboard=True, # set False for production/CI
84
+ checkpoint_path=".pulselog/checkpoints.db",
85
+ level="DEBUG", # minimum capture level
86
+ worker_interval=0.05, # drain interval in seconds
87
+ )
88
+ ```
89
+
90
+ ### Logging
91
+
92
+ ```python
93
+ logger.debug(msg, **extra)
94
+ logger.info(msg, **extra)
95
+ logger.warning(msg, **extra)
96
+ logger.error(msg, **extra)
97
+ logger.critical(msg, **extra)
98
+
99
+ # Extra kwargs appear as metadata in the dashboard
100
+ logger.info("request handled", user_id=42, latency_ms=12)
101
+ ```
102
+
103
+ ### Checkpoints
104
+
105
+ ```python
106
+ logger.save(
107
+ name, # checkpoint identifier
108
+ data, # any JSON-serialisable dict
109
+ status="DONE", # "DONE"|"IN_PROGRESS"|"FAILED"|"SKIPPED"
110
+ note="", # human-readable description
111
+ progress=None # 0–100, shown as progress bar
112
+ )
113
+
114
+ result = logger.load("epoch-5") # → dict | None (never raises)
115
+ names = logger.checkpoints() # → list[str]
116
+ logger.delete_checkpoint("epoch-3")
117
+ ```
118
+
119
+ ### Utilities
120
+
121
+ ```python
122
+ logger.tag("phase-2") # group following logs under a label
123
+ logger.divider("epoch boundary") # insert visual divider in dashboard
124
+ logger.flush() # force-drain queue (call before exit)
125
+ logger.shutdown() # graceful teardown
126
+ stats = logger.stats() # operational metrics dict
127
+ ```
128
+
129
+ ### Stats
130
+
131
+ ```python
132
+ {
133
+ "records_logged": int,
134
+ "records_dropped": int, # due to queue overflow
135
+ "queue_size": int,
136
+ "checkpoints_saved": int,
137
+ "dashboard_clients": int,
138
+ "uptime_seconds": float,
139
+ }
140
+ ```
141
+
142
+ ## Configuration
143
+
144
+ Priority order (highest → lowest): `Logger()` kwargs > env vars > `pulselog.toml` > defaults
145
+
146
+ ### Environment variables
147
+
148
+ ```bash
149
+ PULSELOG_DASHBOARD=false
150
+ PULSELOG_HOST=0.0.0.0
151
+ PULSELOG_PORT=8080
152
+ PULSELOG_AUTO_OPEN=false
153
+ PULSELOG_CHECKPOINT_PATH=/data/checkpoints.db
154
+ PULSELOG_LEVEL=INFO
155
+ PULSELOG_WORKER_INTERVAL=0.01
156
+ ```
157
+
158
+ ### `pulselog.toml` (place in CWD)
159
+
160
+ ```toml
161
+ [pulselog]
162
+ host = "0.0.0.0"
163
+ port = 8080
164
+ auto_open = false
165
+ level = "INFO"
166
+ ```
167
+
168
+ ## stdlib `logging` integration
169
+
170
+ ```python
171
+ import logging
172
+ from pulselog.handler import PulseHandler
173
+
174
+ logging.getLogger().addHandler(PulseHandler("my-app"))
175
+ logging.info("this appears in the pulselog dashboard")
176
+ ```
177
+
178
+ ## Production usage
179
+
180
+ ```python
181
+ # In production: disable dashboard, keep checkpoints
182
+ logger = Logger("prod", dashboard=False, checkpoint_path="/data/checkpoints.db")
183
+ ```
184
+
185
+ With `dashboard=False`:
186
+ - No threads are started
187
+ - No port is bound
188
+ - No browser is opened
189
+ - Checkpoint reads/writes still work
190
+ - Log calls return in ~100ns (level check only)
191
+
192
+ ## Performance
193
+
194
+ | Operation | Throughput |
195
+ |-----------|-----------|
196
+ | `logger.info()` call | ~2µs |
197
+ | 1 million log calls | <2s |
198
+ | Queue `put()` | O(1), <1µs |
199
+ | Dashboard at 100k logs | No lag (virtual list) |
200
+
201
+ ## Design
202
+
203
+ ```
204
+ logger.info() ← O(1), non-blocking
205
+
206
+
207
+ LogQueue ← deque(maxlen=10_000), thread-safe
208
+
209
+ ▼ every 50ms
210
+ BackgroundWorker ← daemon thread
211
+
212
+ ├─▶ DashboardServer.broadcast()
213
+ │ └─▶ WebSocket clients (all connected browsers)
214
+
215
+ └─▶ (additional handlers)
216
+ ```
217
+
218
+ The queue uses `collections.deque(maxlen=N)` — when full, the oldest record is silently dropped (never blocks). Dropped records are counted in `logger.stats()`.
219
+
220
+ ## License
221
+
222
+ MIT
@@ -0,0 +1,196 @@
1
+ # pulselog
2
+
3
+ A Python logging library that streams every `logger.info()` to a live real-time dashboard in your browser — with zero configuration.
4
+
5
+ ```
6
+ pip install pulselog
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from pulselog import Logger
13
+
14
+ logger = Logger("my-app")
15
+
16
+ logger.info("training started", epoch=1)
17
+ logger.warning("learning rate too high", lr=0.1)
18
+ logger.save("epoch-1", {"acc": 0.91, "loss": 0.23}, status="DONE", progress=33)
19
+ logger.shutdown()
20
+ ```
21
+
22
+ A browser tab opens automatically at `http://localhost:5678` showing all logs in real time.
23
+
24
+ ## Why pulselog?
25
+
26
+ Standard logging solutions block the main thread on every log call — waiting for a file write, HTTP request, or DB insert. In tight loops (ML training, inference, data pipelines) this kills performance.
27
+
28
+ pulselog uses a **non-blocking in-memory queue + daemon background worker**. The main thread never waits. Log calls cost ~2µs. 1 million calls complete in under 2 seconds.
29
+
30
+ ## Dashboard
31
+
32
+ The dashboard is a single self-contained HTML file served over WebSocket — no build step, no CDN, no framework.
33
+
34
+ **Logs tab:**
35
+ - Colour-coded by level (DEBUG=gray, INFO=blue, WARNING=amber, ERROR/CRITICAL=red)
36
+ - Level filter + full-text search
37
+ - Virtual list rendering — handles 100k+ logs with zero browser lag
38
+ - Auto-scroll with manual scroll override
39
+ - Export all logs as JSON
40
+
41
+ **Checkpoints tab:**
42
+ - Progress bars for each checkpoint
43
+ - Overall progress = average of all checkpoint progress values
44
+ - Expandable JSON data viewer
45
+ - Status icons: DONE ✅, IN_PROGRESS 🟡, FAILED 🔴, SKIPPED ⚫
46
+
47
+ ## API
48
+
49
+ ### Initialisation
50
+
51
+ ```python
52
+ logger = Logger(
53
+ name, # shown in dashboard
54
+ host="localhost", # dashboard bind host
55
+ port=5678, # auto-increments if port is taken
56
+ auto_open=True, # open browser automatically
57
+ dashboard=True, # set False for production/CI
58
+ checkpoint_path=".pulselog/checkpoints.db",
59
+ level="DEBUG", # minimum capture level
60
+ worker_interval=0.05, # drain interval in seconds
61
+ )
62
+ ```
63
+
64
+ ### Logging
65
+
66
+ ```python
67
+ logger.debug(msg, **extra)
68
+ logger.info(msg, **extra)
69
+ logger.warning(msg, **extra)
70
+ logger.error(msg, **extra)
71
+ logger.critical(msg, **extra)
72
+
73
+ # Extra kwargs appear as metadata in the dashboard
74
+ logger.info("request handled", user_id=42, latency_ms=12)
75
+ ```
76
+
77
+ ### Checkpoints
78
+
79
+ ```python
80
+ logger.save(
81
+ name, # checkpoint identifier
82
+ data, # any JSON-serialisable dict
83
+ status="DONE", # "DONE"|"IN_PROGRESS"|"FAILED"|"SKIPPED"
84
+ note="", # human-readable description
85
+ progress=None # 0–100, shown as progress bar
86
+ )
87
+
88
+ result = logger.load("epoch-5") # → dict | None (never raises)
89
+ names = logger.checkpoints() # → list[str]
90
+ logger.delete_checkpoint("epoch-3")
91
+ ```
92
+
93
+ ### Utilities
94
+
95
+ ```python
96
+ logger.tag("phase-2") # group following logs under a label
97
+ logger.divider("epoch boundary") # insert visual divider in dashboard
98
+ logger.flush() # force-drain queue (call before exit)
99
+ logger.shutdown() # graceful teardown
100
+ stats = logger.stats() # operational metrics dict
101
+ ```
102
+
103
+ ### Stats
104
+
105
+ ```python
106
+ {
107
+ "records_logged": int,
108
+ "records_dropped": int, # due to queue overflow
109
+ "queue_size": int,
110
+ "checkpoints_saved": int,
111
+ "dashboard_clients": int,
112
+ "uptime_seconds": float,
113
+ }
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ Priority order (highest → lowest): `Logger()` kwargs > env vars > `pulselog.toml` > defaults
119
+
120
+ ### Environment variables
121
+
122
+ ```bash
123
+ PULSELOG_DASHBOARD=false
124
+ PULSELOG_HOST=0.0.0.0
125
+ PULSELOG_PORT=8080
126
+ PULSELOG_AUTO_OPEN=false
127
+ PULSELOG_CHECKPOINT_PATH=/data/checkpoints.db
128
+ PULSELOG_LEVEL=INFO
129
+ PULSELOG_WORKER_INTERVAL=0.01
130
+ ```
131
+
132
+ ### `pulselog.toml` (place in CWD)
133
+
134
+ ```toml
135
+ [pulselog]
136
+ host = "0.0.0.0"
137
+ port = 8080
138
+ auto_open = false
139
+ level = "INFO"
140
+ ```
141
+
142
+ ## stdlib `logging` integration
143
+
144
+ ```python
145
+ import logging
146
+ from pulselog.handler import PulseHandler
147
+
148
+ logging.getLogger().addHandler(PulseHandler("my-app"))
149
+ logging.info("this appears in the pulselog dashboard")
150
+ ```
151
+
152
+ ## Production usage
153
+
154
+ ```python
155
+ # In production: disable dashboard, keep checkpoints
156
+ logger = Logger("prod", dashboard=False, checkpoint_path="/data/checkpoints.db")
157
+ ```
158
+
159
+ With `dashboard=False`:
160
+ - No threads are started
161
+ - No port is bound
162
+ - No browser is opened
163
+ - Checkpoint reads/writes still work
164
+ - Log calls return in ~100ns (level check only)
165
+
166
+ ## Performance
167
+
168
+ | Operation | Throughput |
169
+ |-----------|-----------|
170
+ | `logger.info()` call | ~2µs |
171
+ | 1 million log calls | <2s |
172
+ | Queue `put()` | O(1), <1µs |
173
+ | Dashboard at 100k logs | No lag (virtual list) |
174
+
175
+ ## Design
176
+
177
+ ```
178
+ logger.info() ← O(1), non-blocking
179
+
180
+
181
+ LogQueue ← deque(maxlen=10_000), thread-safe
182
+
183
+ ▼ every 50ms
184
+ BackgroundWorker ← daemon thread
185
+
186
+ ├─▶ DashboardServer.broadcast()
187
+ │ └─▶ WebSocket clients (all connected browsers)
188
+
189
+ └─▶ (additional handlers)
190
+ ```
191
+
192
+ The queue uses `collections.deque(maxlen=N)` — when full, the oldest record is silently dropped (never blocks). Dropped records are counted in `logger.stats()`.
193
+
194
+ ## License
195
+
196
+ MIT
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from pulselog.logger import Logger
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["Logger", "__version__"]
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Any
9
+
10
+ from pulselog.exceptions import InvalidStatusError
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Constants
14
+ # ---------------------------------------------------------------------------
15
+ VALID_STATUSES = {"DONE", "IN_PROGRESS", "FAILED", "SKIPPED"}
16
+
17
+ _SCHEMA = """
18
+ CREATE TABLE IF NOT EXISTS checkpoints (
19
+ name TEXT PRIMARY KEY,
20
+ data TEXT NOT NULL,
21
+ status TEXT NOT NULL,
22
+ note TEXT DEFAULT '',
23
+ progress REAL,
24
+ saved_at REAL NOT NULL
25
+ )
26
+ """
27
+
28
+
29
+ class CheckpointStore:
30
+ """Thread-safe SQLite-backed key/value store for training checkpoints.
31
+
32
+ Multiple threads may call save/load/delete concurrently; a threading.Lock
33
+ serialises all SQLite operations so we never hit "database is locked".
34
+
35
+ WAL mode is enabled so concurrent reads do not block writes.
36
+ """
37
+
38
+ def __init__(self, db_path: str = ":memory:") -> None:
39
+ """Open (or create) the checkpoint database.
40
+
41
+ Args:
42
+ db_path: Filesystem path to the SQLite file, or ":memory:".
43
+ """
44
+ if db_path != ":memory:":
45
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
46
+
47
+ # check_same_thread=False is safe because we guard every access with _lock.
48
+ self._conn = sqlite3.connect(db_path, check_same_thread=False) # guarded by self._lock
49
+ self._lock = threading.Lock()
50
+
51
+ with self._lock:
52
+ self._conn.execute("PRAGMA journal_mode=WAL")
53
+ self._conn.execute(_SCHEMA)
54
+ self._conn.commit()
55
+
56
+ self._saved_count = 0 # guarded by self._lock
57
+
58
+ # ------------------------------------------------------------------
59
+ # Public API
60
+ # ------------------------------------------------------------------
61
+
62
+ def save(
63
+ self,
64
+ name: str,
65
+ data: Dict[str, Any],
66
+ status: str = "DONE",
67
+ note: str = "",
68
+ progress: Optional[float] = None,
69
+ ) -> Dict[str, Any]:
70
+ """Persist a checkpoint, overwriting any previous value for *name*.
71
+
72
+ Args:
73
+ name: Unique checkpoint identifier.
74
+ data: JSON-serialisable payload dict.
75
+ status: One of DONE | IN_PROGRESS | FAILED | SKIPPED.
76
+ note: Human-readable description.
77
+ progress: Optional 0–100 progress value.
78
+
79
+ Returns:
80
+ The full record dict including a ``_meta`` key.
81
+
82
+ Raises:
83
+ InvalidStatusError: If *status* is not one of the allowed values.
84
+ """
85
+ upper_status = status.upper()
86
+ if upper_status not in VALID_STATUSES:
87
+ raise InvalidStatusError(
88
+ f"Invalid checkpoint status {status!r}. "
89
+ f"Allowed values: {', '.join(sorted(VALID_STATUSES))}"
90
+ )
91
+
92
+ saved_at = time.time()
93
+ data_json = json.dumps(data)
94
+
95
+ with self._lock:
96
+ self._conn.execute(
97
+ """
98
+ INSERT OR REPLACE INTO checkpoints
99
+ (name, data, status, note, progress, saved_at)
100
+ VALUES (?, ?, ?, ?, ?, ?)
101
+ """,
102
+ (name, data_json, upper_status, note, progress, saved_at),
103
+ )
104
+ self._conn.commit()
105
+ self._saved_count += 1
106
+
107
+ return {
108
+ **data,
109
+ "_meta": {
110
+ "name": name,
111
+ "status": upper_status,
112
+ "note": note,
113
+ "progress": progress,
114
+ "saved_at": saved_at,
115
+ },
116
+ }
117
+
118
+ def load(self, name: str) -> Optional[Dict[str, Any]]:
119
+ """Load a checkpoint by name.
120
+
121
+ Args:
122
+ name: Checkpoint identifier.
123
+
124
+ Returns:
125
+ Dict with original data keys plus ``_meta``, or None if not found.
126
+ """
127
+ with self._lock:
128
+ cur = self._conn.execute(
129
+ "SELECT data, status, note, progress, saved_at FROM checkpoints WHERE name = ?",
130
+ (name,),
131
+ )
132
+ row = cur.fetchone()
133
+
134
+ if row is None:
135
+ return None
136
+
137
+ data_json, status, note, progress, saved_at = row
138
+ data = json.loads(data_json)
139
+ return {
140
+ **data,
141
+ "_meta": {
142
+ "name": name,
143
+ "status": status,
144
+ "note": note,
145
+ "progress": progress,
146
+ "saved_at": saved_at,
147
+ },
148
+ }
149
+
150
+ def checkpoints(self) -> List[str]:
151
+ """Return all checkpoint names ordered by most-recently saved first.
152
+
153
+ Returns:
154
+ List of name strings.
155
+ """
156
+ with self._lock:
157
+ cur = self._conn.execute(
158
+ "SELECT name FROM checkpoints ORDER BY saved_at DESC"
159
+ )
160
+ return [row[0] for row in cur.fetchall()]
161
+
162
+ def delete_checkpoint(self, name: str) -> None:
163
+ """Delete a checkpoint by name. Silent if the name doesn't exist.
164
+
165
+ Args:
166
+ name: Checkpoint identifier to remove.
167
+ """
168
+ with self._lock:
169
+ self._conn.execute("DELETE FROM checkpoints WHERE name = ?", (name,))
170
+ self._conn.commit()
171
+
172
+ def saved_count(self) -> int:
173
+ """Total number of save() calls since this store was created."""
174
+ with self._lock:
175
+ return self._saved_count