logtap 0.2.1__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 CHANGED
@@ -4,5 +4,5 @@ logtap - A CLI-first log access tool for Unix systems.
4
4
  Remote log file access without SSH. No database. No complex setup.
5
5
  """
6
6
 
7
- __version__ = "0.1.0"
7
+ __version__ = "0.3.0"
8
8
  __author__ = "Kyle Cain"
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
 
@@ -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: str) -> Optional[LogLevel]:
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
- pattern = re.compile(regex, flags)
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 re.error:
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
- pattern = re.compile(regex, flags)
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 re.error:
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.2.1
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,cli,devops,sysadmin
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-Dist: aiofiles (>=23.2.1)
28
- Requires-Dist: fastapi (>=0.109.0)
29
- Requires-Dist: httpx (>=0.26.0)
30
- Requires-Dist: pydantic (>=2.5.0)
31
- Requires-Dist: pydantic-settings (>=2.1.0)
32
- Requires-Dist: python-dotenv (>=1.0.1)
33
- Requires-Dist: rich (>=13.7.0)
34
- Requires-Dist: typer[all] (>=0.9.0)
35
- Requires-Dist: uvicorn[standard] (>=0.27.0)
36
- Requires-Dist: websockets (>=12.0)
37
- Project-URL: Homepage, https://github.com/cainky/logtap
38
- Project-URL: Repository, https://github.com/cainky/logtap
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=AZzOTH4EFgrz3x5g_i6HELBi5Cg8pOXqCWbJbCWnl2A,179
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=rIc9pkn7YtVjf0mAluJztVocJ_P9THR280KTEDN1zqE,8431
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=HK886mB2mOuRwsVit_U8UgSfrrQOYwMt_XQm-bgso40,4829
23
- logtap/core/parsers/json_parser.py,sha256=_3uMGT0EW0wwKzXk1Uc9A7iNvey0Ijqr0uRnJuU1_XM,3728
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.2.1.dist-info/METADATA,sha256=5q1nTrBXAX__SwglKI9zN761dC81NZ9zmmZeE2sdAps,7372
33
- logtap-0.2.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
34
- logtap-0.2.1.dist-info/entry_points.txt,sha256=NsN54PsyqB2TA7b8W9-nH4_CNl2_biG4i4FhOQAU9rQ,46
35
- logtap-0.2.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
36
- logtap-0.2.1.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ logtap = logtap.cli.main:app
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- logtap=logtap.cli.main:app
3
-