logtap 0.4.0__py3-none-any.whl → 0.4.1__py3-none-any.whl
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.
- logtap/api/routes/logs.py +26 -31
- logtap/api/routes/parsed.py +8 -7
- logtap/api/routes/runs.py +9 -30
- logtap/cli/commands/doctor.py +127 -0
- logtap/cli/commands/tail.py +13 -5
- logtap/cli/main.py +2 -1
- logtap/core/runs.py +71 -31
- logtap/core/validation.py +132 -0
- logtap-0.4.1.dist-info/METADATA +304 -0
- {logtap-0.4.0.dist-info → logtap-0.4.1.dist-info}/RECORD +13 -12
- logtap-0.4.0.dist-info/METADATA +0 -319
- {logtap-0.4.0.dist-info → logtap-0.4.1.dist-info}/WHEEL +0 -0
- {logtap-0.4.0.dist-info → logtap-0.4.1.dist-info}/entry_points.txt +0 -0
- {logtap-0.4.0.dist-info → logtap-0.4.1.dist-info}/licenses/LICENSE +0 -0
logtap/core/validation.py
CHANGED
|
@@ -3,8 +3,140 @@ Input validation functions for logtap.
|
|
|
3
3
|
|
|
4
4
|
These functions validate user input to prevent security issues
|
|
5
5
|
like path traversal attacks and DoS via overly large inputs.
|
|
6
|
+
|
|
7
|
+
Path Traversal Prevention Model
|
|
8
|
+
===============================
|
|
9
|
+
1. Input validation: reject NUL bytes, control chars, path separators, ".."
|
|
10
|
+
2. Join filename to base directory
|
|
11
|
+
3. Resolve to canonical absolute path (follows symlinks)
|
|
12
|
+
4. Containment check: commonpath([base, resolved]) == base
|
|
13
|
+
5. File type check: must be regular file (not dir, device, etc.)
|
|
14
|
+
|
|
15
|
+
This prevents:
|
|
16
|
+
- Directory traversal (../)
|
|
17
|
+
- Absolute path injection (/etc/passwd)
|
|
18
|
+
- Symlink escape attacks
|
|
19
|
+
- Null byte injection
|
|
20
|
+
- Path prefix collisions (/var/log vs /var/logs)
|
|
6
21
|
"""
|
|
7
22
|
|
|
23
|
+
import os
|
|
24
|
+
import stat
|
|
25
|
+
from typing import Optional, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_safe_path(base_dir: str, filename: str, require_exists: bool = False) -> Optional[str]:
|
|
29
|
+
"""
|
|
30
|
+
Safely resolve a filename within a base directory.
|
|
31
|
+
|
|
32
|
+
Security guarantees:
|
|
33
|
+
- Resolved path is always within base_dir (symlink-safe)
|
|
34
|
+
- No path traversal via "..", separators, or absolute paths
|
|
35
|
+
- No NUL bytes or control characters
|
|
36
|
+
- Containment verified via os.path.commonpath
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
base_dir: The base directory that files must be within.
|
|
40
|
+
filename: The user-provided filename (single component, no path separators).
|
|
41
|
+
require_exists: If True, also verify the file exists and is a regular file.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The resolved filepath if safe, None if validation fails.
|
|
45
|
+
"""
|
|
46
|
+
# Reject empty filenames
|
|
47
|
+
if not filename:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Reject NUL bytes (can truncate paths in some contexts)
|
|
51
|
+
if "\x00" in filename:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Reject control characters (0x00-0x1F, 0x7F)
|
|
55
|
+
if any(ord(c) < 0x20 or ord(c) == 0x7F for c in filename):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Reject special directory entries
|
|
59
|
+
# Note: ".." substring check removed - it over-blocks valid names like "my..log"
|
|
60
|
+
# Traversal requires separators which we reject below; containment check is authoritative
|
|
61
|
+
if filename in {".", ".."}:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Reject path separators - filename must be a single component
|
|
65
|
+
if "/" in filename or "\\" in filename:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Reject absolute paths (Unix and Windows)
|
|
69
|
+
if filename.startswith("/") or filename.startswith("\\"):
|
|
70
|
+
return None
|
|
71
|
+
# Windows drive letters (C:, D:, etc.)
|
|
72
|
+
if len(filename) >= 2 and filename[1] == ":":
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Resolve base directory to canonical absolute form
|
|
76
|
+
base_resolved = os.path.realpath(base_dir)
|
|
77
|
+
|
|
78
|
+
# Join and resolve to canonical absolute path (follows symlinks)
|
|
79
|
+
filepath = os.path.join(base_resolved, filename)
|
|
80
|
+
filepath_resolved = os.path.realpath(filepath)
|
|
81
|
+
|
|
82
|
+
# Containment check using commonpath
|
|
83
|
+
# This is the authoritative check - handles prefix collisions correctly
|
|
84
|
+
# e.g., base=/var/log, candidate=/var/logs/evil will fail
|
|
85
|
+
try:
|
|
86
|
+
common = os.path.commonpath([base_resolved, filepath_resolved])
|
|
87
|
+
if common != base_resolved:
|
|
88
|
+
return None
|
|
89
|
+
except ValueError:
|
|
90
|
+
# Paths on different drives (Windows) or other path issues
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Optional: verify file exists and is a regular file
|
|
94
|
+
if require_exists:
|
|
95
|
+
try:
|
|
96
|
+
file_stat = os.stat(filepath_resolved)
|
|
97
|
+
if not stat.S_ISREG(file_stat.st_mode):
|
|
98
|
+
return None
|
|
99
|
+
except OSError:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
return filepath_resolved
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_safe_path_checked(base_dir: str, filename: str) -> Tuple[Optional[str], str]:
|
|
106
|
+
"""
|
|
107
|
+
Resolve a safe path and return detailed error reason if validation fails.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (resolved_path, error_reason). If path is valid, error_reason is empty.
|
|
111
|
+
"""
|
|
112
|
+
if not filename:
|
|
113
|
+
return None, "empty filename"
|
|
114
|
+
if "\x00" in filename:
|
|
115
|
+
return None, "filename contains NUL byte"
|
|
116
|
+
if any(ord(c) < 0x20 or ord(c) == 0x7F for c in filename):
|
|
117
|
+
return None, "filename contains control character"
|
|
118
|
+
if filename in {".", ".."}:
|
|
119
|
+
return None, "filename is special directory entry"
|
|
120
|
+
if "/" in filename or "\\" in filename:
|
|
121
|
+
return None, "filename contains path separator"
|
|
122
|
+
if filename.startswith("/") or filename.startswith("\\"):
|
|
123
|
+
return None, "filename is absolute path"
|
|
124
|
+
if len(filename) >= 2 and filename[1] == ":":
|
|
125
|
+
return None, "filename contains Windows drive letter"
|
|
126
|
+
|
|
127
|
+
base_resolved = os.path.realpath(base_dir)
|
|
128
|
+
filepath = os.path.join(base_resolved, filename)
|
|
129
|
+
filepath_resolved = os.path.realpath(filepath)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
common = os.path.commonpath([base_resolved, filepath_resolved])
|
|
133
|
+
if common != base_resolved:
|
|
134
|
+
return None, "resolved path escapes base directory"
|
|
135
|
+
except ValueError as e:
|
|
136
|
+
return None, f"path resolution error: {e}"
|
|
137
|
+
|
|
138
|
+
return filepath_resolved, ""
|
|
139
|
+
|
|
8
140
|
|
|
9
141
|
def is_filename_valid(filename: str) -> bool:
|
|
10
142
|
"""
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logtap
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: A CLI-first log access tool for Unix systems. Remote log file access without SSH.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cainky/logtap
|
|
6
|
+
Project-URL: Repository, https://github.com/cainky/logtap
|
|
7
|
+
Author-email: cainky <kylecain.me@gmail.com>
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,devops,logs,monitoring,sysadmin
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: System :: Logging
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Classifier: Topic :: System :: Systems Administration
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: aiofiles>=23.2.1
|
|
26
|
+
Requires-Dist: fastapi>=0.109.0
|
|
27
|
+
Requires-Dist: google-re2>=1.1
|
|
28
|
+
Requires-Dist: httpx>=0.26.0
|
|
29
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
30
|
+
Requires-Dist: pydantic>=2.5.0
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
32
|
+
Requires-Dist: rich>=13.7.0
|
|
33
|
+
Requires-Dist: typer>=0.9.0
|
|
34
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
35
|
+
Requires-Dist: websockets>=12.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pre-commit>=4.5.1; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# logtap
|
|
45
|
+
|
|
46
|
+
[](https://badge.fury.io/py/logtap)
|
|
47
|
+
[](https://github.com/cainky/logtap/actions/workflows/tests.yml)
|
|
48
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
49
|
+
[](https://www.python.org/downloads/)
|
|
50
|
+
|
|
51
|
+
**`tail -f` for GPU clouds. Survives disconnects, aggregates multi-node.**
|
|
52
|
+
|
|
53
|
+
> Stop losing your training logs when SSH drops. Watch from anywhere, reconnect seamlessly.
|
|
54
|
+
|
|
55
|
+
## The Problem
|
|
56
|
+
|
|
57
|
+
You're training a model on RunPod, Vast.ai, or Lambda. You SSH in, start training, and:
|
|
58
|
+
|
|
59
|
+
- Your terminal disconnects after an hour
|
|
60
|
+
- You lose visibility into what's happening
|
|
61
|
+
- You resort to tmux hacks just to keep logs alive
|
|
62
|
+
- Multi-node training means logs scattered across machines
|
|
63
|
+
|
|
64
|
+
## The Solution
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# On your GPU instance
|
|
68
|
+
pip install logtap
|
|
69
|
+
logtap collect &
|
|
70
|
+
python train.py 2>&1 | logtap ingest run1
|
|
71
|
+
|
|
72
|
+
# From your laptop (or phone)
|
|
73
|
+
logtap tail run1 --follow
|
|
74
|
+
|
|
75
|
+
# Connection drops... reconnects automatically
|
|
76
|
+
# "reconnected (missed 0 lines)"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quickstart: RunPod / Vast.ai
|
|
80
|
+
|
|
81
|
+
On the GPU instance:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install logtap
|
|
85
|
+
export LOGTAP_API_KEY=secret
|
|
86
|
+
logtap collect --port 8000
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Start training and stream logs:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python train.py 2>&1 | logtap ingest run1 --tag node=$(hostname)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
From your laptop:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export LOGTAP_SERVER=http://<gpu-ip>:8000
|
|
99
|
+
export LOGTAP_API_KEY=secret
|
|
100
|
+
logtap tail run1 --follow
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Disconnect, close your terminal, or switch networks.
|
|
104
|
+
Re-run `logtap tail` anytime to resume where you left off.
|
|
105
|
+
|
|
106
|
+
Works the same on RunPod, Vast.ai, Lambda, and any ephemeral GPU cloud.
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
- **Survives Disconnects** - Resume from where you left off with cursor-based streaming
|
|
111
|
+
- **Pipe-Friendly** - Works with any training script via stdin
|
|
112
|
+
- **Multi-Node Ready** - Tag runs with `node=gpu1` and filter/aggregate
|
|
113
|
+
- **Zero Infra** - No database, no complex setup, just pip install
|
|
114
|
+
- **Lightweight** - <50MB memory, append-only file storage
|
|
115
|
+
|
|
116
|
+
## Why not tmux / mosh?
|
|
117
|
+
|
|
118
|
+
tmux and mosh help keep SSH sessions alive.
|
|
119
|
+
logtap solves a different problem.
|
|
120
|
+
|
|
121
|
+
- SSH can still drop (web terminals, proxies, idle timeouts)
|
|
122
|
+
- tmux doesn't aggregate logs across machines
|
|
123
|
+
- tmux can't be viewed from another device without SSH
|
|
124
|
+
- tmux sessions die when ephemeral instances stop
|
|
125
|
+
|
|
126
|
+
logtap streams logs over HTTP:
|
|
127
|
+
- survives disconnects
|
|
128
|
+
- resumes without gaps
|
|
129
|
+
- aggregates multi-node training via tags
|
|
130
|
+
- works from anywhere (no SSH required)
|
|
131
|
+
|
|
132
|
+
You can still use tmux. You just don't have to rely on it.
|
|
133
|
+
|
|
134
|
+
## Quick Start
|
|
135
|
+
|
|
136
|
+
### 1. Install
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install logtap
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. Start Collector (on GPU instance)
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
logtap collect --api-key secret
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. Pipe Your Training Logs
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python train.py 2>&1 | logtap ingest run1 --api-key secret
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4. Tail From Anywhere
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export LOGTAP_SERVER=http://your-gpu-ip:8000
|
|
158
|
+
export LOGTAP_API_KEY=secret
|
|
159
|
+
|
|
160
|
+
logtap tail run1 --follow
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## CLI Commands
|
|
164
|
+
|
|
165
|
+
| Command | Description |
|
|
166
|
+
|---------|-------------|
|
|
167
|
+
| `logtap collect` | Start collector server (accepts ingested runs) |
|
|
168
|
+
| `logtap ingest <run>` | Pipe stdin to collector |
|
|
169
|
+
| `logtap tail <run>` | Tail a run with `--follow` for streaming |
|
|
170
|
+
| `logtap runs` | List active runs |
|
|
171
|
+
| `logtap doctor` | Check server connectivity and diagnose issues |
|
|
172
|
+
|
|
173
|
+
### Ingest Options
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Auto-generate run name
|
|
177
|
+
python train.py | logtap ingest
|
|
178
|
+
|
|
179
|
+
# Add tags for multi-node
|
|
180
|
+
python train.py | logtap ingest run1 --tag node=gpu1 --tag rank=0
|
|
181
|
+
|
|
182
|
+
# Quiet mode (no status messages)
|
|
183
|
+
python train.py | logtap ingest run1 --quiet
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Tail Options
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Follow mode (like tail -f)
|
|
190
|
+
logtap tail run1 --follow
|
|
191
|
+
|
|
192
|
+
# Resume from specific cursor (survives disconnects!)
|
|
193
|
+
logtap tail run1 --follow --since 5000
|
|
194
|
+
|
|
195
|
+
# Filter by tag
|
|
196
|
+
logtap tail run1 --tag node=gpu1
|
|
197
|
+
|
|
198
|
+
# Output formats
|
|
199
|
+
logtap tail run1 --output jsonl | jq '.line'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Collector Options
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
logtap collect \
|
|
206
|
+
--port 8000 \
|
|
207
|
+
--api-key secret \
|
|
208
|
+
--data-dir ~/.logtap/runs \
|
|
209
|
+
--max-disk-mb 5000 \
|
|
210
|
+
--retention-hours 72
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Multi-Node Training
|
|
214
|
+
|
|
215
|
+
Tag each node and aggregate:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Node 1
|
|
219
|
+
python train.py | logtap ingest run1 --tag node=gpu1
|
|
220
|
+
|
|
221
|
+
# Node 2
|
|
222
|
+
python train.py | logtap ingest run1 --tag node=gpu2
|
|
223
|
+
|
|
224
|
+
# Watch all nodes
|
|
225
|
+
logtap tail run1 --follow
|
|
226
|
+
|
|
227
|
+
# Watch specific node
|
|
228
|
+
logtap tail run1 --follow --tag node=gpu1
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Environment Variables
|
|
232
|
+
|
|
233
|
+
| Variable | Default | Description |
|
|
234
|
+
|----------|---------|-------------|
|
|
235
|
+
| `LOGTAP_SERVER` | `http://localhost:8000` | Collector URL |
|
|
236
|
+
| `LOGTAP_API_KEY` | - | API key for auth |
|
|
237
|
+
|
|
238
|
+
Set these to avoid typing `--server` and `--api-key` every time.
|
|
239
|
+
|
|
240
|
+
## How It Works
|
|
241
|
+
|
|
242
|
+
1. **Collector** writes logs to append-only files with cursor tracking
|
|
243
|
+
2. **Ingest** streams stdin over HTTP chunked POST
|
|
244
|
+
3. **Tail** uses SSE (Server-Sent Events) with resume support
|
|
245
|
+
4. **Reconnect** passes `?since=<cursor>` to continue without gaps
|
|
246
|
+
|
|
247
|
+
No database. No message queue. Just files and HTTP.
|
|
248
|
+
|
|
249
|
+
## API Endpoints
|
|
250
|
+
|
|
251
|
+
For scripting or custom integrations:
|
|
252
|
+
|
|
253
|
+
| Endpoint | Description |
|
|
254
|
+
|----------|-------------|
|
|
255
|
+
| `POST /runs/{id}/ingest` | Stream lines (chunked POST) |
|
|
256
|
+
| `GET /runs/{id}/stream` | SSE stream with `?since=&follow=` |
|
|
257
|
+
| `GET /runs/{id}/query` | Query with `?from=&to=&search=` |
|
|
258
|
+
| `GET /runs` | List runs |
|
|
259
|
+
| `GET /health` | Health check with capabilities |
|
|
260
|
+
|
|
261
|
+
## Legacy: Static File Mode
|
|
262
|
+
|
|
263
|
+
logtap also works as a simple remote log viewer (the original use case):
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
# On server with log files
|
|
267
|
+
logtap serve --log-dir /var/log
|
|
268
|
+
|
|
269
|
+
# From client
|
|
270
|
+
logtap tail syslog --server http://myserver:8000 --follow
|
|
271
|
+
logtap query auth.log --regex "Failed password"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Security
|
|
275
|
+
|
|
276
|
+
- **API Key Auth** - Optional but recommended for production
|
|
277
|
+
- **Path Traversal Protection** - Comprehensive defense with symlink-safe containment checks (see [SECURITY.md](SECURITY.md))
|
|
278
|
+
- **ReDoS Protection** - Uses google-re2 for guaranteed linear-time regex matching
|
|
279
|
+
- **Read-Only by Default** - Collector only writes to its data directory
|
|
280
|
+
- **Input Validation** - Rejects control characters, NUL bytes, and malicious path patterns
|
|
281
|
+
|
|
282
|
+
## Development
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
git clone https://github.com/cainky/logtap.git
|
|
286
|
+
cd logtap
|
|
287
|
+
|
|
288
|
+
# Install with uv
|
|
289
|
+
uv sync --extra dev
|
|
290
|
+
|
|
291
|
+
# Run tests
|
|
292
|
+
uv run pytest
|
|
293
|
+
|
|
294
|
+
# Run collector in dev mode
|
|
295
|
+
uv run logtap collect --reload
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
GPL v3 - see [LICENSE](LICENSE)
|
|
301
|
+
|
|
302
|
+
## Author
|
|
303
|
+
|
|
304
|
+
Kyle Cain - [@cainky](https://github.com/cainky)
|
|
@@ -6,24 +6,25 @@ logtap/api/dependencies.py,sha256=1cx1qrp0O6v1fHXA2JdEhC8P4caG2oUSCfMk2-8zmGs,16
|
|
|
6
6
|
logtap/api/routes/__init__.py,sha256=XYvFyTP4zKywRZH0v97k0EZCYgxdL2PSUaNet20znPE,29
|
|
7
7
|
logtap/api/routes/files.py,sha256=bqZYrX6jrF5-7GzBpUIXXoPVdxUwm6o0LTcJBLtaJUE,991
|
|
8
8
|
logtap/api/routes/health.py,sha256=s117Hr1E8OcBGPOWq2WwHLZSq35hS7wmLPk6BYq3dq4,1112
|
|
9
|
-
logtap/api/routes/logs.py,sha256=
|
|
10
|
-
logtap/api/routes/parsed.py,sha256=
|
|
11
|
-
logtap/api/routes/runs.py,sha256=
|
|
9
|
+
logtap/api/routes/logs.py,sha256=J_Sp9-Mx2g-tVdpVJMzMz1ytymtCCMJvDJ74uLeiKw0,8294
|
|
10
|
+
logtap/api/routes/parsed.py,sha256=dXIKy0JMxlvoHAbCCk4zhagHUPaWclaUBPcHXG2ew1I,3442
|
|
11
|
+
logtap/api/routes/runs.py,sha256=njyPZ90eXeAvnDWVq-q3x0slDCjUJ8FotOnl1muCKgI,10571
|
|
12
12
|
logtap/cli/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
13
|
-
logtap/cli/main.py,sha256=
|
|
13
|
+
logtap/cli/main.py,sha256=7XSeZrvCfiCcMA_sJLIIaT21ORAsYKiD_gKCSLxGmbc,1267
|
|
14
14
|
logtap/cli/commands/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
15
15
|
logtap/cli/commands/collect.py,sha256=8x6LyMrzI79wYtfLZcbQdgpy5nxPZuQOEillE9IfwwE,3002
|
|
16
|
+
logtap/cli/commands/doctor.py,sha256=fVL6ZhD52zQBqGQRwi0J_m3MaN-0SbwhxM8UZC4nG-Y,4218
|
|
16
17
|
logtap/cli/commands/files.py,sha256=WFr8kA0SdgQHz3ZyONTaljxHMcD-nQlndp3UIOwZATc,2455
|
|
17
18
|
logtap/cli/commands/ingest.py,sha256=JaItHHYV3fBmPkseYpubyHryNbuEuxyjRBk-EiiEwyU,4054
|
|
18
19
|
logtap/cli/commands/query.py,sha256=uD9nH5E-7EqJryLf3hHkDbJSQo4kWFGmzzHgTfAKFwk,3418
|
|
19
20
|
logtap/cli/commands/runs.py,sha256=Dweswku19Dj2KOFhT0kaega9KSKmUrvya3eLn0-5lXo,3632
|
|
20
21
|
logtap/cli/commands/serve.py,sha256=9OvfII21q6cel3zZfSsAsiERKwKFt0ZFTXmUd2Psthg,1910
|
|
21
|
-
logtap/cli/commands/tail.py,sha256=
|
|
22
|
+
logtap/cli/commands/tail.py,sha256=mOYGwXaXeM_PHI27BAm61CR25YAh9dpLOlq8oy0ciLM,10427
|
|
22
23
|
logtap/core/__init__.py,sha256=tsoL0XuDrPd5xHEu975WqFHoA7EQgloxrum7CjsWHuk,450
|
|
23
24
|
logtap/core/reader.py,sha256=BuBrEAbS2naCBTtuBNc0Un6thbekzabaHTBzYE1SwKg,5277
|
|
24
|
-
logtap/core/runs.py,sha256=
|
|
25
|
+
logtap/core/runs.py,sha256=lYOWAWp6RrcaMLkWxqhD0MC5alQe7rv07GtEazL5WWE,14956
|
|
25
26
|
logtap/core/search.py,sha256=rtq8WP96RYUvRkX_R5x_mdD_dw1syDuNkHx3uP_diOg,4574
|
|
26
|
-
logtap/core/validation.py,sha256=
|
|
27
|
+
logtap/core/validation.py,sha256=kRKRuhSaORslgatIQ2nW3ufDOn0mp_mB38CguhizRBY,6071
|
|
27
28
|
logtap/core/parsers/__init__.py,sha256=5f3hFxf_DgNScRDchRT8ocFVgi7Md4xuMN-ShvlssBo,575
|
|
28
29
|
logtap/core/parsers/apache.py,sha256=JjuQ4v-b7HJvTCcjbOMgv5_dSdiNVPX_EUyplc3f5Qw,5332
|
|
29
30
|
logtap/core/parsers/auto.py,sha256=OLLuX7XIxS0Upnv9FQ-_B0sGAyZmfNxjnMDGdZtUIO4,3565
|
|
@@ -34,8 +35,8 @@ logtap/core/parsers/syslog.py,sha256=gBNQ39QXsigOpfnq3cEdmvFa8NLp_wmiSMDlTt0SIbs
|
|
|
34
35
|
logtap/models/__init__.py,sha256=tce3Q0QjPhnlAYG8IcwxPedyh1ibBlKIF3CjXe5wwgo,280
|
|
35
36
|
logtap/models/config.py,sha256=8x6OR_y2ZB8SSoQWQGwDB7DXH30UyMNXUcRWOctjUn8,927
|
|
36
37
|
logtap/models/responses.py,sha256=xKdKdS85soxMYGNad3WfF0pOG0Pb5Z7XwVrwK-TCnHs,4084
|
|
37
|
-
logtap-0.4.
|
|
38
|
-
logtap-0.4.
|
|
39
|
-
logtap-0.4.
|
|
40
|
-
logtap-0.4.
|
|
41
|
-
logtap-0.4.
|
|
38
|
+
logtap-0.4.1.dist-info/METADATA,sha256=Mz7i6uIVOv9mHtQ2oMgKSOVH5oVtpjO7s3N9u7ZSUP8,8203
|
|
39
|
+
logtap-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
40
|
+
logtap-0.4.1.dist-info/entry_points.txt,sha256=tuAit8kt97yjtACQKvN35wWozp4KhSju_gfDhSS1IrM,47
|
|
41
|
+
logtap-0.4.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
42
|
+
logtap-0.4.1.dist-info/RECORD,,
|