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.
- {logler-1.0.2 → logler-1.0.7}/Cargo.lock +21 -20
- {logler-1.0.2 → logler-1.0.7}/Cargo.toml +1 -1
- {logler-1.0.2 → logler-1.0.7}/PKG-INFO +1 -1
- {logler-1.0.2 → logler-1.0.7}/pyproject.toml +2 -2
- {logler-1.0.2 → logler-1.0.7}/src/logler/__init__.py +1 -1
- {logler-1.0.2 → logler-1.0.7}/src/logler/investigate.py +9 -7
- {logler-1.0.2 → logler-1.0.7}/src/logler/llm_cli.py +6 -4
- logler-1.0.7/src/logler/safe_regex.py +124 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/app.py +29 -5
- {logler-1.0.2 → logler-1.0.7}/LICENSE +0 -0
- {logler-1.0.2 → logler-1.0.7}/README.md +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/Cargo.toml +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/filter.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/hierarchy.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/index.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/investigate.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/lib.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/parser.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/reader.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/sql.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/stats.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/thread_tracker.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/trace.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-core/src/types.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-py/Cargo.toml +0 -0
- {logler-1.0.2 → logler-1.0.7}/crates/logler-py/src/lib.rs +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/bootstrap.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/cache.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/cli.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/helpers.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/log_reader.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/parser.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/terminal.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/tracker.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/tree_formatter.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/watcher.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/__init__.py +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/css/tailwind.css +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/css/tailwind.input.css +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/static/logler-logo.png +0 -0
- {logler-1.0.2 → logler-1.0.7}/src/logler/web/tailwind.config.cjs +0 -0
- {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.
|
|
550
|
+
version = "0.4.43"
|
|
551
551
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
552
|
-
checksum = "
|
|
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.
|
|
1483
|
+
version = "0.3.85"
|
|
1484
1484
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1485
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
3247
|
+
version = "0.2.108"
|
|
3248
3248
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3249
|
-
checksum = "
|
|
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.
|
|
3260
|
+
version = "0.4.58"
|
|
3261
3261
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3262
|
-
checksum = "
|
|
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.
|
|
3274
|
+
version = "0.2.108"
|
|
3274
3275
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3275
|
-
checksum = "
|
|
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.
|
|
3284
|
+
version = "0.2.108"
|
|
3284
3285
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3285
|
-
checksum = "
|
|
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.
|
|
3297
|
+
version = "0.2.108"
|
|
3297
3298
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3298
|
-
checksum = "
|
|
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.
|
|
3306
|
+
version = "0.3.85"
|
|
3306
3307
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3307
|
-
checksum = "
|
|
3308
|
+
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
|
3308
3309
|
dependencies = [
|
|
3309
3310
|
"js-sys",
|
|
3310
3311
|
"wasm-bindgen",
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "logler"
|
|
7
|
-
version = "1.0.
|
|
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
|
|
72
|
+
# features passed via CLI in CI (sql not supported on Windows)
|
|
73
73
|
include = ["LICENSE", "README.md"]
|
|
74
74
|
|
|
75
75
|
[tool.black]
|
|
@@ -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
|
-
|
|
52
|
-
except
|
|
54
|
+
warnings.warn("Rust backend not available. Using Python fallback.", stacklevel=2)
|
|
55
|
+
except (ImportError, AttributeError, OSError):
|
|
53
56
|
RUST_AVAILABLE = False
|
|
54
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|