logtap 0.2.2__py3-none-any.whl → 0.3.0__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/__init__.py +1 -1
- logtap/api/routes/logs.py +2 -2
- logtap/core/parsers/base.py +3 -1
- logtap/core/parsers/json_parser.py +11 -0
- logtap/core/reader.py +3 -5
- logtap/core/search.py +15 -11
- {logtap-0.2.2.dist-info → logtap-0.3.0.dist-info}/METADATA +23 -21
- {logtap-0.2.2.dist-info → logtap-0.3.0.dist-info}/RECORD +13 -13
- {logtap-0.2.2.dist-info → logtap-0.3.0.dist-info}/WHEEL +1 -1
- logtap-0.3.0.dist-info/entry_points.txt +2 -0
- logtap-0.2.2.dist-info/entry_points.txt +0 -3
- {logtap-0.2.2.dist-info → logtap-0.3.0.dist-info}/licenses/LICENSE +0 -0
logtap/__init__.py
CHANGED
logtap/api/routes/logs.py
CHANGED
|
@@ -180,7 +180,7 @@ async def stream_logs(
|
|
|
180
180
|
return
|
|
181
181
|
|
|
182
182
|
try:
|
|
183
|
-
async with aiofiles.open(filepath, mode="r", encoding="utf-8") as f:
|
|
183
|
+
async with aiofiles.open(filepath, mode="r", encoding="utf-8", errors="replace") as f:
|
|
184
184
|
# Seek to end of file
|
|
185
185
|
await f.seek(0, 2)
|
|
186
186
|
|
|
@@ -225,7 +225,7 @@ async def stream_logs_sse(
|
|
|
225
225
|
filepath = get_filepath(filename, settings)
|
|
226
226
|
|
|
227
227
|
async def event_generator():
|
|
228
|
-
async with aiofiles.open(filepath, mode="r", encoding="utf-8") as f:
|
|
228
|
+
async with aiofiles.open(filepath, mode="r", encoding="utf-8", errors="replace") as f:
|
|
229
229
|
# Seek to end
|
|
230
230
|
await f.seek(0, 2)
|
|
231
231
|
|
logtap/core/parsers/base.py
CHANGED
|
@@ -136,12 +136,14 @@ class LogParser(ABC):
|
|
|
136
136
|
"""
|
|
137
137
|
return [self.parse(line) for line in lines]
|
|
138
138
|
|
|
139
|
-
def _detect_level_from_content(self, content:
|
|
139
|
+
def _detect_level_from_content(self, content: Any) -> Optional[LogLevel]:
|
|
140
140
|
"""
|
|
141
141
|
Detect log level from message content.
|
|
142
142
|
|
|
143
143
|
Common patterns like "ERROR:", "[error]", etc.
|
|
144
144
|
"""
|
|
145
|
+
if not isinstance(content, str):
|
|
146
|
+
return None
|
|
145
147
|
content_lower = content.lower()
|
|
146
148
|
|
|
147
149
|
# Check for explicit level indicators
|
|
@@ -50,8 +50,19 @@ class JsonLogParser(LogParser):
|
|
|
50
50
|
level=self._detect_level_from_content(line),
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
+
# Only handle JSON objects (dicts), not arrays or primitives
|
|
54
|
+
if not isinstance(data, dict):
|
|
55
|
+
return ParsedLogEntry(
|
|
56
|
+
raw=line,
|
|
57
|
+
message=line,
|
|
58
|
+
level=self._detect_level_from_content(line),
|
|
59
|
+
)
|
|
60
|
+
|
|
53
61
|
# Extract message
|
|
54
62
|
message = self._get_field(data, self.MESSAGE_FIELDS, line)
|
|
63
|
+
# Ensure message is a string for level detection
|
|
64
|
+
if not isinstance(message, str):
|
|
65
|
+
message = str(message) if message is not None else line
|
|
55
66
|
|
|
56
67
|
# Extract level
|
|
57
68
|
level_str = self._get_field(data, self.LEVEL_FIELDS)
|
logtap/core/reader.py
CHANGED
|
@@ -27,7 +27,7 @@ def tail(filename: str, lines_limit: int = 50, block_size: int = 1024) -> List[s
|
|
|
27
27
|
A list of the last 'lines_limit' lines in the file.
|
|
28
28
|
"""
|
|
29
29
|
lines: List[str] = []
|
|
30
|
-
with open(filename, "r", encoding="utf-8") as f:
|
|
30
|
+
with open(filename, "r", encoding="utf-8", errors="replace") as f:
|
|
31
31
|
# Seek to the end of the file.
|
|
32
32
|
f.seek(0, SEEK_END)
|
|
33
33
|
# Get the current position in the file.
|
|
@@ -70,9 +70,7 @@ def read_block(file: IO, block_end_byte: int, block_size: int) -> Tuple[List[str
|
|
|
70
70
|
return lines, block_end_byte
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
async def tail_async(
|
|
74
|
-
filename: str, lines_limit: int = 50, block_size: int = 1024
|
|
75
|
-
) -> List[str]:
|
|
73
|
+
async def tail_async(filename: str, lines_limit: int = 50, block_size: int = 1024) -> List[str]:
|
|
76
74
|
"""
|
|
77
75
|
Async version of tail() for use with FastAPI.
|
|
78
76
|
|
|
@@ -87,7 +85,7 @@ async def tail_async(
|
|
|
87
85
|
A list of the last 'lines_limit' lines in the file.
|
|
88
86
|
"""
|
|
89
87
|
lines: List[str] = []
|
|
90
|
-
async with aiofiles.open(filename, "r", encoding="utf-8") as f:
|
|
88
|
+
async with aiofiles.open(filename, "r", encoding="utf-8", errors="replace") as f:
|
|
91
89
|
# Seek to the end of the file.
|
|
92
90
|
await f.seek(0, SEEK_END)
|
|
93
91
|
# Get the current position in the file.
|
logtap/core/search.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Search and filtering functionality for logtap.
|
|
3
3
|
|
|
4
4
|
Provides substring, regex-based, and severity-based filtering of log lines.
|
|
5
|
+
Uses google-re2 for regex matching to prevent ReDoS attacks.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
|
-
import re
|
|
8
8
|
from typing import List, Optional, Set
|
|
9
9
|
|
|
10
|
+
import re2
|
|
11
|
+
|
|
10
12
|
from logtap.core.parsers.base import LogLevel, ParsedLogEntry
|
|
11
13
|
|
|
12
14
|
|
|
@@ -19,6 +21,9 @@ def filter_lines(
|
|
|
19
21
|
"""
|
|
20
22
|
Filter lines by substring or regex pattern.
|
|
21
23
|
|
|
24
|
+
Uses google-re2 for regex matching, which guarantees linear time
|
|
25
|
+
complexity and is immune to ReDoS attacks.
|
|
26
|
+
|
|
22
27
|
Args:
|
|
23
28
|
lines: List of log lines to filter.
|
|
24
29
|
term: Substring to search for. If provided, only lines containing
|
|
@@ -35,11 +40,12 @@ def filter_lines(
|
|
|
35
40
|
return lines
|
|
36
41
|
|
|
37
42
|
if regex:
|
|
38
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
39
43
|
try:
|
|
40
|
-
|
|
44
|
+
options = re2.Options()
|
|
45
|
+
options.case_sensitive = case_sensitive
|
|
46
|
+
pattern = re2.compile(regex, options)
|
|
41
47
|
return [line for line in lines if pattern.search(line)]
|
|
42
|
-
except
|
|
48
|
+
except re2.error:
|
|
43
49
|
# Invalid regex, return empty list
|
|
44
50
|
return []
|
|
45
51
|
|
|
@@ -85,10 +91,7 @@ def filter_by_level(
|
|
|
85
91
|
|
|
86
92
|
if min_level:
|
|
87
93
|
min_severity = min_level.severity
|
|
88
|
-
return [
|
|
89
|
-
e for e in entries
|
|
90
|
-
if e.level is not None and e.level.severity <= min_severity
|
|
91
|
-
]
|
|
94
|
+
return [e for e in entries if e.level is not None and e.level.severity <= min_severity]
|
|
92
95
|
|
|
93
96
|
return entries
|
|
94
97
|
|
|
@@ -126,11 +129,12 @@ def filter_entries(
|
|
|
126
129
|
# Apply text filter
|
|
127
130
|
if term or regex:
|
|
128
131
|
if regex:
|
|
129
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
130
132
|
try:
|
|
131
|
-
|
|
133
|
+
options = re2.Options()
|
|
134
|
+
options.case_sensitive = case_sensitive
|
|
135
|
+
pattern = re2.compile(regex, options)
|
|
132
136
|
result = [e for e in result if pattern.search(e.message)]
|
|
133
|
-
except
|
|
137
|
+
except re2.error:
|
|
134
138
|
result = []
|
|
135
139
|
elif term:
|
|
136
140
|
if case_sensitive:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logtap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
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>
|
|
5
8
|
License: GPL-3.0-or-later
|
|
6
9
|
License-File: LICENSE
|
|
7
|
-
Keywords: logs,monitoring,
|
|
8
|
-
Author: cainky
|
|
9
|
-
Author-email: kylecain.me@gmail.com
|
|
10
|
-
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Keywords: cli,devops,logs,monitoring,sysadmin
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,27 +15,30 @@ Classifier: Intended Audience :: System Administrators
|
|
|
15
15
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
-
Classifier: Programming Language :: Python :: 3
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
24
21
|
Classifier: Topic :: System :: Logging
|
|
25
22
|
Classifier: Topic :: System :: Monitoring
|
|
26
23
|
Classifier: Topic :: System :: Systems Administration
|
|
27
|
-
Requires-
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
|
|
38
|
-
|
|
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'
|
|
39
42
|
Description-Content-Type: text/markdown
|
|
40
43
|
|
|
41
44
|
# logtap
|
|
@@ -314,4 +317,3 @@ Contributions are welcome! Please open an issue to discuss potential changes bef
|
|
|
314
317
|
## Author
|
|
315
318
|
|
|
316
319
|
Kyle Cain - [@cainky](https://github.com/cainky)
|
|
317
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
logtap/__init__.py,sha256=
|
|
1
|
+
logtap/__init__.py,sha256=QGputf7HjtjMkCnDHsPNBBqZ27DYZ-7a2_biF5Rvk6c,179
|
|
2
2
|
logtap/__main__.py,sha256=vqJPz3Zf-ICn_4P3B1o7U4NxcCo0qWgfAGEE_j13t-c,138
|
|
3
3
|
logtap/api/__init__.py,sha256=80bP-eIxtAzidgv5nzcfyCEdl8EI2QjVY_eyxjvvhA0,98
|
|
4
4
|
logtap/api/app.py,sha256=BBphxLKvk7yIiLyQ3tdiagqtjc7xFixd_FUzNFjlC0c,1176
|
|
@@ -6,31 +6,31 @@ 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=Ak-z2ChqZZ7FgHdu1JDo3v5aDBPR3VIICyXTLDBf75E,462
|
|
9
|
-
logtap/api/routes/logs.py,sha256=
|
|
9
|
+
logtap/api/routes/logs.py,sha256=XpRAd4fZmVyylz6bHCHm4y0Y2GofSquH6j5WJP3Jyao,8467
|
|
10
10
|
logtap/api/routes/parsed.py,sha256=XVvkKBE_hQvfJyrDBBPR_PpVxvof-y4B77xKe9Rr0Qk,3367
|
|
11
11
|
logtap/cli/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
12
|
+
logtap/cli/main.py,sha256=fWSuQdin9G-RC7Oqzesfp93WZI1-v7227P-WWTsxtIQ,1045
|
|
12
13
|
logtap/cli/commands/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
13
14
|
logtap/cli/commands/files.py,sha256=WFr8kA0SdgQHz3ZyONTaljxHMcD-nQlndp3UIOwZATc,2455
|
|
14
15
|
logtap/cli/commands/query.py,sha256=uD9nH5E-7EqJryLf3hHkDbJSQo4kWFGmzzHgTfAKFwk,3418
|
|
15
16
|
logtap/cli/commands/serve.py,sha256=9OvfII21q6cel3zZfSsAsiERKwKFt0ZFTXmUd2Psthg,1910
|
|
16
17
|
logtap/cli/commands/tail.py,sha256=dwPRXub1dcRwKulDt_qNa2waQm1YPOxIg0QokAK6Gyw,3648
|
|
17
|
-
logtap/cli/main.py,sha256=fWSuQdin9G-RC7Oqzesfp93WZI1-v7227P-WWTsxtIQ,1045
|
|
18
18
|
logtap/core/__init__.py,sha256=tsoL0XuDrPd5xHEu975WqFHoA7EQgloxrum7CjsWHuk,450
|
|
19
|
+
logtap/core/reader.py,sha256=BuBrEAbS2naCBTtuBNc0Un6thbekzabaHTBzYE1SwKg,5277
|
|
20
|
+
logtap/core/search.py,sha256=rtq8WP96RYUvRkX_R5x_mdD_dw1syDuNkHx3uP_diOg,4574
|
|
21
|
+
logtap/core/validation.py,sha256=Nk86jHqEfI4H96fk-1rjbC5sBwfzls43hyOhnRV6rxI,1359
|
|
19
22
|
logtap/core/parsers/__init__.py,sha256=5f3hFxf_DgNScRDchRT8ocFVgi7Md4xuMN-ShvlssBo,575
|
|
20
23
|
logtap/core/parsers/apache.py,sha256=JjuQ4v-b7HJvTCcjbOMgv5_dSdiNVPX_EUyplc3f5Qw,5332
|
|
21
24
|
logtap/core/parsers/auto.py,sha256=OLLuX7XIxS0Upnv9FQ-_B0sGAyZmfNxjnMDGdZtUIO4,3565
|
|
22
|
-
logtap/core/parsers/base.py,sha256=
|
|
23
|
-
logtap/core/parsers/json_parser.py,sha256=
|
|
25
|
+
logtap/core/parsers/base.py,sha256=AVTk64djuIxih2mav3N25V_ldLXYTc68JIchH7ZVd3g,4894
|
|
26
|
+
logtap/core/parsers/json_parser.py,sha256=AEWKfKnFdMVmtImcJqtXYZjyW2TKnla6YwXomGFpXr4,4169
|
|
24
27
|
logtap/core/parsers/nginx.py,sha256=j_oILELOM0azDPLc41wXrLu5o_LhnPs9fT0_iaOqqAQ,3526
|
|
25
28
|
logtap/core/parsers/syslog.py,sha256=gBNQ39QXsigOpfnq3cEdmvFa8NLp_wmiSMDlTt0SIbs,2430
|
|
26
|
-
logtap/core/reader.py,sha256=YY3wyV9NvYiskTh2nrl1tZAZNY11fr33vo85OhW1qHc,5247
|
|
27
|
-
logtap/core/search.py,sha256=-P2KLkjTWANyLQhdxqZc3I-0UpbhWjQQK_yHHxwYJaA,4350
|
|
28
|
-
logtap/core/validation.py,sha256=Nk86jHqEfI4H96fk-1rjbC5sBwfzls43hyOhnRV6rxI,1359
|
|
29
29
|
logtap/models/__init__.py,sha256=tce3Q0QjPhnlAYG8IcwxPedyh1ibBlKIF3CjXe5wwgo,280
|
|
30
30
|
logtap/models/config.py,sha256=8x6OR_y2ZB8SSoQWQGwDB7DXH30UyMNXUcRWOctjUn8,927
|
|
31
31
|
logtap/models/responses.py,sha256=45J-Xw1Gb35uP5188wxy2QZlyy3Fh18fpAFizZnpi3A,1850
|
|
32
|
-
logtap-0.
|
|
33
|
-
logtap-0.
|
|
34
|
-
logtap-0.
|
|
35
|
-
logtap-0.
|
|
36
|
-
logtap-0.
|
|
32
|
+
logtap-0.3.0.dist-info/METADATA,sha256=O1zoWpiUNGvVxjSLfAGPfZKnxgy4CFZV1p3_olMe7rM,7466
|
|
33
|
+
logtap-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
logtap-0.3.0.dist-info/entry_points.txt,sha256=tuAit8kt97yjtACQKvN35wWozp4KhSju_gfDhSS1IrM,47
|
|
35
|
+
logtap-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
36
|
+
logtap-0.3.0.dist-info/RECORD,,
|
|
File without changes
|