traceact 0.2.0__tar.gz → 0.2.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.
- {traceact-0.2.0/traceact.egg-info → traceact-0.2.1}/PKG-INFO +1 -1
- {traceact-0.2.0 → traceact-0.2.1}/USAGE.md +1 -1
- {traceact-0.2.0 → traceact-0.2.1}/pyproject.toml +1 -1
- {traceact-0.2.0 → traceact-0.2.1}/traceact/__init__.py +1 -1
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/reader.py +59 -12
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/server.py +9 -5
- {traceact-0.2.0 → traceact-0.2.1/traceact.egg-info}/PKG-INFO +1 -1
- {traceact-0.2.0 → traceact-0.2.1}/LICENSE +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/MANIFEST.in +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/README.md +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/setup.cfg +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/budget.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/config.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/context.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/decorators.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/helpers.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/ids.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/sinks.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/trace.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/__init__.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/cli.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/instance.py +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/app.js +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/index.html +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/styles.css +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/SOURCES.txt +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/dependency_links.txt +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/entry_points.txt +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/requires.txt +0 -0
- {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/top_level.txt +0 -0
|
@@ -745,7 +745,7 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
|
|
|
745
745
|
|
|
746
746
|
| Method | Path | Request | Response |
|
|
747
747
|
|---|---|---|---|
|
|
748
|
-
| `GET` | `/api/health` | — | `{"status":"ok","version":"0.2.
|
|
748
|
+
| `GET` | `/api/health` | — | `{"status":"ok","version":"0.2.1","sources":N}` |
|
|
749
749
|
| `GET` | `/api/sources` | — | `[{"name":"...","path":"..."}]` |
|
|
750
750
|
| `POST` | `/api/sources` | `{"path":"..."}` | `{"name":"...","path":"..."}` |
|
|
751
751
|
| `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
|
|
@@ -84,6 +84,13 @@ class SourceReader:
|
|
|
84
84
|
# New files that appear later (e.g. a new shard) start at offset 0 and
|
|
85
85
|
# so are read from their beginning on the next poll.
|
|
86
86
|
self._offsets: Dict[str, int] = {}
|
|
87
|
+
# Per-file inode number. When a file is deleted and recreated at the same
|
|
88
|
+
# path the inode changes even if the new file is larger than the old one,
|
|
89
|
+
# so a size-only check would seek into the wrong position. Tracking the
|
|
90
|
+
# inode lets us detect replacement (not just truncation) reliably.
|
|
91
|
+
# st_ino is 0 on Windows (inodes unavailable); the check is a safe no-op
|
|
92
|
+
# there and falls back to the existing truncation-only detection.
|
|
93
|
+
self._inodes: Dict[str, int] = {}
|
|
87
94
|
|
|
88
95
|
# -- initial load ------------------------------------------------------
|
|
89
96
|
|
|
@@ -110,6 +117,9 @@ class SourceReader:
|
|
|
110
117
|
ring.append(obj)
|
|
111
118
|
# Record where we stopped so poll() reads only new bytes.
|
|
112
119
|
self._offsets[filepath] = _file_size(filepath)
|
|
120
|
+
# Record which physical file we just read, so poll() can
|
|
121
|
+
# tell a delete+recreate apart from a plain append.
|
|
122
|
+
self._inodes[filepath] = _file_inode(filepath)
|
|
113
123
|
except OSError:
|
|
114
124
|
# A file that vanished or can't be opened is simply skipped.
|
|
115
125
|
continue
|
|
@@ -120,22 +130,37 @@ class SourceReader:
|
|
|
120
130
|
|
|
121
131
|
# -- live tail ---------------------------------------------------------
|
|
122
132
|
|
|
123
|
-
def poll(self) ->
|
|
133
|
+
def poll(self, limit: int) -> Dict[str, Any]:
|
|
124
134
|
"""
|
|
125
|
-
Return
|
|
126
|
-
|
|
127
|
-
Handles
|
|
128
|
-
- new bytes appended
|
|
129
|
-
- a brand-new file
|
|
130
|
-
- a truncated
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
Return traces that arrived since the last snapshot() or poll().
|
|
136
|
+
|
|
137
|
+
Handles four cases per file:
|
|
138
|
+
- new bytes appended → read and parse them from the stored offset.
|
|
139
|
+
- a brand-new file → offset defaults to 0, so it's read in full.
|
|
140
|
+
- a truncated file → size < stored offset; reset to 0 and re-read.
|
|
141
|
+
- a deleted+recreated file at the same path → its inode changes even
|
|
142
|
+
though the new file may be larger than the old offset, so a
|
|
143
|
+
size-only check can't detect it: it would silently seek into the
|
|
144
|
+
middle of unrelated new content and drop everything before that
|
|
145
|
+
byte position. Detected here by comparing `st_ino` across calls.
|
|
146
|
+
|
|
147
|
+
On a plain append, returns {"kind": "append", "traces": [...]}
|
|
148
|
+
(newest-last, arrival order — the caller prepends these to the top of
|
|
149
|
+
the log). If any file was replaced, the reader instead rebuilds a full
|
|
150
|
+
snapshot from scratch (so no earlier trace is lost) and returns
|
|
151
|
+
{"kind": "snapshot", "traces": [...]} (newest-first) — the caller
|
|
152
|
+
should replace its trace list rather than prepend onto it.
|
|
135
153
|
"""
|
|
136
154
|
fresh: List[Dict[str, Any]] = []
|
|
155
|
+
replaced = False
|
|
137
156
|
|
|
138
157
|
for filepath in _jsonl_files(self.path):
|
|
158
|
+
inode = _file_inode(filepath)
|
|
159
|
+
prior_inode = self._inodes.get(filepath)
|
|
160
|
+
if prior_inode is not None and inode is not None and inode != prior_inode:
|
|
161
|
+
replaced = True
|
|
162
|
+
self._inodes[filepath] = inode
|
|
163
|
+
|
|
139
164
|
size = _file_size(filepath)
|
|
140
165
|
offset = self._offsets.get(filepath, 0)
|
|
141
166
|
|
|
@@ -159,7 +184,13 @@ class SourceReader:
|
|
|
159
184
|
except OSError:
|
|
160
185
|
continue
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
if replaced:
|
|
188
|
+
# Discard whatever `fresh` picked up from the wrong byte offset
|
|
189
|
+
# and rebuild cleanly. snapshot() re-reads every file from the
|
|
190
|
+
# start and refreshes both _offsets and _inodes.
|
|
191
|
+
return {"kind": "snapshot", "traces": self.snapshot(limit)}
|
|
192
|
+
|
|
193
|
+
return {"kind": "append", "traces": fresh}
|
|
163
194
|
|
|
164
195
|
|
|
165
196
|
# ---------------------------------------------------------------------------
|
|
@@ -191,6 +222,22 @@ def _file_size(filepath: str) -> int:
|
|
|
191
222
|
return 0
|
|
192
223
|
|
|
193
224
|
|
|
225
|
+
def _file_inode(filepath: str) -> Optional[int]:
|
|
226
|
+
"""
|
|
227
|
+
Inode number for a file, or None if it can't be determined.
|
|
228
|
+
|
|
229
|
+
st_ino is 0 on some Windows filesystems where Python can't populate it;
|
|
230
|
+
treated as unknown there so replacement detection simply doesn't trigger
|
|
231
|
+
(falling back to the existing truncation-only check) rather than firing
|
|
232
|
+
false positives on every poll.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
ino = os.stat(filepath).st_ino
|
|
236
|
+
except OSError:
|
|
237
|
+
return None
|
|
238
|
+
return ino or None
|
|
239
|
+
|
|
240
|
+
|
|
194
241
|
def _started_at_key(trace: Dict[str, Any]) -> str:
|
|
195
242
|
"""
|
|
196
243
|
Sort key for ordering traces by start time. started_at is an ISO 8601
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# What it serves:
|
|
7
7
|
# GET / the single-page app (static/index.html)
|
|
8
8
|
# GET /static/<file> the app's CSS and JS
|
|
9
|
-
# GET /api/health {"status":"ok","version":"
|
|
9
|
+
# GET /api/health {"status":"ok","version":"<pkg version>","sources":N}
|
|
10
10
|
# GET /api/sources list configured sources (JSON)
|
|
11
11
|
# POST /api/sources add a source by path (JSON body)
|
|
12
12
|
# GET /api/pick?type=file|folder open a native OS dialog; returns {"path":"..."}
|
|
@@ -41,6 +41,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
41
41
|
from typing import Dict, Optional
|
|
42
42
|
from urllib.parse import parse_qs, urlparse
|
|
43
43
|
|
|
44
|
+
from traceact import __version__
|
|
44
45
|
from traceact.viewer.reader import SourceReader
|
|
45
46
|
|
|
46
47
|
# Directory where drag-dropped files are saved so they can be tailed.
|
|
@@ -173,7 +174,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
173
174
|
def _serve_health(self) -> None:
|
|
174
175
|
self._send_json(200, {
|
|
175
176
|
"status": "ok",
|
|
176
|
-
"version":
|
|
177
|
+
"version": __version__,
|
|
177
178
|
"sources": len(self.server.state.sources), # type: ignore[attr-defined]
|
|
178
179
|
})
|
|
179
180
|
|
|
@@ -274,11 +275,14 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
274
275
|
|
|
275
276
|
# Phase 2: the live tail — poll for appended traces forever, until
|
|
276
277
|
# the browser disconnects (which surfaces as a write error).
|
|
278
|
+
# poll() returns kind="snapshot" instead of "append" when a source
|
|
279
|
+
# file was deleted and recreated mid-stream, so the client replaces
|
|
280
|
+
# its trace list instead of prepending onto stale data.
|
|
277
281
|
while True:
|
|
278
282
|
time.sleep(_POLL_INTERVAL_SECONDS)
|
|
279
|
-
|
|
280
|
-
if
|
|
281
|
-
self._send_event(
|
|
283
|
+
result = reader.poll(limit)
|
|
284
|
+
if result["traces"]:
|
|
285
|
+
self._send_event(result)
|
|
282
286
|
else:
|
|
283
287
|
# A comment line acts as a heartbeat: it keeps the
|
|
284
288
|
# connection alive and lets us notice a dropped client.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|