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 ADDED
@@ -0,0 +1,33 @@
1
+ from .debugger import DebuggerLog, LoggerSettings, LogDestination, LogLevel
2
+ from .telemetry import TelemetryLog, EventSettings
3
+ from .cli.handlers.decode import decode_file
4
+
5
+ def create_debugger(name="MAIN", stream=LogDestination.CONSOLE_AND_FILE_HISTORY, logs_base_dir="."):
6
+ """
7
+ Helper function to get or create an operational Debugger instance.
8
+
9
+ Parameters
10
+ ----------
11
+ name : str
12
+ Name of the debugger channel.
13
+ stream : LogDestination
14
+ Where to send logs (ONLY_CONSOLE, ONLY_FILE, CONSOLE_AND_FILE_HISTORY).
15
+ logs_base_dir : str
16
+ Base directory to store operational logs.
17
+ """
18
+ settings = LoggerSettings(name=name, logs_base_dir=logs_base_dir, stream=stream)
19
+ return DebuggerLog(settings)
20
+
21
+ def create_telemetry(name="MAIN", logs_base_dir="."):
22
+ """
23
+ Helper function to get or create a structured Telemetry instance.
24
+
25
+ Parameters
26
+ ----------
27
+ name : str
28
+ Name of the telemetry channel.
29
+ logs_base_dir : str
30
+ Base directory to store binary telemetry files.
31
+ """
32
+ settings = EventSettings(name=name, logs_base_dir=logs_base_dir)
33
+ return TelemetryLog(settings)
File without changes
@@ -0,0 +1,453 @@
1
+ import click
2
+ import sys
3
+ import threading
4
+ from loggerbuf.cli.handlers import protos
5
+ from loggerbuf.cli.handlers import decode as decode_handler
6
+ from loggerbuf.cli.handlers import stress
7
+ from loggerbuf.cli.handlers import fields
8
+ from loggerbuf.cli.handlers import events
9
+ from ..config import ConfigManager
10
+
11
+ @click.group()
12
+ def cli():
13
+ """LoggerBuf CLI - Asynchronous Telemetry Framework."""
14
+ pass
15
+
16
+ @cli.group(name="protos")
17
+ def protos_group():
18
+ """Manage Protocol Buffers schemas."""
19
+ pass
20
+
21
+ @protos_group.command(name="init")
22
+ def protos_init_cmd():
23
+ """Initializes the Protos structure and the Registry in the local project."""
24
+ try:
25
+ from ..config import ConfigManager
26
+ protos_dir = ConfigManager().get("PROTOS_DIR", "loggerbuf_schemas")
27
+ import os
28
+ if os.path.exists(protos_dir) and os.listdir(protos_dir):
29
+ click.secho(f"[{protos_dir}] is already initialized.", fg="green")
30
+ else:
31
+ protos.init()
32
+ click.secho(f"[{protos_dir}] created and initialized successfully.", fg="green")
33
+ except Exception as e:
34
+ click.secho(f"Error: {e}", fg="red")
35
+ sys.exit(1)
36
+
37
+ @cli.command()
38
+ @click.pass_context
39
+ def init(ctx):
40
+ """Bootstraps a new LoggerBuf project in a single command."""
41
+ click.secho("Starting LoggerBuf project initialization...", fg="cyan")
42
+ try:
43
+ ctx.invoke(config_init)
44
+ ctx.invoke(protos_init_cmd)
45
+ ctx.invoke(build)
46
+ click.secho("Project initialized successfully! You are ready to log.", fg="green", bold=True)
47
+ except Exception as e:
48
+ click.secho(f"Initialization failed: {e}", fg="red")
49
+ sys.exit(1)
50
+
51
+ @cli.command(name="factory-reset")
52
+ @click.option('--hard', is_flag=True, help="Permanently delete setup files without creating a backup.")
53
+ def factory_reset(hard):
54
+ """Resets the LoggerBuf configuration and schemas to ground zero."""
55
+ import os
56
+ import shutil
57
+ import datetime
58
+ from ..config import CONFIG_FILE, ConfigManager
59
+
60
+ click.secho("WARNING: This command will reset your entire LoggerBuf configuration and schemas.", fg="red", bold=True)
61
+ click.secho("Log data files will NOT be affected.\n", fg="yellow")
62
+
63
+ config_mgr = ConfigManager()
64
+ challenge_key = "LOGGING_BASE_DIR"
65
+ challenge_value = str(config_mgr.get(challenge_key))
66
+
67
+ click.secho(f"[Security] To confirm the reset, please type the current value of '{challenge_key}':", fg="cyan")
68
+ response = click.prompt("Value", type=str)
69
+
70
+ if response != challenge_value:
71
+ click.secho("Challenge failed. Factory reset aborted.", fg="red")
72
+ sys.exit(1)
73
+
74
+ protos_dir = config_mgr.get("PROTOS_DIR", "loggerbuf_schemas")
75
+
76
+ if not hard:
77
+ backup_dir = f"loggerbuf_backup_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
78
+ os.makedirs(backup_dir, exist_ok=True)
79
+
80
+ if os.path.exists(CONFIG_FILE):
81
+ shutil.copy(CONFIG_FILE, backup_dir)
82
+
83
+ if os.path.exists(protos_dir):
84
+ backup_protos = os.path.join(backup_dir, "schemas")
85
+ os.makedirs(backup_protos, exist_ok=True)
86
+ for f in os.listdir(protos_dir):
87
+ if f.endswith(".proto") or f.endswith(".json"):
88
+ shutil.copy(os.path.join(protos_dir, f), backup_protos)
89
+
90
+ click.secho(f"Backup created at: {backup_dir}/", fg="green")
91
+
92
+ # Delete
93
+ if os.path.exists(CONFIG_FILE):
94
+ os.remove(CONFIG_FILE)
95
+ if os.path.exists(protos_dir):
96
+ shutil.rmtree(protos_dir)
97
+
98
+ click.secho("Factory reset complete. The project has been wiped clean.", fg="green", bold=True)
99
+
100
+ @cli.command()
101
+ def build():
102
+ """Rebuilds main_data.proto and compiles all classes."""
103
+ try:
104
+ protos.build()
105
+ click.secho("Build completed successfully.", fg="green")
106
+ except Exception as e:
107
+ click.secho(f"Error: {e}", fg="red")
108
+ sys.exit(1)
109
+
110
+ # Alias for 'build' inside the 'protos' group
111
+ @protos_group.command(name="build")
112
+ @click.pass_context
113
+ def protos_build(ctx):
114
+ """Rebuilds main_data.proto and compiles all classes (alias for 'loggerbuf build')."""
115
+ ctx.invoke(build)
116
+
117
+ @cli.command()
118
+ @click.argument('name')
119
+ @click.option('--field', multiple=True, help="Fields in name:type format (e.g. --field age:int32)")
120
+ def create_event(name, field):
121
+ """Creates a new .proto file for an event.
122
+
123
+ If --field is not provided, an interactive wizard will start.
124
+ """
125
+ fields_list = []
126
+ if field:
127
+ for f in field:
128
+ parts = f.split(':')
129
+ if len(parts) != 2:
130
+ click.secho(f"Invalid field format: {f}. Use name:type", fg="red")
131
+ sys.exit(1)
132
+ fields_list.append((parts[0], parts[1]))
133
+ else:
134
+ click.secho(f"Wizard to create event '{name}'", fg="cyan")
135
+ while click.confirm("Add a field?"):
136
+ f_name = click.prompt("Field name")
137
+ f_type = click.prompt("Data type (e.g. string, int32, Enum_Name)")
138
+ fields_list.append((f_name, f_type))
139
+
140
+ try:
141
+ protos.create_event(name, fields_list)
142
+ click.secho(f"Event {name} created. Now register it with 'loggerbuf register-event'.", fg="green")
143
+ except Exception as e:
144
+ click.secho(f"Error: {e}", fg="red")
145
+ sys.exit(1)
146
+
147
+ @cli.command()
148
+ @click.argument('field_name')
149
+ @click.argument('message_name')
150
+ @click.option('--file', help="Force the filename in case of collisions.")
151
+ def register_event(field_name, message_name, file):
152
+ """Registers an event in main_data.proto and generates the build.
153
+
154
+ FIELD_NAME: Name of the variable (e.g. user_login)
155
+ MESSAGE_NAME: Name of the proto message (e.g. UserLogin)
156
+ """
157
+ try:
158
+ protos.register_event(field_name, message_name, file)
159
+ click.secho(f"Event '{field_name}' registered and compiled successfully.", fg="green")
160
+ click.secho(
161
+ f"\n[TIP] Consider adding specific sub-classifications (EventTypes) and statuses (EventStatus) "
162
+ f"for your new event using 'loggerbuf event add-type' to enable deeper telemetry analytics.",
163
+ fg="cyan"
164
+ )
165
+ except Exception as e:
166
+ click.secho(f"Error: {e}", fg="red")
167
+ sys.exit(1)
168
+
169
+ @cli.command()
170
+ @click.argument('field_name')
171
+ def deprecate_event(field_name):
172
+ """Marks an event as deprecated for safe backward compatibility."""
173
+ try:
174
+ protos.deprecate_event(field_name)
175
+ click.secho(f"Event '{field_name}' marked as deprecated successfully.", fg="yellow")
176
+ except Exception as e:
177
+ click.secho(f"Error: {e}", fg="red")
178
+ sys.exit(1)
179
+
180
+ @cli.command()
181
+ @click.argument('message_name')
182
+ @click.argument('field_name')
183
+ @click.argument('field_type')
184
+ @click.option('-f', '--file', help="Specify the .proto file name if the message exists in multiple files.")
185
+ def add_subfield(message_name, field_name, field_type, file):
186
+ """Adds a new field to a specific proto message."""
187
+ try:
188
+ fields.add_subfield(message_name, field_name, field_type, file_name=file)
189
+ except Exception as e:
190
+ click.secho(f"Error: {e}", fg="red")
191
+ sys.exit(1)
192
+
193
+ @cli.command()
194
+ @click.argument('message_name')
195
+ @click.argument('field_name')
196
+ @click.option('-f', '--file', help="Specify the .proto file name if the message exists in multiple files.")
197
+ def deprecate_subfield(message_name, field_name, file):
198
+ """Marks a specific field in a proto message as deprecated."""
199
+ try:
200
+ fields.deprecate_subfield(message_name, field_name, file_name=file)
201
+ except Exception as e:
202
+ click.secho(f"Error: {e}", fg="red")
203
+ sys.exit(1)
204
+
205
+ @cli.command()
206
+ @click.argument('input_file')
207
+ @click.option('--output', '-o', default=None, help="Output file (prints to stdout if not provided)")
208
+ @click.option('--format', type=click.Choice(['jsonl', 'pretty']), default='pretty', help="Output format")
209
+ @click.option('--stats', is_flag=True, help="Show summary statistics instead of full logs")
210
+ @click.option('--head', type=int, help="Output the first N events")
211
+ @click.option('--tail', type=int, help="Output the last N events")
212
+ @click.option('--verify', help="HMAC secret key to verify the integrity of the log file")
213
+ @click.option('--skip-integrity', is_flag=True, help="Skip HMAC integrity checks even if records contain signatures")
214
+ @click.option('--counters', is_flag=True, help="Decode file as CounterEvents instead of standard Events")
215
+ def decode(input_file, output, format, stats, head, tail, verify, skip_integrity, counters):
216
+ """Decodes a LoggerBuf binary telemetry log into JSON/Stats."""
217
+ if head and tail:
218
+ click.secho("Error: Cannot use both --head and --tail simultaneously.", fg='red', err=True)
219
+ sys.exit(1)
220
+
221
+ decode_handler.run_decode(
222
+ input_file=input_file,
223
+ output_file=output,
224
+ format=format,
225
+ stats=stats,
226
+ head=head,
227
+ tail=tail,
228
+ verify_key=verify,
229
+ skip_integrity=skip_integrity,
230
+ is_counter=counters
231
+ )
232
+
233
+ @cli.command()
234
+ @click.argument('input_file')
235
+ @click.option('--grep', help="Filter logs by keyword (case-insensitive).")
236
+ @click.option('--head', type=int, help="Show only the first N logs.")
237
+ @click.option('--tail', type=int, help="Show only the last N logs.")
238
+ def decode_debug(input_file, grep, head, tail):
239
+ """Explores historical JSON debug logs visually in the terminal."""
240
+ decode_handler.run_decode_debug(input_file, grep, head, tail)
241
+
242
+ @cli.group()
243
+ def event():
244
+ """Manage Event types and statuses."""
245
+ pass
246
+
247
+ @cli.group()
248
+ def config():
249
+ """Manage LoggerBuf global configurations (loggerbuf.json)."""
250
+ pass
251
+
252
+ @config.command(name="init")
253
+ def config_init():
254
+ """Generates the default loggerbuf.json configuration file."""
255
+ from ..config import CONFIG_FILE, ConfigManager
256
+ try:
257
+ config_mgr = ConfigManager()
258
+ click.secho(f"Configuration file '{CONFIG_FILE}' is ready.", fg="green")
259
+ except Exception as e:
260
+ click.secho(f"Error: {e}", fg="red")
261
+ sys.exit(1)
262
+
263
+ @cli.group()
264
+ def counter():
265
+ """Manage Counter types."""
266
+ pass
267
+
268
+ @counter.command(name="add-type")
269
+ @click.argument('name')
270
+ @click.option('--counters', default="", help="Comma separated list of counters (e.g. CLICK,VIEW)")
271
+ @click.option('--reserve', default=10, type=int, help="Number of extra indices to reserve for future counters.")
272
+ def add_counter_type(name, counters, reserve):
273
+ """Adds a new counter block and optional counters."""
274
+ counter_list = [c.strip() for c in counters.split(",")] if counters else []
275
+ try:
276
+ events.add_counter_type(name, counter_list, reserve)
277
+ click.secho("\n[WARNING] You modified the .proto file.", fg="yellow")
278
+ click.secho("Run 'loggerbuf build' to compile it, and restart your application for changes to take effect.", fg="yellow")
279
+ except Exception as e:
280
+ click.secho(f"Error: {e}", fg="red")
281
+ sys.exit(1)
282
+
283
+ @event.command()
284
+ @click.argument('name')
285
+ @click.option('--statuses', default="", help="Comma separated list of statuses (e.g. ST1,ST2)")
286
+ @click.option('--reserve', default=10, type=int, help="Number of extra indices to reserve for future statuses.")
287
+ def add_type(name, statuses, reserve):
288
+ """Adds a new event type and optional statuses."""
289
+ status_list = [s.strip() for s in statuses.split(",")] if statuses else []
290
+ try:
291
+ events.add_type(name, status_list, reserve)
292
+ click.secho("\n[WARNING] You modified the .proto file.", fg="yellow")
293
+ click.secho("Run 'loggerbuf build' to compile it, and restart your application for changes to take effect.", fg="yellow")
294
+ except Exception as e:
295
+ click.secho(f"Error: {e}", fg="red")
296
+ sys.exit(1)
297
+
298
+ @event.command()
299
+ @click.argument('type_name')
300
+ @click.argument('status_name')
301
+ def add_status(type_name, status_name):
302
+ """Adds a new status to an existing event type."""
303
+ try:
304
+ events.add_status(type_name, status_name)
305
+ click.secho("\n[WARNING] You modified the .proto file.", fg="yellow")
306
+ click.secho("Run 'loggerbuf build' to compile it, and restart your application for changes to take effect.", fg="yellow")
307
+ except Exception as e:
308
+ click.secho(f"Error: {e}", fg="red")
309
+ sys.exit(1)
310
+
311
+ @event.command(name="list")
312
+ @click.argument('type_name', required=False)
313
+ def list_cmd(type_name):
314
+ """Lists registered event types and statuses."""
315
+ try:
316
+ events.list_events(type_name)
317
+ except Exception as e:
318
+ click.secho(f"Error: {e}", fg="red")
319
+ sys.exit(1)
320
+
321
+ @config.command()
322
+ @click.argument('key')
323
+ @click.argument('value')
324
+ def set(key, value):
325
+ """Sets a global configuration key to a specific value."""
326
+ try:
327
+ # Convert value to its proper type
328
+ if value.lower() in ("true", "false"):
329
+ value = value.lower() == "true"
330
+ elif value.isdigit():
331
+ value = int(value)
332
+ elif value.startswith("[") and value.endswith("]"):
333
+ import json
334
+ value = json.loads(value.replace("'", '"'))
335
+
336
+ config_mgr = ConfigManager()
337
+ config_mgr.set(key, value)
338
+ click.secho(f"Updated {key} = {value}", fg="green")
339
+
340
+ if key == "LOG_LEVEL":
341
+ click.secho("The ConfigWatcher will hot-reload this setting in ~5 seconds without restarting the app.", fg="green")
342
+ else:
343
+ click.secho(f"Warning: Modifying '{key}' requires manually restarting your application to take effect.", fg="yellow")
344
+
345
+ except Exception as e:
346
+ click.secho(f"Error: {e}", fg="red")
347
+ sys.exit(1)
348
+
349
+ @config.command()
350
+ @click.argument('key')
351
+ def reset(key):
352
+ """Resets a configuration key to its global default."""
353
+ try:
354
+ config_mgr = ConfigManager()
355
+ # Verify it exists in our live config before trying to remove
356
+ if key not in config_mgr._config:
357
+ click.secho(f"Key '{key}' is not explicitly set, already using default.", fg="yellow")
358
+ sys.exit(0)
359
+
360
+ config_mgr.remove(key)
361
+ new_val = config_mgr.get(key)
362
+ click.secho(f"Reset {key} to default: {new_val}", fg="green")
363
+ except Exception as e:
364
+ click.secho(f"Error: {e}", fg="red")
365
+ sys.exit(1)
366
+
367
+ @config.command()
368
+ @click.argument('key')
369
+ def get(key):
370
+ """Gets a configuration value, or group (all, logging, telemetry, metrics)."""
371
+ from ..config import DEFAULT_CONFIG, CONFIG_CHOICES
372
+ config_manager = ConfigManager()
373
+
374
+ group = key.lower()
375
+ if group in ('all', 'logging', 'telemetry', 'metrics'):
376
+ def print_group(prefix, title, color):
377
+ click.secho(f"\n--- {title} ---", fg=color, bold=True)
378
+ for k in DEFAULT_CONFIG.keys():
379
+ if k.startswith(prefix) or (prefix == 'LOG' and k == 'LOG_LEVEL'):
380
+ val = config_manager.get(k)
381
+
382
+ # If this key has limited choices, display them as hints
383
+ if k in CONFIG_CHOICES:
384
+ hints = f" [{' | '.join(map(str, CONFIG_CHOICES[k]))}]"
385
+ click.secho(f"{k}", fg="cyan", nl=False)
386
+ click.secho(hints, fg="yellow", nl=False)
387
+ click.secho(": ", fg="cyan", nl=False)
388
+ else:
389
+ click.secho(f"{k}: ", fg="cyan", nl=False)
390
+ click.echo(f"{val}")
391
+
392
+ if group in ('all', 'logging'):
393
+ print_group('LOG', 'LOGGING (DEBUGGER) SETTINGS', 'green')
394
+ if group in ('all', 'telemetry'):
395
+ print_group('EVENT_', 'TELEMETRY (EVENTS) SETTINGS', 'blue')
396
+ if group in ('all', 'metrics'):
397
+ print_group('METRIC_', 'METRICS (COUNTERS) SETTINGS', 'magenta')
398
+ if group == 'all':
399
+ print_group('STRESS_', 'STRESS TEST SETTINGS', 'yellow')
400
+ print_group('HMAC_', 'SECURITY SETTINGS', 'red')
401
+
402
+ click.echo("")
403
+ return
404
+
405
+ value = config_manager.get(key)
406
+ if value is not None:
407
+ click.echo("")
408
+ if key in CONFIG_CHOICES:
409
+ hints = f" [{' | '.join(map(str, CONFIG_CHOICES[key]))}]"
410
+ click.secho(f"Configuration for ", fg="white", nl=False)
411
+ click.secho(f"{key}", fg="cyan", bold=True, nl=False)
412
+ click.secho(hints, fg="yellow", nl=False)
413
+ click.secho(":", fg="white")
414
+ click.secho(f" Current value: ", fg="white", nl=False)
415
+ click.secho(f"{value}", fg="green", bold=True)
416
+ else:
417
+ click.secho(f"Configuration for ", fg="white", nl=False)
418
+ click.secho(f"{key}", fg="cyan", bold=True, nl=False)
419
+ click.secho(":", fg="white")
420
+ click.secho(f" Current value: ", fg="white", nl=False)
421
+ click.secho(f"{value}", fg="green", bold=True)
422
+ click.echo("")
423
+ else:
424
+ click.secho(f"Key '{key}' not found in config or defaults.", fg="red")
425
+
426
+ @cli.command()
427
+ @click.option('--threads', type=int, help="Number of concurrent threads. Overrides config.")
428
+ @click.option('--writes', type=int, help="Total number of writes across all threads. Overrides config.")
429
+ @click.option('--duration', type=int, help="Target duration in seconds to pace the traffic. 0 for burst. Overrides config.")
430
+ @click.option('--queue-size', type=int, help="Max queue capacity. Overrides config.")
431
+ @click.option('--strategy', type=click.Choice(['lossless', 'lossy'], case_sensitive=False), help="Queue strategy. Overrides config.")
432
+ @click.option('--keep-logs', is_flag=True, default=False, help="Do not delete the stress test log directory at the end.")
433
+ def stress_test(threads, writes, duration, queue_size, strategy, keep_logs):
434
+ """Runs a performance stress test on LoggerBuf with resource monitoring."""
435
+ config_manager = ConfigManager()
436
+
437
+ threads = threads if threads is not None else config_manager.get('STRESS_TEST_THREADS', 10)
438
+ writes = writes if writes is not None else config_manager.get('STRESS_TEST_WRITES', 10000)
439
+ duration = duration if duration is not None else config_manager.get('STRESS_TEST_DURATION', 0)
440
+ queue_size = queue_size if queue_size is not None else config_manager.get('LOGGING_QUEUE_MAX_SIZE')
441
+ strategy = strategy if strategy is not None else config_manager.get('LOGGING_QUEUE_STRATEGY')
442
+
443
+ stress.run_stress_test(
444
+ num_threads=threads,
445
+ total_writes=writes,
446
+ duration=duration,
447
+ queue_size=queue_size,
448
+ strategy=strategy.upper(),
449
+ keep_logs=keep_logs
450
+ )
451
+
452
+ if __name__ == '__main__':
453
+ cli()
File without changes