logler 1.0.2__tar.gz → 1.0.7__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.
Files changed (42) hide show
  1. {logler-1.0.2 → logler-1.0.7}/Cargo.lock +21 -20
  2. {logler-1.0.2 → logler-1.0.7}/Cargo.toml +1 -1
  3. {logler-1.0.2 → logler-1.0.7}/PKG-INFO +1 -1
  4. {logler-1.0.2 → logler-1.0.7}/pyproject.toml +2 -2
  5. {logler-1.0.2 → logler-1.0.7}/src/logler/__init__.py +1 -1
  6. {logler-1.0.2 → logler-1.0.7}/src/logler/investigate.py +9 -7
  7. {logler-1.0.2 → logler-1.0.7}/src/logler/llm_cli.py +6 -4
  8. logler-1.0.7/src/logler/safe_regex.py +124 -0
  9. {logler-1.0.2 → logler-1.0.7}/src/logler/web/app.py +29 -5
  10. {logler-1.0.2 → logler-1.0.7}/LICENSE +0 -0
  11. {logler-1.0.2 → logler-1.0.7}/README.md +0 -0
  12. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/Cargo.toml +0 -0
  13. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/filter.rs +0 -0
  14. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/hierarchy.rs +0 -0
  15. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/index.rs +0 -0
  16. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/investigate.rs +0 -0
  17. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/lib.rs +0 -0
  18. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/parser.rs +0 -0
  19. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/reader.rs +0 -0
  20. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/sql.rs +0 -0
  21. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/stats.rs +0 -0
  22. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/thread_tracker.rs +0 -0
  23. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/trace.rs +0 -0
  24. {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/types.rs +0 -0
  25. {logler-1.0.2 → logler-1.0.7}/crates/logler-py/Cargo.toml +0 -0
  26. {logler-1.0.2 → logler-1.0.7}/crates/logler-py/src/lib.rs +0 -0
  27. {logler-1.0.2 → logler-1.0.7}/src/logler/bootstrap.py +0 -0
  28. {logler-1.0.2 → logler-1.0.7}/src/logler/cache.py +0 -0
  29. {logler-1.0.2 → logler-1.0.7}/src/logler/cli.py +0 -0
  30. {logler-1.0.2 → logler-1.0.7}/src/logler/helpers.py +0 -0
  31. {logler-1.0.2 → logler-1.0.7}/src/logler/log_reader.py +0 -0
  32. {logler-1.0.2 → logler-1.0.7}/src/logler/parser.py +0 -0
  33. {logler-1.0.2 → logler-1.0.7}/src/logler/terminal.py +0 -0
  34. {logler-1.0.2 → logler-1.0.7}/src/logler/tracker.py +0 -0
  35. {logler-1.0.2 → logler-1.0.7}/src/logler/tree_formatter.py +0 -0
  36. {logler-1.0.2 → logler-1.0.7}/src/logler/watcher.py +0 -0
  37. {logler-1.0.2 → logler-1.0.7}/src/logler/web/__init__.py +0 -0
  38. {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/css/tailwind.css +0 -0
  39. {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/css/tailwind.input.css +0 -0
  40. {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/logler-logo.png +0 -0
  41. {logler-1.0.2 → logler-1.0.7}/src/logler/web/tailwind.config.cjs +0 -0
  42. {logler-1.0.2 → logler-1.0.7}/src/logler/web/templates/index.html +0 -0
@@ -547,9 +547,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
547
547
 
548
548
  [[package]]
549
549
  name = "chrono"
550
- version = "0.4.42"
550
+ version = "0.4.43"
551
551
  source = "registry+https://github.com/rust-lang/crates.io-index"
552
- checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
552
+ checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
553
553
  dependencies = [
554
554
  "iana-time-zone",
555
555
  "js-sys",
@@ -1480,9 +1480,9 @@ dependencies = [
1480
1480
 
1481
1481
  [[package]]
1482
1482
  name = "js-sys"
1483
- version = "0.3.83"
1483
+ version = "0.3.85"
1484
1484
  source = "registry+https://github.com/rust-lang/crates.io-index"
1485
- checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
1485
+ checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
1486
1486
  dependencies = [
1487
1487
  "once_cell",
1488
1488
  "wasm-bindgen",
@@ -1640,7 +1640,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
1640
1640
 
1641
1641
  [[package]]
1642
1642
  name = "logler-cli"
1643
- version = "1.0.2"
1643
+ version = "1.0.6"
1644
1644
  dependencies = [
1645
1645
  "anyhow",
1646
1646
  "clap",
@@ -1655,7 +1655,7 @@ dependencies = [
1655
1655
 
1656
1656
  [[package]]
1657
1657
  name = "logler-core"
1658
- version = "1.0.2"
1658
+ version = "1.0.6"
1659
1659
  dependencies = [
1660
1660
  "anyhow",
1661
1661
  "async-stream",
@@ -1680,7 +1680,7 @@ dependencies = [
1680
1680
 
1681
1681
  [[package]]
1682
1682
  name = "logler-py"
1683
- version = "1.0.2"
1683
+ version = "1.0.6"
1684
1684
  dependencies = [
1685
1685
  "anyhow",
1686
1686
  "logler-core",
@@ -1692,7 +1692,7 @@ dependencies = [
1692
1692
 
1693
1693
  [[package]]
1694
1694
  name = "logler-server"
1695
- version = "1.0.2"
1695
+ version = "1.0.6"
1696
1696
  dependencies = [
1697
1697
  "anyhow",
1698
1698
  "axum",
@@ -3244,9 +3244,9 @@ dependencies = [
3244
3244
 
3245
3245
  [[package]]
3246
3246
  name = "wasm-bindgen"
3247
- version = "0.2.106"
3247
+ version = "0.2.108"
3248
3248
  source = "registry+https://github.com/rust-lang/crates.io-index"
3249
- checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
3249
+ checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
3250
3250
  dependencies = [
3251
3251
  "cfg-if",
3252
3252
  "once_cell",
@@ -3257,11 +3257,12 @@ dependencies = [
3257
3257
 
3258
3258
  [[package]]
3259
3259
  name = "wasm-bindgen-futures"
3260
- version = "0.4.56"
3260
+ version = "0.4.58"
3261
3261
  source = "registry+https://github.com/rust-lang/crates.io-index"
3262
- checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
3262
+ checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
3263
3263
  dependencies = [
3264
3264
  "cfg-if",
3265
+ "futures-util",
3265
3266
  "js-sys",
3266
3267
  "once_cell",
3267
3268
  "wasm-bindgen",
@@ -3270,9 +3271,9 @@ dependencies = [
3270
3271
 
3271
3272
  [[package]]
3272
3273
  name = "wasm-bindgen-macro"
3273
- version = "0.2.106"
3274
+ version = "0.2.108"
3274
3275
  source = "registry+https://github.com/rust-lang/crates.io-index"
3275
- checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
3276
+ checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
3276
3277
  dependencies = [
3277
3278
  "quote",
3278
3279
  "wasm-bindgen-macro-support",
@@ -3280,9 +3281,9 @@ dependencies = [
3280
3281
 
3281
3282
  [[package]]
3282
3283
  name = "wasm-bindgen-macro-support"
3283
- version = "0.2.106"
3284
+ version = "0.2.108"
3284
3285
  source = "registry+https://github.com/rust-lang/crates.io-index"
3285
- checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
3286
+ checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
3286
3287
  dependencies = [
3287
3288
  "bumpalo",
3288
3289
  "proc-macro2",
@@ -3293,18 +3294,18 @@ dependencies = [
3293
3294
 
3294
3295
  [[package]]
3295
3296
  name = "wasm-bindgen-shared"
3296
- version = "0.2.106"
3297
+ version = "0.2.108"
3297
3298
  source = "registry+https://github.com/rust-lang/crates.io-index"
3298
- checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
3299
+ checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
3299
3300
  dependencies = [
3300
3301
  "unicode-ident",
3301
3302
  ]
3302
3303
 
3303
3304
  [[package]]
3304
3305
  name = "web-sys"
3305
- version = "0.3.83"
3306
+ version = "0.3.85"
3306
3307
  source = "registry+https://github.com/rust-lang/crates.io-index"
3307
- checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
3308
+ checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
3308
3309
  dependencies = [
3309
3310
  "js-sys",
3310
3311
  "wasm-bindgen",
@@ -3,7 +3,7 @@ members = ["crates/logler-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "1.0.2"
6
+ version = "1.0.6"
7
7
  edition = "2021"
8
8
  authors = ["Logler Contributors"]
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logler
3
- Version: 1.0.2
3
+ Version: 1.0.7
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "logler"
7
- version = "1.0.2"
7
+ version = "1.0.7"
8
8
  description = "Beautiful local log viewer with thread tracking and real-time updates"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -69,7 +69,7 @@ logler = ["web/templates/**/*", "web/static/**/*"]
69
69
  manifest-path = "crates/logler-py/Cargo.toml"
70
70
  python-source = "src"
71
71
  python-packages = ["logler"]
72
- features = ["sql"]
72
+ # features passed via CLI in CI (sql not supported on Windows)
73
73
  include = ["LICENSE", "README.md"]
74
74
 
75
75
  [tool.black]
@@ -2,7 +2,7 @@
2
2
  Logler - Beautiful local log viewer with thread tracking and real-time updates.
3
3
  """
4
4
 
5
- __version__ = "1.0.0"
5
+ __version__ = "1.0.7"
6
6
  __author__ = "Logler Contributors"
7
7
 
8
8
  from .parser import LogParser, LogEntry
@@ -30,10 +30,13 @@ Example Usage:
30
30
 
31
31
  import json
32
32
  import re
33
+ import warnings
33
34
  from typing import List, Optional, Dict, Any, Tuple
34
35
  from datetime import datetime
35
36
  from collections import defaultdict
36
37
 
38
+ from .safe_regex import try_compile
39
+
37
40
  try:
38
41
  import logler_rs
39
42
 
@@ -48,10 +51,10 @@ except ImportError:
48
51
  RUST_AVAILABLE = True
49
52
  else:
50
53
  RUST_AVAILABLE = False
51
- print("Warning: Rust backend not available. Using Python fallback.")
52
- except Exception:
54
+ warnings.warn("Rust backend not available. Using Python fallback.", stacklevel=2)
55
+ except (ImportError, AttributeError, OSError):
53
56
  RUST_AVAILABLE = False
54
- print("Warning: Rust backend not available. Using Python fallback.")
57
+ warnings.warn("Rust backend not available. Using Python fallback.", stacklevel=2)
55
58
 
56
59
 
57
60
  def _normalize_entry(entry: Dict[str, Any]) -> None:
@@ -97,9 +100,8 @@ def _apply_custom_regex_to_results(result: Dict[str, Any], pattern: Optional[str
97
100
  """Apply a user-provided regex to fill missing fields like timestamp/level."""
98
101
  if not pattern:
99
102
  return
100
- try:
101
- regex = re.compile(pattern)
102
- except re.error:
103
+ regex = try_compile(pattern)
104
+ if regex is None:
103
105
  return
104
106
 
105
107
  for item in result.get("results", []) or []:
@@ -2198,7 +2200,7 @@ def cross_service_timeline(
2198
2200
  e for e in all_entries if e["timestamp"] and start_dt <= e["timestamp"] <= end_dt
2199
2201
  ]
2200
2202
  except Exception as e:
2201
- print(f"Warning: Could not parse time window: {e}")
2203
+ warnings.warn(f"Could not parse time window: {e}", stacklevel=2)
2202
2204
 
2203
2205
  # Sort by timestamp
2204
2206
  all_entries.sort(key=lambda e: e["timestamp"] if e["timestamp"] else datetime.min)
@@ -18,6 +18,8 @@ from typing import Optional, List, Dict, Any
18
18
  from datetime import datetime, timedelta
19
19
  from collections import defaultdict
20
20
 
21
+ from .safe_regex import safe_compile, RegexTimeoutError, RegexPatternTooLongError
22
+
21
23
  # Exit codes
22
24
  EXIT_SUCCESS = 0 # Success with results
23
25
  EXIT_NO_RESULTS = 1 # Success but no results found
@@ -808,8 +810,8 @@ def verify_pattern(
808
810
  _error_json(f"No files found matching: {files}")
809
811
 
810
812
  try:
811
- regex = re.compile(pattern)
812
- except re.error as e:
813
+ regex = safe_compile(pattern)
814
+ except (re.error, RegexTimeoutError, RegexPatternTooLongError) as e:
813
815
  _error_json(f"Invalid regex pattern: {e}")
814
816
 
815
817
  parser = LogParser()
@@ -950,8 +952,8 @@ def emit(
950
952
  query_regex = None
951
953
  if query:
952
954
  try:
953
- query_regex = re.compile(query, re.IGNORECASE)
954
- except re.error as e:
955
+ query_regex = safe_compile(query, re.IGNORECASE)
956
+ except (re.error, RegexTimeoutError, RegexPatternTooLongError) as e:
955
957
  _error_json(f"Invalid regex pattern: {e}")
956
958
 
957
959
  for file_path in file_list:
@@ -0,0 +1,124 @@
1
+ """
2
+ Safe regex compilation with timeout protection against ReDoS attacks.
3
+
4
+ This module provides a safe_compile function that wraps re.compile with:
5
+ - Pattern length validation
6
+ - Compilation timeout (Unix only, graceful fallback on Windows)
7
+ - Clear error messages
8
+ """
9
+
10
+ import re
11
+ import threading
12
+ from typing import Optional
13
+
14
+
15
+ class RegexTimeoutError(Exception):
16
+ """Raised when regex compilation times out."""
17
+
18
+ pass
19
+
20
+
21
+ class RegexPatternTooLongError(Exception):
22
+ """Raised when regex pattern exceeds maximum allowed length."""
23
+
24
+ pass
25
+
26
+
27
+ # Maximum pattern length to prevent ReDoS via complexity
28
+ MAX_PATTERN_LENGTH = 1000
29
+
30
+ # Timeout for regex compilation in seconds
31
+ COMPILE_TIMEOUT = 2.0
32
+
33
+
34
+ def _compile_with_timeout(
35
+ pattern: str,
36
+ flags: int,
37
+ timeout: float,
38
+ result_container: dict,
39
+ ) -> None:
40
+ """Worker function for threaded compilation."""
41
+ try:
42
+ result_container["result"] = re.compile(pattern, flags)
43
+ except re.error as e:
44
+ result_container["error"] = e
45
+ except Exception as e:
46
+ result_container["error"] = e
47
+
48
+
49
+ def safe_compile(
50
+ pattern: str,
51
+ flags: int = 0,
52
+ timeout: float = COMPILE_TIMEOUT,
53
+ max_length: int = MAX_PATTERN_LENGTH,
54
+ ) -> re.Pattern:
55
+ """
56
+ Safely compile a regex pattern with timeout protection.
57
+
58
+ Args:
59
+ pattern: The regex pattern to compile
60
+ flags: Optional regex flags (e.g., re.IGNORECASE)
61
+ timeout: Maximum time in seconds to allow for compilation
62
+ max_length: Maximum allowed pattern length
63
+
64
+ Returns:
65
+ Compiled regex pattern
66
+
67
+ Raises:
68
+ RegexPatternTooLongError: If pattern exceeds max_length
69
+ RegexTimeoutError: If compilation takes longer than timeout
70
+ re.error: If the pattern is invalid
71
+ """
72
+ # Validate pattern length
73
+ if len(pattern) > max_length:
74
+ raise RegexPatternTooLongError(
75
+ f"Regex pattern length {len(pattern)} exceeds maximum {max_length}"
76
+ )
77
+
78
+ # Use threading for cross-platform timeout support
79
+ result_container: dict = {}
80
+ thread = threading.Thread(
81
+ target=_compile_with_timeout,
82
+ args=(pattern, flags, timeout, result_container),
83
+ )
84
+ thread.start()
85
+ thread.join(timeout=timeout)
86
+
87
+ if thread.is_alive():
88
+ # Thread is still running - compilation timed out
89
+ # Note: We can't actually kill the thread, but we return an error
90
+ raise RegexTimeoutError(
91
+ f"Regex compilation timed out after {timeout}s (pattern may cause catastrophic backtracking)"
92
+ )
93
+
94
+ if "error" in result_container:
95
+ raise result_container["error"]
96
+
97
+ return result_container["result"]
98
+
99
+
100
+ def try_compile(
101
+ pattern: str,
102
+ flags: int = 0,
103
+ timeout: float = COMPILE_TIMEOUT,
104
+ max_length: int = MAX_PATTERN_LENGTH,
105
+ ) -> Optional[re.Pattern]:
106
+ """
107
+ Try to compile a regex pattern safely, returning None on failure.
108
+
109
+ This is a convenience wrapper around safe_compile that catches all
110
+ exceptions and returns None instead.
111
+
112
+ Args:
113
+ pattern: The regex pattern to compile
114
+ flags: Optional regex flags
115
+ timeout: Maximum compilation time
116
+ max_length: Maximum pattern length
117
+
118
+ Returns:
119
+ Compiled pattern or None if compilation fails
120
+ """
121
+ try:
122
+ return safe_compile(pattern, flags, timeout, max_length)
123
+ except (RegexTimeoutError, RegexPatternTooLongError, re.error):
124
+ return None
@@ -36,6 +36,22 @@ def _ensure_within_root(path: Path) -> Path:
36
36
  raise HTTPException(status_code=403, detail="Requested path is outside the configured log root")
37
37
 
38
38
 
39
+ def _sanitize_glob_pattern(pattern: str) -> str:
40
+ """Remove path traversal sequences from glob patterns."""
41
+ import re as _re
42
+
43
+ # Remove any ../ or ..\ sequences that could escape the root
44
+ # Use a loop to handle multiple consecutive traversal attempts like ../../
45
+ while ".." in pattern:
46
+ old_pattern = pattern
47
+ pattern = _re.sub(r"\.\.[\\/]", "", pattern)
48
+ pattern = _re.sub(r"[\\/]\.\.", "", pattern)
49
+ pattern = _re.sub(r"^\.\.", "", pattern) # Leading ..
50
+ if pattern == old_pattern:
51
+ break # No more changes possible
52
+ return pattern
53
+
54
+
39
55
  def _glob_within_root(pattern: str) -> List[Path]:
40
56
  """
41
57
  Run a glob pattern scoped to LOG_ROOT, returning file paths only.
@@ -43,6 +59,9 @@ def _glob_within_root(pattern: str) -> List[Path]:
43
59
  if not pattern:
44
60
  return []
45
61
 
62
+ # Sanitize the pattern to prevent path traversal
63
+ pattern = _sanitize_glob_pattern(pattern)
64
+
46
65
  # Normalize relative patterns to LOG_ROOT
47
66
  raw_pattern = pattern
48
67
  if not Path(pattern).is_absolute():
@@ -168,14 +187,14 @@ def _rust_filter(
168
187
  ) -> Optional[List[Dict[str, Any]]]:
169
188
  try:
170
189
  import logler_rs # type: ignore
171
- except Exception:
190
+ except ImportError:
172
191
  return None
173
192
 
174
193
  try:
175
194
  from ..cache import get_cached_investigator
176
195
 
177
196
  inv = get_cached_investigator(files)
178
- except Exception:
197
+ except (ImportError, AttributeError, TypeError):
179
198
  inv = logler_rs.PyInvestigator()
180
199
  inv.load_files(files)
181
200
 
@@ -226,7 +245,8 @@ def _rust_filter(
226
245
  if track:
227
246
  _track_entries(entries)
228
247
  return entries
229
- except Exception:
248
+ except (json.JSONDecodeError, KeyError, ValueError, AttributeError, RuntimeError):
249
+ # Rust filter failed - fall back to Python implementation
230
250
  return None
231
251
 
232
252
 
@@ -646,7 +666,10 @@ async def websocket_endpoint(websocket: WebSocket):
646
666
  await follow_file(websocket, file_path, current_filters, drop_count)
647
667
 
648
668
  except WebSocketDisconnect:
649
- websocket_clients.remove(websocket)
669
+ pass # Normal disconnect
670
+ finally:
671
+ if websocket in websocket_clients:
672
+ websocket_clients.remove(websocket)
650
673
 
651
674
 
652
675
  async def follow_file(
@@ -667,7 +690,8 @@ async def follow_file(
667
690
  with open(path, "r") as f:
668
691
  f.seek(0, 2)
669
692
  position = f.tell()
670
- line_number = sum(1 for _ in open(path))
693
+ with open(path, "r") as f:
694
+ line_number = sum(1 for _ in f)
671
695
 
672
696
  # Follow file
673
697
  try:
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