loggerbuf 0.9.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.
@@ -0,0 +1,234 @@
1
+ import sys
2
+ import os
3
+ import gzip
4
+ import json
5
+ import re
6
+ from google.protobuf.json_format import MessageToDict
7
+
8
+ try:
9
+ from ... import schema_loader
10
+ main_data_pb2 = schema_loader.get_main_data_pb2()
11
+ Event = main_data_pb2.Event
12
+ CounterEvent = main_data_pb2.CounterEvent
13
+ except ImportError:
14
+ Event = None
15
+ CounterEvent = None
16
+
17
+ def decode_file(filepath, verify_key=None, skip_integrity=False, is_counter=False):
18
+ """
19
+ Reads a length-prefixed binary log file (raw or gzipped)
20
+ and yields deserialized Event objects.
21
+ """
22
+ record_class = CounterEvent if is_counter else Event
23
+ if not record_class:
24
+ raise ImportError(f"{record_class.__name__ if record_class else 'Event class'} is not available. Please run 'loggerbuf build' first.")
25
+
26
+ if not os.path.exists(filepath):
27
+ raise FileNotFoundError(f"File not found: {filepath}")
28
+
29
+ open_func = gzip.open if filepath.endswith('.gz') else open
30
+
31
+ import hmac
32
+ import hashlib
33
+ import click
34
+
35
+ current_hash = None
36
+ event_index = 0
37
+
38
+ with open_func(filepath, 'rb') as f:
39
+ while True:
40
+ header = f.read(4)
41
+ if not header:
42
+ break
43
+ if len(header) < 4:
44
+ click.secho(f"Warning: Truncated header found in {filepath} at position {f.tell() - len(header)}", fg='red', err=True)
45
+ break
46
+
47
+ size = int.from_bytes(header, byteorder='big')
48
+ payload = f.read(size)
49
+ if len(payload) < size:
50
+ click.secho(f"Warning: Truncated payload of size {size} in {filepath}", fg='red', err=True)
51
+ break
52
+
53
+ event_index += 1
54
+
55
+ try:
56
+ event = record_class.FromString(payload)
57
+
58
+ # --- HMAC Verification ---
59
+ if not skip_integrity and verify_key:
60
+ if hasattr(event, "hmac_signature") and event.hmac_signature:
61
+ stored_sig = event.hmac_signature
62
+
63
+ # Prepare payload for hash
64
+ event.ClearField("hmac_signature")
65
+
66
+ if getattr(event, "is_chain_start", False):
67
+ # Start of a new chain
68
+ prev = event.previous_file_hash if getattr(event, "previous_file_hash", b'') else b''
69
+ else:
70
+ prev = current_hash if current_hash else b''
71
+
72
+ clean_payload = event.SerializeToString()
73
+
74
+ expected_hash = hmac.new(verify_key.encode('utf-8'), clean_payload + prev, hashlib.sha256).digest()
75
+
76
+ if expected_hash != stored_sig:
77
+ click.secho(f"\n[!] CRITICAL ALERT: Integrity Compromised at Event #{event_index} (offset {f.tell() - size})", fg='red', err=True)
78
+ click.secho(f" Calculated signature does not match stored signature.", fg='red', err=True)
79
+
80
+ # Update running hash
81
+ current_hash = stored_sig
82
+ # Restore signature for output consistency if needed
83
+ event.hmac_signature = stored_sig
84
+
85
+ yield event
86
+ except Exception as e:
87
+ click.secho(f"Warning: Failed to deserialize event of size {size} at index {event_index}: {e}", fg='red', err=True)
88
+
89
+ def run_decode(input_file: str, output_file: str, format: str, stats: bool, head: int, tail: int, verify_key: str = None, skip_integrity: bool = False, is_counter: bool = False):
90
+ """Main decoding logic separated from click."""
91
+
92
+ try:
93
+ event_generator = decode_file(input_file, verify_key, skip_integrity, is_counter)
94
+ except Exception as e:
95
+ print(f"Error reading file: {e}", file=sys.stderr)
96
+ sys.exit(1)
97
+
98
+ if head:
99
+ import itertools
100
+ event_generator = itertools.islice(event_generator, head)
101
+ elif tail:
102
+ import collections
103
+ event_generator = collections.deque(event_generator, maxlen=tail)
104
+
105
+ total_events = 0
106
+
107
+ if stats:
108
+ event_types = {}
109
+ statuses = {}
110
+
111
+ try:
112
+ for ev in event_generator:
113
+ total_events += 1
114
+ if is_counter:
115
+ ev_type_name = str(ev.counter_type)
116
+ status_name = "N/A"
117
+ else:
118
+ ev_type_name = str(ev.event_type)
119
+ status_name = str(ev.status)
120
+
121
+ event_types[ev_type_name] = event_types.get(ev_type_name, 0) + (ev.count if is_counter else 1)
122
+ statuses[status_name] = statuses.get(status_name, 0) + 1
123
+ except Exception as e:
124
+ print(f"Error decoding events: {e}", file=sys.stderr)
125
+
126
+ print(f"=== LoggerBuf Telemetry Statistics ===")
127
+ print(f"File: {input_file}")
128
+ print(f"Total events decoded: {total_events}")
129
+
130
+ if is_counter:
131
+ print("\nCounters Breakdown:")
132
+ for k, v in event_types.items():
133
+ print(f" - {k}: {v} (total count)")
134
+ else:
135
+ print("\nEvent Types Breakdown:")
136
+ for k, v in event_types.items():
137
+ print(f" - {k}: {v}")
138
+ print("\nStatuses Breakdown:")
139
+ for k, v in statuses.items():
140
+ print(f" - {k}: {v}")
141
+ return
142
+
143
+ out_f = open(output_file, "w") if output_file else sys.stdout
144
+ try:
145
+ for ev in event_generator:
146
+ total_events += 1
147
+ ev_dict = MessageToDict(ev, always_print_fields_with_no_presence=True)
148
+ if format == "pretty":
149
+ out_f.write(json.dumps(ev_dict, indent=2, ensure_ascii=False) + "\n")
150
+ else:
151
+ out_f.write(json.dumps(ev_dict, ensure_ascii=False) + "\n")
152
+ except Exception as e:
153
+ print(f"Error decoding events: {e}", file=sys.stderr)
154
+ finally:
155
+ if output_file:
156
+ out_f.close()
157
+ print(f"Successfully decoded {total_events} events and exported to {output_file}")
158
+
159
+
160
+ import collections
161
+ import itertools
162
+
163
+ def decode_debug_file(filepath):
164
+ """
165
+ Reads a jsonl log file (raw or gzipped) and yields parsed JSON objects.
166
+ """
167
+ if not os.path.exists(filepath):
168
+ raise FileNotFoundError(f"File not found: {filepath}")
169
+
170
+ open_func = gzip.open if filepath.endswith('.gz') else open
171
+
172
+ with open_func(filepath, 'rt', encoding='utf-8') as f:
173
+ for line_num, line in enumerate(f, 1):
174
+ line = line.strip()
175
+ if not line:
176
+ continue
177
+ try:
178
+ yield json.loads(line)
179
+ except json.JSONDecodeError as e:
180
+ print(f"Warning: Failed to decode JSON at {filepath}:{line_num}: {e}", file=sys.stderr)
181
+
182
+ def run_decode_debug(input_file: str, grep_keyword: str = None, head: int = None, tail: int = None):
183
+ # Warning if it is the base file without rotation ext
184
+ basename = os.path.basename(input_file)
185
+ if basename.endswith(".log") and not re.search(r'\d{4}-\d{2}-\d{2}', basename):
186
+ print(f"\nWarning: You are decoding what appears to be the live active file '{basename}'. "
187
+ f"Some final bytes might be incomplete if written concurrently.\n", file=sys.stderr)
188
+
189
+ try:
190
+ log_generator = decode_debug_file(input_file)
191
+ except Exception as e:
192
+ print(f"Error reading file: {e}", file=sys.stderr)
193
+ sys.exit(1)
194
+
195
+ total_logs = 0
196
+ matched_logs = 0
197
+
198
+ grep_lower = grep_keyword.lower() if grep_keyword else None
199
+
200
+ if tail is not None and tail > 0:
201
+ log_generator = collections.deque(log_generator, maxlen=tail)
202
+
203
+ # Original visual format: '[{asctime}] >>{name}<< ({filename}::{caller_class}::{funcName}->{lineno}) - *{levelname}* - message::>{message}'
204
+ try:
205
+ for log_obj in log_generator:
206
+ if head is not None and matched_logs >= head:
207
+ break
208
+
209
+ total_logs += 1
210
+
211
+ timestamp = log_obj.get("timestamp", "Unknown")
212
+ logger_name = log_obj.get("logger", "Unknown")
213
+ filename = log_obj.get("file", "Unknown")
214
+ caller_class = log_obj.get("class", "None")
215
+ func_name = log_obj.get("function", "Unknown")
216
+ lineno = log_obj.get("line", 0)
217
+ level = log_obj.get("level", "UNKNOWN")
218
+ message = log_obj.get("message", "")
219
+
220
+ formatted_msg = f"[{timestamp}] >>{logger_name}<< ({filename}::{caller_class}::{func_name}->{lineno}) - *{level}* - message::>{message}"
221
+
222
+ if grep_lower:
223
+ if grep_lower not in formatted_msg.lower():
224
+ continue
225
+
226
+ matched_logs += 1
227
+ print(formatted_msg)
228
+
229
+ except Exception as e:
230
+ print(f"Error decoding debug logs: {e}", file=sys.stderr)
231
+ finally:
232
+ if grep_keyword:
233
+ print(f"--- Filtered {matched_logs} matches out of {total_logs} total logs in {input_file} ---", file=sys.stderr)
234
+
@@ -0,0 +1,193 @@
1
+ import os
2
+ import re
3
+ import click
4
+ from typing import List
5
+ from loggerbuf.cli.handlers.protos import get_protos_dir
6
+
7
+ def _get_registry_proto() -> str:
8
+ return os.path.join(get_protos_dir(), "registry.proto")
9
+
10
+ def _read_proto() -> str:
11
+ with open(_get_registry_proto(), "r") as f:
12
+ return f.read()
13
+
14
+ def _write_proto(content: str):
15
+ with open(_get_registry_proto(), "w") as f:
16
+ f.write(content)
17
+
18
+ def _get_max_value(enum_content: str) -> int:
19
+ """Finds the absolute maximum integer value used in the enum body."""
20
+ matches = re.findall(r'=\s*(\d+)\s*;', enum_content)
21
+ if not matches:
22
+ return 0
23
+ return max(int(m) for m in matches)
24
+
25
+ def _append_to_enum(proto_content: str, enum_name: str, block_name: str, items: List[str], reserve: int) -> str:
26
+ """
27
+ Appends a new block to an enum.
28
+ """
29
+ enum_pattern = re.compile(rf'(enum\s+{enum_name}\s+{{)(.*?)(^\}})', re.DOTALL | re.MULTILINE)
30
+ match = enum_pattern.search(proto_content)
31
+ if not match:
32
+ raise ValueError(f"Enum {enum_name} not found in {REGISTRY_PROTO}")
33
+
34
+ enum_start = match.group(1)
35
+ enum_body = match.group(2)
36
+ enum_end = match.group(3)
37
+
38
+ max_val = _get_max_value(enum_body)
39
+
40
+ start_val = max_val + 1
41
+ if start_val % 10 != 0:
42
+ start_val = ((start_val // 10) + 1) * 10
43
+
44
+ next_val = start_val
45
+ new_block = f"\n // Specific {enum_name} for {block_name}\n"
46
+
47
+ added_lines = []
48
+ for item in items:
49
+ added_lines.append(f" {item} = {next_val};")
50
+ next_val += 1
51
+
52
+ end_range = next_val + reserve - 1
53
+ if end_range < next_val:
54
+ end_range = next_val
55
+
56
+ new_block += f" // Range: {start_val}-{end_range}\n"
57
+ new_block += f" // Next value: {next_val}\n"
58
+ if added_lines:
59
+ new_block += "\n".join(added_lines) + "\n"
60
+
61
+ new_enum_body = enum_body.rstrip() + "\n" + new_block
62
+ new_content = proto_content[:match.start()] + enum_start + new_enum_body + enum_end + proto_content[match.end():]
63
+ return new_content
64
+
65
+ def _add_status_to_block(proto_content: str, enum_name: str, block_keyword: str, status_name: str) -> str:
66
+ enum_pattern = re.compile(rf'(enum\s+{enum_name}\s+{{)(.*?)(^\}})', re.DOTALL | re.MULTILINE)
67
+ match = enum_pattern.search(proto_content)
68
+ if not match:
69
+ raise ValueError(f"Enum {enum_name} not found.")
70
+
71
+ enum_start = match.group(1)
72
+ enum_body = match.group(2)
73
+ enum_end = match.group(3)
74
+
75
+ block_pattern = re.compile(rf'(//.*?{block_keyword}.*?\n\s*//\s*Range:\s*\d+-\d+\n\s*//\s*Next value:\s*)(\d+)(\n(?:.*?\n)*?)', re.IGNORECASE)
76
+
77
+ block_match = None
78
+ for m in block_pattern.finditer(enum_body):
79
+ block_match = m
80
+
81
+ if not block_match:
82
+ raise ValueError(f"Could not find reservation block for '{block_keyword}' in {enum_name}. Make sure it was created via CLI.")
83
+
84
+ next_val = int(block_match.group(2))
85
+
86
+ prefix = enum_body[:block_match.start(2)]
87
+ updated_next_val = str(next_val + 1)
88
+ suffix = enum_body[block_match.end(2):]
89
+
90
+ lines = suffix.split('\n')
91
+ insert_idx = 0
92
+ for i, line in enumerate(lines):
93
+ if line.strip() == '' or line.strip().startswith('//'):
94
+ insert_idx = i
95
+ break
96
+ else:
97
+ insert_idx = len(lines)
98
+
99
+ new_line = f" {status_name} = {next_val};"
100
+ lines.insert(insert_idx, new_line)
101
+
102
+ new_suffix = "\n".join(lines)
103
+ new_enum_body = prefix + updated_next_val + new_suffix
104
+
105
+ new_content = proto_content[:match.start()] + enum_start + new_enum_body + enum_end + proto_content[match.end():]
106
+ return new_content
107
+
108
+ def add_type(name: str, statuses: List[str], reserve: int):
109
+ proto_content = _read_proto()
110
+
111
+ type_name = name.upper()
112
+ if not type_name.startswith("EVENT_"):
113
+ type_name = f"EVENT_{type_name}"
114
+
115
+ proto_content = _append_to_enum(proto_content, "EventType", name, [type_name], reserve=reserve)
116
+
117
+ status_names = []
118
+ prefix = name.upper()
119
+ if prefix.startswith("EVENT_"):
120
+ prefix = prefix[6:]
121
+
122
+ for st in statuses:
123
+ st_name = st.upper()
124
+ if not st_name.startswith(f"{prefix}_STATUS_"):
125
+ st_name = f"{prefix}_STATUS_{st_name}"
126
+ status_names.append(st_name)
127
+
128
+ proto_content = _append_to_enum(proto_content, "EventStatus", name, status_names, reserve=reserve)
129
+ _write_proto(proto_content)
130
+
131
+ click.secho(f"Event type '{name}' and its statuses added successfully to registry.proto", fg="green")
132
+
133
+ def add_status(type_name: str, status_name: str):
134
+ proto_content = _read_proto()
135
+
136
+ prefix = type_name.upper()
137
+ if prefix.startswith("EVENT_"):
138
+ prefix = prefix[6:]
139
+
140
+ st_name = status_name.upper()
141
+ if not st_name.startswith(f"{prefix}_STATUS_"):
142
+ st_name = f"{prefix}_STATUS_{st_name}"
143
+
144
+ proto_content = _add_status_to_block(proto_content, "EventStatus", prefix, st_name)
145
+ _write_proto(proto_content)
146
+
147
+ click.secho(f"Status '{st_name}' added successfully to registry.proto", fg="green")
148
+
149
+ def list_events(type_name: str = None):
150
+ proto_content = _read_proto()
151
+ click.secho(f"--- EventType ---", fg="cyan")
152
+
153
+ enum_pattern = re.compile(r'enum\s+EventType\s+\{(.*?)\}', re.DOTALL)
154
+ match = enum_pattern.search(proto_content)
155
+ if match:
156
+ for line in match.group(1).split('\n'):
157
+ if '=' in line and not line.strip().startswith('//'):
158
+ click.echo(line.strip())
159
+
160
+ click.secho(f"\n--- EventStatus ---", fg="cyan")
161
+ enum_pattern = re.compile(r'enum\s+EventStatus\s+\{(.*?)\}', re.DOTALL)
162
+ match = enum_pattern.search(proto_content)
163
+ if match:
164
+ for line in match.group(1).split('\n'):
165
+ if '=' in line and not line.strip().startswith('//'):
166
+ if type_name:
167
+ prefix = type_name.upper()
168
+ if prefix.startswith("EVENT_"):
169
+ prefix = prefix[6:]
170
+ if prefix in line:
171
+ click.echo(line.strip())
172
+ else:
173
+ click.echo(line.strip())
174
+
175
+ def add_counter_type(name: str, counters: List[str], reserve: int):
176
+ proto_content = _read_proto()
177
+
178
+ prefix = name.upper()
179
+ if prefix.startswith("COUNTER_"):
180
+ prefix = prefix[8:]
181
+
182
+ counter_names = []
183
+ for ct in counters:
184
+ ct_name = ct.upper()
185
+ if not ct_name.startswith(f"{prefix}_"):
186
+ ct_name = f"{prefix}_{ct_name}"
187
+ counter_names.append(ct_name)
188
+
189
+ proto_content = _append_to_enum(proto_content, "CounterType", name, counter_names, reserve=reserve)
190
+ _write_proto(proto_content)
191
+
192
+ click.secho(f"Counter block '{name}' with {len(counter_names)} counters added successfully to registry.proto", fg="green")
193
+
@@ -0,0 +1,119 @@
1
+ import os
2
+ import glob
3
+ import re
4
+ import sys
5
+ from loggerbuf.cli.handlers.protos import build, get_protos_dir
6
+
7
+ def _find_file_for_message(message_name: str, file_name: str = None) -> str:
8
+ protos_dir = get_protos_dir()
9
+ if file_name:
10
+ path = os.path.join(protos_dir, file_name)
11
+ if not os.path.exists(path):
12
+ raise FileNotFoundError(f"File '{file_name}' not found in {protos_dir}.")
13
+ return path
14
+
15
+ files = glob.glob(os.path.join(protos_dir, "*.proto"))
16
+ matches = []
17
+
18
+ for f in files:
19
+ with open(f, "r") as file:
20
+ content = file.read()
21
+ if f"message {message_name} " in content or f"message {message_name}{{" in content or f"message {message_name}\n" in content:
22
+ matches.append(f)
23
+
24
+ if len(matches) == 0:
25
+ raise ValueError(f"Message '{message_name}' not found in any .proto file.")
26
+ elif len(matches) > 1:
27
+ basenames = [os.path.basename(m) for m in matches]
28
+ raise ValueError(f"Found message '{message_name}' in multiple files: {basenames}. Please use --file to specify.")
29
+
30
+ return matches[0]
31
+
32
+ def add_subfield(message_name: str, field_name: str, field_type: str, file_name: str = None):
33
+ """Injects a new field into an existing proto message."""
34
+ target_file = _find_file_for_message(message_name, file_name)
35
+
36
+ with open(target_file, "r") as f:
37
+ lines = f.readlines()
38
+
39
+ in_message = False
40
+ brace_count = 0
41
+ max_tag = 0
42
+ insert_idx = -1
43
+
44
+ for i, line in enumerate(lines):
45
+ if re.match(rf"^message\s+{message_name}\s*{{", line) or re.match(rf"^message\s+{message_name}\n", line):
46
+ in_message = True
47
+ brace_count += line.count('{') - line.count('}')
48
+ continue
49
+
50
+ if in_message:
51
+ brace_count += line.count('{') - line.count('}')
52
+ if brace_count == 0 and '}' in line:
53
+ insert_idx = i
54
+ break
55
+
56
+ # Only match fields at the main message level (brace_count == 1)
57
+ if brace_count == 1:
58
+ if re.search(rf"\s+{field_name}\s*=", line):
59
+ raise ValueError(f"Field '{field_name}' already exists in message '{message_name}'.")
60
+
61
+ match = re.search(r"=\s*(\d+)\s*;", line)
62
+ if match:
63
+ tag = int(match.group(1))
64
+ if tag > max_tag:
65
+ max_tag = tag
66
+
67
+ if insert_idx == -1:
68
+ raise ValueError(f"Could not find the end of message '{message_name}'. Check syntax in {target_file}.")
69
+
70
+ new_tag = max_tag + 1
71
+ new_field = f" {field_type} {field_name} = {new_tag};\n"
72
+
73
+ lines.insert(insert_idx, new_field)
74
+
75
+ with open(target_file, "w") as f:
76
+ f.writelines(lines)
77
+
78
+ print(f"Added field '{field_name}' (Tag {new_tag}) to '{message_name}' in {os.path.basename(target_file)}.")
79
+ build()
80
+
81
+ def deprecate_subfield(message_name: str, field_name: str, file_name: str = None):
82
+ """Marks a specific field in a sub-message as deprecated."""
83
+ target_file = _find_file_for_message(message_name, file_name)
84
+
85
+ with open(target_file, "r") as f:
86
+ lines = f.readlines()
87
+
88
+ in_message = False
89
+ brace_count = 0
90
+ found = False
91
+
92
+ for i, line in enumerate(lines):
93
+ if re.match(rf"^message\s+{message_name}\s*{{", line) or re.match(rf"^message\s+{message_name}\n", line):
94
+ in_message = True
95
+ brace_count += line.count('{') - line.count('}')
96
+ continue
97
+
98
+ if in_message:
99
+ brace_count += line.count('{') - line.count('}')
100
+ if brace_count == 0 and '}' in line:
101
+ break
102
+
103
+ if brace_count == 1:
104
+ if re.search(rf"\s+{field_name}\s*=", line):
105
+ found = True
106
+ if "[deprecated" in line.lower():
107
+ print(f"Field '{field_name}' is already deprecated.")
108
+ return
109
+ lines[i] = re.sub(r";", " [deprecated = true];", line, count=1)
110
+ break
111
+
112
+ if not found:
113
+ raise ValueError(f"Field '{field_name}' not found in message '{message_name}'.")
114
+
115
+ with open(target_file, "w") as f:
116
+ f.writelines(lines)
117
+
118
+ print(f"Marked field '{field_name}' in '{message_name}' as deprecated in {os.path.basename(target_file)}.")
119
+ build()