logscope-cli 0.4.1__tar.gz → 0.4.3__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.
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/LICENSE +21 -21
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/PKG-INFO +1 -1
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/logscope/__init__.py +4 -4
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/logscope/parser.py +188 -188
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/logscope/themes.py +160 -160
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/pyproject.toml +1 -1
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/README.md +0 -0
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/logscope/cli.py +0 -0
- {logscope_cli-0.4.1 → logscope_cli-0.4.3}/logscope/viewer.py +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 João Vinny
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João Vinny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LogScope — Beautiful log viewer for the terminal
|
|
3
|
-
"""
|
|
4
|
-
__version__ = "0.4.
|
|
1
|
+
"""
|
|
2
|
+
LogScope — Beautiful log viewer for the terminal
|
|
3
|
+
"""
|
|
4
|
+
__version__ = "0.4.3"
|
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import json
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import Optional, Tuple
|
|
6
|
-
|
|
7
|
-
# Compiled regex patterns for performance
|
|
8
|
-
_BRACKET_LEVEL_PATTERN = re.compile(
|
|
9
|
-
r'\[(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|ERR|CRITICAL|ALERT|FATAL|EMERGENCY)\]',
|
|
10
|
-
re.IGNORECASE
|
|
11
|
-
)
|
|
12
|
-
_BRACKETLESS_LEVEL_PATTERN = re.compile(
|
|
13
|
-
r'\b(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|ERR|CRITICAL|ALERT|FATAL|EMERGENCY)\b',
|
|
14
|
-
re.IGNORECASE
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
# Multiple timestamp patterns for different log formats
|
|
18
|
-
_TIMESTAMP_PATTERNS = [
|
|
19
|
-
# ISO 8601: 2026-03-21T10:00:00Z or 2026-03-21T10:00:00.123Z or 2026-03-21T10:00:00+00:00
|
|
20
|
-
re.compile(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)'),
|
|
21
|
-
# ISO-like with space: 2026-03-21 10:00:00 or 2026-03-21 10:00:00.123
|
|
22
|
-
re.compile(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)'),
|
|
23
|
-
# Common Log Format / Apache: 21/Mar/2026:10:00:00 +0000 or [21/Mar/2026:10:00:00 +0000]
|
|
24
|
-
re.compile(r'(\d{2}/[A-Za-z]{3}/\d{4}:\d{2}:\d{2}:\d{2}(?:\s+[+-]\d{4})?)'),
|
|
25
|
-
# Syslog-style: Mar 21 10:00:00 (year is assumed current year)
|
|
26
|
-
re.compile(r'([A-Za-z]{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})'),
|
|
27
|
-
# Unix timestamp: 1711054800 (10 digits for seconds)
|
|
28
|
-
re.compile(r'\b(\d{10})\b'),
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
# Month name mapping for parsing
|
|
32
|
-
_MONTH_MAP = {
|
|
33
|
-
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
|
34
|
-
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@dataclass
|
|
38
|
-
class LogEntry:
|
|
39
|
-
level: str
|
|
40
|
-
message: str
|
|
41
|
-
raw: str
|
|
42
|
-
timestamp: Optional[datetime] = None
|
|
43
|
-
service: Optional[str] = None
|
|
44
|
-
trace_id: Optional[str] = None
|
|
45
|
-
span_id: Optional[str] = None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Level normalization constants
|
|
49
|
-
_NORMALIZE_LEVEL_MAP = {
|
|
50
|
-
"WARNING": "WARN",
|
|
51
|
-
"EMERGENCY": "FATAL",
|
|
52
|
-
"ERR": "ERROR",
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _normalize_level(level: str) -> str:
|
|
57
|
-
"""Normalize log level aliases to canonical forms."""
|
|
58
|
-
return _NORMALIZE_LEVEL_MAP.get(level.upper(), level.upper())
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _extract_json_observability(data: dict) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
62
|
-
"""Pull service / trace / span from common JSON log shapes (K8s, OTel, Docker)."""
|
|
63
|
-
k8s = data.get("kubernetes")
|
|
64
|
-
k8s_d: dict = k8s if isinstance(k8s, dict) else {}
|
|
65
|
-
pod_name = k8s_d.get("pod_name")
|
|
66
|
-
if not pod_name and isinstance(k8s_d.get("pod"), dict):
|
|
67
|
-
pod_name = k8s_d["pod"].get("name")
|
|
68
|
-
|
|
69
|
-
service = (
|
|
70
|
-
data.get("service")
|
|
71
|
-
or data.get("service.name")
|
|
72
|
-
or data.get("service_name")
|
|
73
|
-
or pod_name
|
|
74
|
-
or k8s_d.get("container_name")
|
|
75
|
-
or data.get("container")
|
|
76
|
-
or data.get("container.name")
|
|
77
|
-
or data.get("logger")
|
|
78
|
-
or data.get("logger.name")
|
|
79
|
-
)
|
|
80
|
-
if service is not None:
|
|
81
|
-
service = str(service)
|
|
82
|
-
|
|
83
|
-
trace_id = data.get("trace_id") or data.get("traceId") or data.get("trace.id")
|
|
84
|
-
if not trace_id and isinstance(data.get("trace"), dict):
|
|
85
|
-
trace_id = data["trace"].get("id")
|
|
86
|
-
if not trace_id and isinstance(data.get("otelTraceID"), str):
|
|
87
|
-
trace_id = data["otelTraceID"]
|
|
88
|
-
if trace_id is not None:
|
|
89
|
-
trace_id = str(trace_id)
|
|
90
|
-
|
|
91
|
-
span_id = data.get("span_id") or data.get("spanId") or data.get("span.id")
|
|
92
|
-
if span_id is not None:
|
|
93
|
-
span_id = str(span_id)
|
|
94
|
-
|
|
95
|
-
return service, trace_id, span_id
|
|
96
|
-
|
|
97
|
-
def parse_line(line: str) -> LogEntry:
|
|
98
|
-
"""Parse a single line of log and extract severity level."""
|
|
99
|
-
line = line.strip()
|
|
100
|
-
|
|
101
|
-
# 1. Check if JSON log object (common in docker/kubernetes/modern APIs)
|
|
102
|
-
if line.startswith('{') and line.endswith('}'):
|
|
103
|
-
try:
|
|
104
|
-
data = json.loads(line)
|
|
105
|
-
# Find level key
|
|
106
|
-
level = _normalize_level(data.get('level', data.get('severity', data.get('log.level', 'UNKNOWN'))))
|
|
107
|
-
# Find message key
|
|
108
|
-
message = str(data.get('message', data.get('msg', data.get('text', line))))
|
|
109
|
-
|
|
110
|
-
# Find timestamp
|
|
111
|
-
timestamp_str = data.get('timestamp', data.get('time', data.get('@timestamp')))
|
|
112
|
-
timestamp = None
|
|
113
|
-
if timestamp_str:
|
|
114
|
-
try:
|
|
115
|
-
# Basic ISO parsing
|
|
116
|
-
timestamp = datetime.fromisoformat(str(timestamp_str).replace('Z', '+00:00'))
|
|
117
|
-
except ValueError:
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
|
-
svc, tid, sid = _extract_json_observability(data)
|
|
121
|
-
return LogEntry(
|
|
122
|
-
level=level,
|
|
123
|
-
message=message,
|
|
124
|
-
raw=line,
|
|
125
|
-
timestamp=timestamp,
|
|
126
|
-
service=svc,
|
|
127
|
-
trace_id=tid,
|
|
128
|
-
span_id=sid,
|
|
129
|
-
)
|
|
130
|
-
except json.JSONDecodeError:
|
|
131
|
-
pass
|
|
132
|
-
|
|
133
|
-
# 2. Try typical log formats like [INFO], (WARN), ERROR:
|
|
134
|
-
match = _BRACKET_LEVEL_PATTERN.search(line)
|
|
135
|
-
if not match:
|
|
136
|
-
# Try finding without brackets as a fallback, e.g. "INFO:" or "INFO - "
|
|
137
|
-
match = _BRACKETLESS_LEVEL_PATTERN.search(line)
|
|
138
|
-
|
|
139
|
-
if match:
|
|
140
|
-
level = _normalize_level(match.group(1))
|
|
141
|
-
|
|
142
|
-
# Remove the [LEVEL] part from the message for cleaner display
|
|
143
|
-
message = line.replace(match.group(0), '', 1).strip()
|
|
144
|
-
|
|
145
|
-
# Clean up common separators left behind like ": " or "- "
|
|
146
|
-
if message.startswith(':') or message.startswith('-'):
|
|
147
|
-
message = message[1:].strip()
|
|
148
|
-
|
|
149
|
-
return LogEntry(level=level, message=message, raw=line, timestamp=extract_timestamp(line))
|
|
150
|
-
|
|
151
|
-
# fallback
|
|
152
|
-
return LogEntry(level="UNKNOWN", message=line, raw=line, timestamp=extract_timestamp(line))
|
|
153
|
-
|
|
154
|
-
def extract_timestamp(text: str) -> Optional[datetime]:
|
|
155
|
-
"""Extract a timestamp from a raw string using multiple format patterns."""
|
|
156
|
-
for pattern in _TIMESTAMP_PATTERNS:
|
|
157
|
-
match = pattern.search(text)
|
|
158
|
-
if match:
|
|
159
|
-
ts_str = match.group(1)
|
|
160
|
-
try:
|
|
161
|
-
# Try ISO format first (handles most cases)
|
|
162
|
-
if '-' in ts_str and ('T' in ts_str or ts_str[10:11] == ' '):
|
|
163
|
-
# Handle ISO-like with space instead of T
|
|
164
|
-
return datetime.fromisoformat(ts_str.replace('Z', '+00:00').replace(' ', 'T'))
|
|
165
|
-
# Handle Common Log Format: 21/Mar/2026:10:00:00 +0000
|
|
166
|
-
elif '/' in ts_str:
|
|
167
|
-
parts = ts_str.split()
|
|
168
|
-
main_part = parts[0]
|
|
169
|
-
# Parse: DD/Mon/YYYY:HH:MM:SS
|
|
170
|
-
match_parts = re.match(r'(\d{2})/([A-Za-z]{3})/(\d{4}):(\d{2}):(\d{2}):(\d{2})', main_part)
|
|
171
|
-
if match_parts:
|
|
172
|
-
day, month_str, year, hour, minute, second = match_parts.groups()
|
|
173
|
-
month = _MONTH_MAP.get(month_str, 1)
|
|
174
|
-
return datetime(int(year), month, int(day), int(hour), int(minute), int(second))
|
|
175
|
-
# Handle Syslog-style: Mar 21 10:00:00
|
|
176
|
-
elif ts_str[0].isalpha():
|
|
177
|
-
match_parts = re.match(r'([A-Za-z]{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})', ts_str)
|
|
178
|
-
if match_parts:
|
|
179
|
-
month_str, day, hour, minute, second = match_parts.groups()
|
|
180
|
-
month = _MONTH_MAP.get(month_str, 1)
|
|
181
|
-
year = datetime.now().year # Assume current year
|
|
182
|
-
return datetime(year, month, int(day), int(hour), int(minute), int(second))
|
|
183
|
-
# Handle Unix timestamp
|
|
184
|
-
elif ts_str.isdigit():
|
|
185
|
-
return datetime.fromtimestamp(int(ts_str))
|
|
186
|
-
except (ValueError, OSError):
|
|
187
|
-
continue
|
|
188
|
-
return None
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
# Compiled regex patterns for performance
|
|
8
|
+
_BRACKET_LEVEL_PATTERN = re.compile(
|
|
9
|
+
r'\[(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|ERR|CRITICAL|ALERT|FATAL|EMERGENCY)\]',
|
|
10
|
+
re.IGNORECASE
|
|
11
|
+
)
|
|
12
|
+
_BRACKETLESS_LEVEL_PATTERN = re.compile(
|
|
13
|
+
r'\b(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|ERR|CRITICAL|ALERT|FATAL|EMERGENCY)\b',
|
|
14
|
+
re.IGNORECASE
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Multiple timestamp patterns for different log formats
|
|
18
|
+
_TIMESTAMP_PATTERNS = [
|
|
19
|
+
# ISO 8601: 2026-03-21T10:00:00Z or 2026-03-21T10:00:00.123Z or 2026-03-21T10:00:00+00:00
|
|
20
|
+
re.compile(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)'),
|
|
21
|
+
# ISO-like with space: 2026-03-21 10:00:00 or 2026-03-21 10:00:00.123
|
|
22
|
+
re.compile(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)'),
|
|
23
|
+
# Common Log Format / Apache: 21/Mar/2026:10:00:00 +0000 or [21/Mar/2026:10:00:00 +0000]
|
|
24
|
+
re.compile(r'(\d{2}/[A-Za-z]{3}/\d{4}:\d{2}:\d{2}:\d{2}(?:\s+[+-]\d{4})?)'),
|
|
25
|
+
# Syslog-style: Mar 21 10:00:00 (year is assumed current year)
|
|
26
|
+
re.compile(r'([A-Za-z]{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})'),
|
|
27
|
+
# Unix timestamp: 1711054800 (10 digits for seconds)
|
|
28
|
+
re.compile(r'\b(\d{10})\b'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Month name mapping for parsing
|
|
32
|
+
_MONTH_MAP = {
|
|
33
|
+
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
|
34
|
+
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class LogEntry:
|
|
39
|
+
level: str
|
|
40
|
+
message: str
|
|
41
|
+
raw: str
|
|
42
|
+
timestamp: Optional[datetime] = None
|
|
43
|
+
service: Optional[str] = None
|
|
44
|
+
trace_id: Optional[str] = None
|
|
45
|
+
span_id: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Level normalization constants
|
|
49
|
+
_NORMALIZE_LEVEL_MAP = {
|
|
50
|
+
"WARNING": "WARN",
|
|
51
|
+
"EMERGENCY": "FATAL",
|
|
52
|
+
"ERR": "ERROR",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _normalize_level(level: str) -> str:
|
|
57
|
+
"""Normalize log level aliases to canonical forms."""
|
|
58
|
+
return _NORMALIZE_LEVEL_MAP.get(level.upper(), level.upper())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_json_observability(data: dict) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
62
|
+
"""Pull service / trace / span from common JSON log shapes (K8s, OTel, Docker)."""
|
|
63
|
+
k8s = data.get("kubernetes")
|
|
64
|
+
k8s_d: dict = k8s if isinstance(k8s, dict) else {}
|
|
65
|
+
pod_name = k8s_d.get("pod_name")
|
|
66
|
+
if not pod_name and isinstance(k8s_d.get("pod"), dict):
|
|
67
|
+
pod_name = k8s_d["pod"].get("name")
|
|
68
|
+
|
|
69
|
+
service = (
|
|
70
|
+
data.get("service")
|
|
71
|
+
or data.get("service.name")
|
|
72
|
+
or data.get("service_name")
|
|
73
|
+
or pod_name
|
|
74
|
+
or k8s_d.get("container_name")
|
|
75
|
+
or data.get("container")
|
|
76
|
+
or data.get("container.name")
|
|
77
|
+
or data.get("logger")
|
|
78
|
+
or data.get("logger.name")
|
|
79
|
+
)
|
|
80
|
+
if service is not None:
|
|
81
|
+
service = str(service)
|
|
82
|
+
|
|
83
|
+
trace_id = data.get("trace_id") or data.get("traceId") or data.get("trace.id")
|
|
84
|
+
if not trace_id and isinstance(data.get("trace"), dict):
|
|
85
|
+
trace_id = data["trace"].get("id")
|
|
86
|
+
if not trace_id and isinstance(data.get("otelTraceID"), str):
|
|
87
|
+
trace_id = data["otelTraceID"]
|
|
88
|
+
if trace_id is not None:
|
|
89
|
+
trace_id = str(trace_id)
|
|
90
|
+
|
|
91
|
+
span_id = data.get("span_id") or data.get("spanId") or data.get("span.id")
|
|
92
|
+
if span_id is not None:
|
|
93
|
+
span_id = str(span_id)
|
|
94
|
+
|
|
95
|
+
return service, trace_id, span_id
|
|
96
|
+
|
|
97
|
+
def parse_line(line: str) -> LogEntry:
|
|
98
|
+
"""Parse a single line of log and extract severity level."""
|
|
99
|
+
line = line.strip()
|
|
100
|
+
|
|
101
|
+
# 1. Check if JSON log object (common in docker/kubernetes/modern APIs)
|
|
102
|
+
if line.startswith('{') and line.endswith('}'):
|
|
103
|
+
try:
|
|
104
|
+
data = json.loads(line)
|
|
105
|
+
# Find level key
|
|
106
|
+
level = _normalize_level(data.get('level', data.get('severity', data.get('log.level', 'UNKNOWN'))))
|
|
107
|
+
# Find message key
|
|
108
|
+
message = str(data.get('message', data.get('msg', data.get('text', line))))
|
|
109
|
+
|
|
110
|
+
# Find timestamp
|
|
111
|
+
timestamp_str = data.get('timestamp', data.get('time', data.get('@timestamp')))
|
|
112
|
+
timestamp = None
|
|
113
|
+
if timestamp_str:
|
|
114
|
+
try:
|
|
115
|
+
# Basic ISO parsing
|
|
116
|
+
timestamp = datetime.fromisoformat(str(timestamp_str).replace('Z', '+00:00'))
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
svc, tid, sid = _extract_json_observability(data)
|
|
121
|
+
return LogEntry(
|
|
122
|
+
level=level,
|
|
123
|
+
message=message,
|
|
124
|
+
raw=line,
|
|
125
|
+
timestamp=timestamp,
|
|
126
|
+
service=svc,
|
|
127
|
+
trace_id=tid,
|
|
128
|
+
span_id=sid,
|
|
129
|
+
)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# 2. Try typical log formats like [INFO], (WARN), ERROR:
|
|
134
|
+
match = _BRACKET_LEVEL_PATTERN.search(line)
|
|
135
|
+
if not match:
|
|
136
|
+
# Try finding without brackets as a fallback, e.g. "INFO:" or "INFO - "
|
|
137
|
+
match = _BRACKETLESS_LEVEL_PATTERN.search(line)
|
|
138
|
+
|
|
139
|
+
if match:
|
|
140
|
+
level = _normalize_level(match.group(1))
|
|
141
|
+
|
|
142
|
+
# Remove the [LEVEL] part from the message for cleaner display
|
|
143
|
+
message = line.replace(match.group(0), '', 1).strip()
|
|
144
|
+
|
|
145
|
+
# Clean up common separators left behind like ": " or "- "
|
|
146
|
+
if message.startswith(':') or message.startswith('-'):
|
|
147
|
+
message = message[1:].strip()
|
|
148
|
+
|
|
149
|
+
return LogEntry(level=level, message=message, raw=line, timestamp=extract_timestamp(line))
|
|
150
|
+
|
|
151
|
+
# fallback
|
|
152
|
+
return LogEntry(level="UNKNOWN", message=line, raw=line, timestamp=extract_timestamp(line))
|
|
153
|
+
|
|
154
|
+
def extract_timestamp(text: str) -> Optional[datetime]:
|
|
155
|
+
"""Extract a timestamp from a raw string using multiple format patterns."""
|
|
156
|
+
for pattern in _TIMESTAMP_PATTERNS:
|
|
157
|
+
match = pattern.search(text)
|
|
158
|
+
if match:
|
|
159
|
+
ts_str = match.group(1)
|
|
160
|
+
try:
|
|
161
|
+
# Try ISO format first (handles most cases)
|
|
162
|
+
if '-' in ts_str and ('T' in ts_str or ts_str[10:11] == ' '):
|
|
163
|
+
# Handle ISO-like with space instead of T
|
|
164
|
+
return datetime.fromisoformat(ts_str.replace('Z', '+00:00').replace(' ', 'T'))
|
|
165
|
+
# Handle Common Log Format: 21/Mar/2026:10:00:00 +0000
|
|
166
|
+
elif '/' in ts_str:
|
|
167
|
+
parts = ts_str.split()
|
|
168
|
+
main_part = parts[0]
|
|
169
|
+
# Parse: DD/Mon/YYYY:HH:MM:SS
|
|
170
|
+
match_parts = re.match(r'(\d{2})/([A-Za-z]{3})/(\d{4}):(\d{2}):(\d{2}):(\d{2})', main_part)
|
|
171
|
+
if match_parts:
|
|
172
|
+
day, month_str, year, hour, minute, second = match_parts.groups()
|
|
173
|
+
month = _MONTH_MAP.get(month_str, 1)
|
|
174
|
+
return datetime(int(year), month, int(day), int(hour), int(minute), int(second))
|
|
175
|
+
# Handle Syslog-style: Mar 21 10:00:00
|
|
176
|
+
elif ts_str[0].isalpha():
|
|
177
|
+
match_parts = re.match(r'([A-Za-z]{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})', ts_str)
|
|
178
|
+
if match_parts:
|
|
179
|
+
month_str, day, hour, minute, second = match_parts.groups()
|
|
180
|
+
month = _MONTH_MAP.get(month_str, 1)
|
|
181
|
+
year = datetime.now().year # Assume current year
|
|
182
|
+
return datetime(year, month, int(day), int(hour), int(minute), int(second))
|
|
183
|
+
# Handle Unix timestamp
|
|
184
|
+
elif ts_str.isdigit():
|
|
185
|
+
return datetime.fromtimestamp(int(ts_str))
|
|
186
|
+
except (ValueError, OSError):
|
|
187
|
+
continue
|
|
188
|
+
return None
|
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
# Default themes and mappings for LogScope
|
|
2
|
-
|
|
3
|
-
DEFAULT_THEMES = {
|
|
4
|
-
"default": {
|
|
5
|
-
"levels": {
|
|
6
|
-
"TRACE": ("🔍", "dim white"),
|
|
7
|
-
"DEBUG": ("🐛", "bold blue"),
|
|
8
|
-
"INFO": ("🔵", "bold green"),
|
|
9
|
-
"NOTICE": ("🔔", "bold cyan"),
|
|
10
|
-
"WARN": ("🟡", "bold yellow"),
|
|
11
|
-
"ERROR": ("🔴", "bold red"),
|
|
12
|
-
"CRITICAL": ("💥", "bold magenta"),
|
|
13
|
-
"ALERT": ("🚨", "bold color(208)"),
|
|
14
|
-
"FATAL": ("💀", "bold dark_red"),
|
|
15
|
-
"UNKNOWN": ("⚪", "dim white")
|
|
16
|
-
},
|
|
17
|
-
"highlights": {
|
|
18
|
-
"logscope.ip": "bold green",
|
|
19
|
-
"logscope.url": "underline blue",
|
|
20
|
-
"logscope.timestamp": "cyan",
|
|
21
|
-
"logscope.uuid": "bold magenta",
|
|
22
|
-
"logscope.email": "underline yellow",
|
|
23
|
-
"logscope.path": "dim blue",
|
|
24
|
-
"logscope.status_ok": "bold green",
|
|
25
|
-
"logscope.status_warn": "bold yellow",
|
|
26
|
-
"logscope.status_err": "bold red",
|
|
27
|
-
"logscope.method": "bold cyan"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"neon": {
|
|
31
|
-
"levels": {
|
|
32
|
-
"TRACE": ("·", "magenta"),
|
|
33
|
-
"DEBUG": ("⚡", "bold magenta"),
|
|
34
|
-
"INFO": ("✨", "bold pink1"),
|
|
35
|
-
"NOTICE": ("🌌", "bold purple"),
|
|
36
|
-
"WARN": ("🔥", "bold yellow1"),
|
|
37
|
-
"ERROR": ("🧨", "bold red1"),
|
|
38
|
-
"CRITICAL": ("⬢", "bold magenta"),
|
|
39
|
-
"ALERT": ("📣", "bold orange1"),
|
|
40
|
-
"FATAL": ("☣️", "bold dark_red"),
|
|
41
|
-
"UNKNOWN": ("?", "dim white")
|
|
42
|
-
},
|
|
43
|
-
"highlights": {
|
|
44
|
-
"logscope.ip": "bold pink1",
|
|
45
|
-
"logscope.url": "italic underline magenta",
|
|
46
|
-
"logscope.timestamp": "bold purple",
|
|
47
|
-
"logscope.uuid": "bold yellow",
|
|
48
|
-
"logscope.email": "bold pink1",
|
|
49
|
-
"logscope.path": "italic magenta",
|
|
50
|
-
"logscope.status_ok": "bold pink1",
|
|
51
|
-
"logscope.status_warn": "bold yellow",
|
|
52
|
-
"logscope.status_err": "bold red",
|
|
53
|
-
"logscope.method": "bold cyan"
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"ocean": {
|
|
57
|
-
"levels": {
|
|
58
|
-
"TRACE": ("⚓", "dim cyan"),
|
|
59
|
-
"DEBUG": ("🌊", "blue"),
|
|
60
|
-
"INFO": ("💧", "bold blue"),
|
|
61
|
-
"NOTICE": ("🐳", "cyan"),
|
|
62
|
-
"WARN": ("🐚", "yellow"),
|
|
63
|
-
"ERROR": ("🌋", "red"),
|
|
64
|
-
"CRITICAL": ("⛈️ ", "bold blue"),
|
|
65
|
-
"ALERT": ("🚩", "bold orange1"),
|
|
66
|
-
"FATAL": ("🌪️ ", "bold red"),
|
|
67
|
-
"UNKNOWN": ("?", "dim cyan")
|
|
68
|
-
},
|
|
69
|
-
"highlights": {
|
|
70
|
-
"logscope.ip": "bold cyan",
|
|
71
|
-
"logscope.url": "underline deep_sky_blue1",
|
|
72
|
-
"logscope.timestamp": "blue",
|
|
73
|
-
"logscope.uuid": "bold deep_sky_blue3",
|
|
74
|
-
"logscope.email": "underline cyan",
|
|
75
|
-
"logscope.path": "dim cyan",
|
|
76
|
-
"logscope.status_ok": "bold green",
|
|
77
|
-
"logscope.status_warn": "bold yellow",
|
|
78
|
-
"logscope.status_err": "bold red",
|
|
79
|
-
"logscope.method": "bold cyan"
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"forest": {
|
|
83
|
-
"levels": {
|
|
84
|
-
"TRACE": ("🍃", "dim green"),
|
|
85
|
-
"DEBUG": ("🌿", "green"),
|
|
86
|
-
"INFO": ("🌳", "bold green"),
|
|
87
|
-
"NOTICE": ("🍄", "magenta"),
|
|
88
|
-
"WARN": ("🍂", "yellow"),
|
|
89
|
-
"ERROR": ("🪵", "red"),
|
|
90
|
-
"CRITICAL": ("🌲", "bold white"),
|
|
91
|
-
"ALERT": ("🐺", "bold orange1"),
|
|
92
|
-
"FATAL": ("💀", "bold dark_red"),
|
|
93
|
-
"UNKNOWN": ("?", "dim green")
|
|
94
|
-
},
|
|
95
|
-
"highlights": {
|
|
96
|
-
"logscope.ip": "bold green3",
|
|
97
|
-
"logscope.url": "underline dark_green",
|
|
98
|
-
"logscope.timestamp": "yellow4",
|
|
99
|
-
"logscope.uuid": "bold orange4",
|
|
100
|
-
"logscope.email": "underline green",
|
|
101
|
-
"logscope.path": "dim green3",
|
|
102
|
-
"logscope.status_ok": "bold green",
|
|
103
|
-
"logscope.status_warn": "bold yellow",
|
|
104
|
-
"logscope.status_err": "bold red",
|
|
105
|
-
"logscope.method": "bold green1"
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
"minimal": {
|
|
109
|
-
"levels": {
|
|
110
|
-
"TRACE": (" ", "dim"),
|
|
111
|
-
"DEBUG": (" ", "dim"),
|
|
112
|
-
"INFO": (" ", "white"),
|
|
113
|
-
"NOTICE": ("!", "cyan"),
|
|
114
|
-
"WARN": ("?", "yellow"),
|
|
115
|
-
"ERROR": ("#", "red"),
|
|
116
|
-
"CRITICAL": ("*", "bold red"),
|
|
117
|
-
"ALERT": ("!", "bold yellow"),
|
|
118
|
-
"FATAL": ("X", "bold red"),
|
|
119
|
-
"UNKNOWN": ("?", "dim")
|
|
120
|
-
},
|
|
121
|
-
"highlights": {
|
|
122
|
-
"logscope.ip": "bold",
|
|
123
|
-
"logscope.url": "underline",
|
|
124
|
-
"logscope.timestamp": "dim",
|
|
125
|
-
"logscope.uuid": "dim",
|
|
126
|
-
"logscope.email": "underline",
|
|
127
|
-
"logscope.path": "italic",
|
|
128
|
-
"logscope.status_ok": "green",
|
|
129
|
-
"logscope.status_warn": "yellow",
|
|
130
|
-
"logscope.status_err": "red",
|
|
131
|
-
"logscope.method": "bold"
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
"spectra": {
|
|
135
|
-
"levels": {
|
|
136
|
-
"TRACE": ("◦", "dim cyan"),
|
|
137
|
-
"DEBUG": ("◇", "cyan"),
|
|
138
|
-
"INFO": ("◆", "bold bright_cyan"),
|
|
139
|
-
"NOTICE": ("▹", "bold turquoise2"),
|
|
140
|
-
"WARN": ("⚠", "bold bright_yellow"),
|
|
141
|
-
"ERROR": ("✖", "bold bright_red"),
|
|
142
|
-
"CRITICAL": ("⬢", "bold magenta"),
|
|
143
|
-
"ALERT": ("⚡", "bold color(214)"),
|
|
144
|
-
"FATAL": ("☠", "bold red3"),
|
|
145
|
-
"UNKNOWN": ("·", "dim white")
|
|
146
|
-
},
|
|
147
|
-
"highlights": {
|
|
148
|
-
"logscope.ip": "bold bright_cyan",
|
|
149
|
-
"logscope.url": "underline bright_magenta",
|
|
150
|
-
"logscope.timestamp": "bold cyan",
|
|
151
|
-
"logscope.uuid": "bold bright_white",
|
|
152
|
-
"logscope.email": "underline turquoise2",
|
|
153
|
-
"logscope.path": "italic cyan",
|
|
154
|
-
"logscope.status_ok": "bold spring_green1",
|
|
155
|
-
"logscope.status_warn": "bold gold1",
|
|
156
|
-
"logscope.status_err": "bold red1",
|
|
157
|
-
"logscope.method": "bold deep_sky_blue1"
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
}
|
|
1
|
+
# Default themes and mappings for LogScope
|
|
2
|
+
|
|
3
|
+
DEFAULT_THEMES = {
|
|
4
|
+
"default": {
|
|
5
|
+
"levels": {
|
|
6
|
+
"TRACE": ("🔍", "dim white"),
|
|
7
|
+
"DEBUG": ("🐛", "bold blue"),
|
|
8
|
+
"INFO": ("🔵", "bold green"),
|
|
9
|
+
"NOTICE": ("🔔", "bold cyan"),
|
|
10
|
+
"WARN": ("🟡", "bold yellow"),
|
|
11
|
+
"ERROR": ("🔴", "bold red"),
|
|
12
|
+
"CRITICAL": ("💥", "bold magenta"),
|
|
13
|
+
"ALERT": ("🚨", "bold color(208)"),
|
|
14
|
+
"FATAL": ("💀", "bold dark_red"),
|
|
15
|
+
"UNKNOWN": ("⚪", "dim white")
|
|
16
|
+
},
|
|
17
|
+
"highlights": {
|
|
18
|
+
"logscope.ip": "bold green",
|
|
19
|
+
"logscope.url": "underline blue",
|
|
20
|
+
"logscope.timestamp": "cyan",
|
|
21
|
+
"logscope.uuid": "bold magenta",
|
|
22
|
+
"logscope.email": "underline yellow",
|
|
23
|
+
"logscope.path": "dim blue",
|
|
24
|
+
"logscope.status_ok": "bold green",
|
|
25
|
+
"logscope.status_warn": "bold yellow",
|
|
26
|
+
"logscope.status_err": "bold red",
|
|
27
|
+
"logscope.method": "bold cyan"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"neon": {
|
|
31
|
+
"levels": {
|
|
32
|
+
"TRACE": ("·", "magenta"),
|
|
33
|
+
"DEBUG": ("⚡", "bold magenta"),
|
|
34
|
+
"INFO": ("✨", "bold pink1"),
|
|
35
|
+
"NOTICE": ("🌌", "bold purple"),
|
|
36
|
+
"WARN": ("🔥", "bold yellow1"),
|
|
37
|
+
"ERROR": ("🧨", "bold red1"),
|
|
38
|
+
"CRITICAL": ("⬢", "bold magenta"),
|
|
39
|
+
"ALERT": ("📣", "bold orange1"),
|
|
40
|
+
"FATAL": ("☣️", "bold dark_red"),
|
|
41
|
+
"UNKNOWN": ("?", "dim white")
|
|
42
|
+
},
|
|
43
|
+
"highlights": {
|
|
44
|
+
"logscope.ip": "bold pink1",
|
|
45
|
+
"logscope.url": "italic underline magenta",
|
|
46
|
+
"logscope.timestamp": "bold purple",
|
|
47
|
+
"logscope.uuid": "bold yellow",
|
|
48
|
+
"logscope.email": "bold pink1",
|
|
49
|
+
"logscope.path": "italic magenta",
|
|
50
|
+
"logscope.status_ok": "bold pink1",
|
|
51
|
+
"logscope.status_warn": "bold yellow",
|
|
52
|
+
"logscope.status_err": "bold red",
|
|
53
|
+
"logscope.method": "bold cyan"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"ocean": {
|
|
57
|
+
"levels": {
|
|
58
|
+
"TRACE": ("⚓", "dim cyan"),
|
|
59
|
+
"DEBUG": ("🌊", "blue"),
|
|
60
|
+
"INFO": ("💧", "bold blue"),
|
|
61
|
+
"NOTICE": ("🐳", "cyan"),
|
|
62
|
+
"WARN": ("🐚", "yellow"),
|
|
63
|
+
"ERROR": ("🌋", "red"),
|
|
64
|
+
"CRITICAL": ("⛈️ ", "bold blue"),
|
|
65
|
+
"ALERT": ("🚩", "bold orange1"),
|
|
66
|
+
"FATAL": ("🌪️ ", "bold red"),
|
|
67
|
+
"UNKNOWN": ("?", "dim cyan")
|
|
68
|
+
},
|
|
69
|
+
"highlights": {
|
|
70
|
+
"logscope.ip": "bold cyan",
|
|
71
|
+
"logscope.url": "underline deep_sky_blue1",
|
|
72
|
+
"logscope.timestamp": "blue",
|
|
73
|
+
"logscope.uuid": "bold deep_sky_blue3",
|
|
74
|
+
"logscope.email": "underline cyan",
|
|
75
|
+
"logscope.path": "dim cyan",
|
|
76
|
+
"logscope.status_ok": "bold green",
|
|
77
|
+
"logscope.status_warn": "bold yellow",
|
|
78
|
+
"logscope.status_err": "bold red",
|
|
79
|
+
"logscope.method": "bold cyan"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"forest": {
|
|
83
|
+
"levels": {
|
|
84
|
+
"TRACE": ("🍃", "dim green"),
|
|
85
|
+
"DEBUG": ("🌿", "green"),
|
|
86
|
+
"INFO": ("🌳", "bold green"),
|
|
87
|
+
"NOTICE": ("🍄", "magenta"),
|
|
88
|
+
"WARN": ("🍂", "yellow"),
|
|
89
|
+
"ERROR": ("🪵", "red"),
|
|
90
|
+
"CRITICAL": ("🌲", "bold white"),
|
|
91
|
+
"ALERT": ("🐺", "bold orange1"),
|
|
92
|
+
"FATAL": ("💀", "bold dark_red"),
|
|
93
|
+
"UNKNOWN": ("?", "dim green")
|
|
94
|
+
},
|
|
95
|
+
"highlights": {
|
|
96
|
+
"logscope.ip": "bold green3",
|
|
97
|
+
"logscope.url": "underline dark_green",
|
|
98
|
+
"logscope.timestamp": "yellow4",
|
|
99
|
+
"logscope.uuid": "bold orange4",
|
|
100
|
+
"logscope.email": "underline green",
|
|
101
|
+
"logscope.path": "dim green3",
|
|
102
|
+
"logscope.status_ok": "bold green",
|
|
103
|
+
"logscope.status_warn": "bold yellow",
|
|
104
|
+
"logscope.status_err": "bold red",
|
|
105
|
+
"logscope.method": "bold green1"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"minimal": {
|
|
109
|
+
"levels": {
|
|
110
|
+
"TRACE": (" ", "dim"),
|
|
111
|
+
"DEBUG": (" ", "dim"),
|
|
112
|
+
"INFO": (" ", "white"),
|
|
113
|
+
"NOTICE": ("!", "cyan"),
|
|
114
|
+
"WARN": ("?", "yellow"),
|
|
115
|
+
"ERROR": ("#", "red"),
|
|
116
|
+
"CRITICAL": ("*", "bold red"),
|
|
117
|
+
"ALERT": ("!", "bold yellow"),
|
|
118
|
+
"FATAL": ("X", "bold red"),
|
|
119
|
+
"UNKNOWN": ("?", "dim")
|
|
120
|
+
},
|
|
121
|
+
"highlights": {
|
|
122
|
+
"logscope.ip": "bold",
|
|
123
|
+
"logscope.url": "underline",
|
|
124
|
+
"logscope.timestamp": "dim",
|
|
125
|
+
"logscope.uuid": "dim",
|
|
126
|
+
"logscope.email": "underline",
|
|
127
|
+
"logscope.path": "italic",
|
|
128
|
+
"logscope.status_ok": "green",
|
|
129
|
+
"logscope.status_warn": "yellow",
|
|
130
|
+
"logscope.status_err": "red",
|
|
131
|
+
"logscope.method": "bold"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"spectra": {
|
|
135
|
+
"levels": {
|
|
136
|
+
"TRACE": ("◦", "dim cyan"),
|
|
137
|
+
"DEBUG": ("◇", "cyan"),
|
|
138
|
+
"INFO": ("◆", "bold bright_cyan"),
|
|
139
|
+
"NOTICE": ("▹", "bold turquoise2"),
|
|
140
|
+
"WARN": ("⚠", "bold bright_yellow"),
|
|
141
|
+
"ERROR": ("✖", "bold bright_red"),
|
|
142
|
+
"CRITICAL": ("⬢", "bold magenta"),
|
|
143
|
+
"ALERT": ("⚡", "bold color(214)"),
|
|
144
|
+
"FATAL": ("☠", "bold red3"),
|
|
145
|
+
"UNKNOWN": ("·", "dim white")
|
|
146
|
+
},
|
|
147
|
+
"highlights": {
|
|
148
|
+
"logscope.ip": "bold bright_cyan",
|
|
149
|
+
"logscope.url": "underline bright_magenta",
|
|
150
|
+
"logscope.timestamp": "bold cyan",
|
|
151
|
+
"logscope.uuid": "bold bright_white",
|
|
152
|
+
"logscope.email": "underline turquoise2",
|
|
153
|
+
"logscope.path": "italic cyan",
|
|
154
|
+
"logscope.status_ok": "bold spring_green1",
|
|
155
|
+
"logscope.status_warn": "bold gold1",
|
|
156
|
+
"logscope.status_err": "bold red1",
|
|
157
|
+
"logscope.method": "bold deep_sky_blue1"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|