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.
- loggerbuf/__init__.py +33 -0
- loggerbuf/cli/__init__.py +0 -0
- loggerbuf/cli/console.py +453 -0
- loggerbuf/cli/handlers/__init__.py +0 -0
- loggerbuf/cli/handlers/decode.py +234 -0
- loggerbuf/cli/handlers/events.py +193 -0
- loggerbuf/cli/handlers/fields.py +119 -0
- loggerbuf/cli/handlers/protos.py +283 -0
- loggerbuf/cli/handlers/stress.py +272 -0
- loggerbuf/cli/utils/__init__.py +0 -0
- loggerbuf/cli/utils/registry.py +105 -0
- loggerbuf/cli/utils/schema_validator.py +138 -0
- loggerbuf/config.py +253 -0
- loggerbuf/data_logs/__init__.py +5 -0
- loggerbuf/data_logs/main_data_pb2.py +39 -0
- loggerbuf/data_logs/registry_pb2.py +40 -0
- loggerbuf/debugger.py +637 -0
- loggerbuf/queue_metrics.py +171 -0
- loggerbuf/schema_loader.py +70 -0
- loggerbuf/telemetry.py +391 -0
- loggerbuf-0.9.0.dist-info/METADATA +340 -0
- loggerbuf-0.9.0.dist-info/RECORD +25 -0
- loggerbuf-0.9.0.dist-info/WHEEL +5 -0
- loggerbuf-0.9.0.dist-info/entry_points.txt +2 -0
- loggerbuf-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|