hyperprobe-agent 1.12.19__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.
hyperprobe/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ __all__ = ["HyperProbe"]
2
+
3
+ def __getattr__(name):
4
+ if name == "HyperProbe":
5
+ from hyperprobe.agent import HyperProbeAgent
6
+ return HyperProbeAgent
7
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
hyperprobe/agent.py ADDED
@@ -0,0 +1,476 @@
1
+ import sys
2
+ import os
3
+ import uuid
4
+ import time
5
+ import threading
6
+ import queue
7
+ import re
8
+ from urllib.parse import urlparse
9
+
10
+ from hyperprobe.core.quota import QuotaManager
11
+ from hyperprobe.core.safety import SafetyMonitor, AgentHealth
12
+ from hyperprobe.core.broker import BrokerClient
13
+ from hyperprobe.core.logger import get_logger
14
+
15
+ logger = get_logger("hyperprobe:agent")
16
+ from hyperprobe.protos.agent_pb2 import PROBE_TYPE_UNSPECIFIED,PROBE_TYPE_SNAPSHOT,PROBE_TYPE_LOG,PROBE_TYPE_COUNTER,PROBE_TYPE_METRIC,PROBE_TYPE_DURATION
17
+
18
+ UUID_REGEX = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.IGNORECASE)
19
+
20
+ SUPPORTED_PROBE_TYPES = {PROBE_TYPE_UNSPECIFIED,PROBE_TYPE_SNAPSHOT,PROBE_TYPE_LOG,PROBE_TYPE_COUNTER,PROBE_TYPE_METRIC,PROBE_TYPE_DURATION}
21
+
22
+
23
+ class _TelemetryItem:
24
+ def __init__(self, event, bandwidth_reservation=None):
25
+ self.event = event
26
+ self.bandwidth_reservation = bandwidth_reservation
27
+
28
+
29
+ def is_valid_broker_url(url):
30
+ if not url or not str(url).strip():
31
+ return False
32
+ url = str(url).strip()
33
+
34
+ if url.startswith("http://") or url.startswith("https://"):
35
+ try:
36
+ parsed = urlparse(url)
37
+ return not url.endswith("/") and not parsed.query and bool(parsed.hostname)
38
+ except Exception:
39
+ return False
40
+
41
+ # Raw gRPC targets should have no schema, no path, and no query parameters
42
+ return "://" not in url and "/" not in url and "?" not in url
43
+
44
+ class HyperProbeAgent:
45
+ _instance = None
46
+ _lock = threading.Lock()
47
+
48
+ def __init__(self, options):
49
+ self.options = options
50
+ self.agent_id = str(uuid.uuid4())
51
+ self.is_shutdown = False
52
+ self.stop_event = threading.Event()
53
+
54
+ # Options & Envs Parsing
55
+ self.sync_interval_sec = (options.get("sync_interval_ms") or int(os.getenv("HYPERPROBE_SYNC_INTERVAL_MS", 60000))) / 1000.0
56
+ self.flush_interval_sec = (options.get("flush_interval_ms") or int(os.getenv("HYPERPROBE_FLUSH_INTERVAL_MS", 1000))) / 1000.0
57
+ self.max_queue_size = options.get("max_queue_size") or int(os.getenv("HYPERPROBE_MAX_QUEUE_SIZE", 100))
58
+ self._cooldown_sec = options.get("hyperprobe_cooldown_sec") or int(os.getenv("HYPERPROBE_COOLDOWN_SEC", 10))
59
+
60
+ hits_per_sec = options.get("hits_per_sec") or int(os.getenv("HYPERPROBE_HITS_PER_SEC", 10))
61
+ bandwidth_kb = options.get("bandwidth_kb_per_sec") or int(os.getenv("HYPERPROBE_BANDWIDTH_KB_PER_SEC", 1024))
62
+ rpc_timeout_sec = options.get("hyperprobe_rpc_timeout_sec") or float(os.getenv("HYPERPROBE_RPC_TIMEOUT_SEC", 10.0))
63
+ max_lag_ms = options.get("hyperprobe_max_lag_ms") or float(os.getenv("HYPERPROBE_MAX_LAG_MS", 50.0))
64
+ pause_budget_ms = options.get("hyperprobe_pause_budget_ms") or float(os.getenv("HYPERPROBE_PAUSE_BUDGET_MS", 15.0))
65
+
66
+ self.global_config = {
67
+ "redact_keys": options.get("redact_keys") or os.getenv("HYPERPROBE_REDACT_KEYS", "password,secret,token,authorization,cookie,key,signature").split(","),
68
+ "redact_values": options.get("redact_values") or os.getenv("HYPERPROBE_REDACT_VALUES", "").split(","),
69
+ "max_object_depth": options.get("max_object_depth") or int(os.getenv("HYPERPROBE_MAX_OBJECT_DEPTH", 3)),
70
+ "max_array_length": options.get("max_array_length") or int(os.getenv("HYPERPROBE_MAX_ARRAY_LENGTH", 3)),
71
+ "stack_frame_depth": options.get("stack_frame_depth") or int(os.getenv("HYPERPROBE_STACK_FRAME_DEPTH", 3)),
72
+ "max_object_properties": options.get("max_object_properties") or int(os.getenv("HYPERPROBE_MAX_OBJECT_PROPERTIES", 50)),
73
+ "max_string_length": options.get("max_string_length") or int(os.getenv("HYPERPROBE_MAX_STRING_LENGTH", 1024))
74
+ }
75
+
76
+ # Strip empty redaction strings
77
+ self.global_config["redact_keys"] = [k.strip() for k in self.global_config["redact_keys"] if k.strip()]
78
+ self.global_config["redact_values"] = [v.strip() for v in self.global_config["redact_values"] if v.strip()]
79
+
80
+ # Initialize core telemetry components
81
+ self.quota_manager = QuotaManager(hits_per_sec, bandwidth_kb * 1024)
82
+ self.safety_monitor = SafetyMonitor(
83
+ self._handle_health_change,
84
+ max_lag_ms=max_lag_ms,
85
+ pause_budget_ms=pause_budget_ms,
86
+ )
87
+
88
+ self.broker_client = BrokerClient(
89
+ broker_url=options["broker_url"],
90
+ service_id=options["service_id"],
91
+ environment=options["environment"],
92
+ commit_sha=options.get("commit_sha") or os.getenv("GIT_COMMIT", "unknown"),
93
+ agent_id=self.agent_id,
94
+ rpc_timeout_sec=rpc_timeout_sec,
95
+ )
96
+
97
+ # Thread-safe queue for telemetry events
98
+ self.telemetry_queue = queue.Queue(maxsize=self.max_queue_size)
99
+ self.active_probes = {} # probe_id -> Probe
100
+ self.local_hits = {} # probe_id -> count
101
+ self.pending_hits = {} # probe_id -> queued capture reservations
102
+ self.cooldown_timer = None
103
+ self._cooldown_generation = 0
104
+ self._agent_lock = threading.RLock() # Synchronizes state access with re-entrancy safety
105
+ self._inflight_batch = None
106
+
107
+ # Determine and initialize dynamic instrumentation engine
108
+ self.engine = self._init_instrumentation_engine()
109
+ self.engine.set_global_config(self.global_config)
110
+
111
+ # Background worker threads
112
+ self.sync_thread = None
113
+ self.flush_thread = None
114
+ self.stats_thread = None
115
+
116
+ @classmethod
117
+ def start(cls, options):
118
+ """Starts the global HyperProbe Agent singleton."""
119
+ with cls._lock:
120
+ if cls._instance is not None:
121
+ logger.forceInfo("[HyperProbe] Agent is already running.")
122
+ return cls._instance
123
+
124
+ # Check kill switch
125
+ if os.getenv("HYPERPROBE_DISABLED", "").strip().upper() == "YES":
126
+ logger.forceInfo("[HyperProbe] Explicitly disabled via HYPERPROBE_DISABLED.")
127
+ return None
128
+
129
+ commit_sha = options.get("commit_sha") or os.getenv("HYPERPROBE_COMMIT_SHA") or os.getenv("GIT_COMMIT")
130
+ if not commit_sha or not str(commit_sha).strip() or str(commit_sha).strip().lower() == "unknown":
131
+ logger.forceError('\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. A valid "commit_sha" is required via options or the GIT_COMMIT environment variable to ensure accurate source map resolution and prevent cross-deployment collisions.\033[0m')
132
+ return None
133
+ options["commit_sha"] = commit_sha
134
+
135
+ service_id = options.get("service_id")
136
+ if not service_id or not str(service_id).strip():
137
+ logger.forceError('\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. HYPERPROBE_SERVICE_ID is required.\033[0m')
138
+ return None
139
+ if not UUID_REGEX.match(str(service_id).strip()):
140
+ logger.forceInfo('\033[1m\033[33m⚠️ [HyperProbe] WARN: service_id is not a valid UUID, please check again.\033[0m')
141
+
142
+ environment = options.get("environment") or os.getenv("HYPERPROBE_ENVIRONMENT")
143
+ if not environment or not str(environment).strip():
144
+ logger.forceError('\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. HYPERPROBE_ENVIRONMENT is required.\033[0m')
145
+ return None
146
+ options["environment"] = environment
147
+
148
+ broker_url = options.get("broker_url")
149
+ if not is_valid_broker_url(broker_url):
150
+ logger.forceError(f'\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. Invalid broker_url "{broker_url}". It must be a valid URL (http:// or https://) or a raw gRPC target (e.g. localhost:60051), with no query parameters and no trailing slash.\033[0m')
151
+ return None
152
+
153
+ cls._instance = cls(options)
154
+ cls._instance._start_background_loops()
155
+ logger.forceInfo(f"[HyperProbe] Python agent successfully started (ID: {cls._instance.agent_id}).")
156
+ return cls._instance
157
+
158
+ @classmethod
159
+ def shutdown(cls):
160
+ """Cleanly and safely shuts down the running agent singleton."""
161
+ with cls._lock:
162
+ if cls._instance is None:
163
+ return
164
+ cls._instance._stop()
165
+ cls._instance = None
166
+ print("[HyperProbe] Python agent successfully shut down.")
167
+
168
+ def _init_instrumentation_engine(self):
169
+ from hyperprobe.core.monitoring_engine import MonitoringEngine
170
+ return MonitoringEngine(
171
+ self.quota_manager,
172
+ self.safety_monitor,
173
+ self._handle_capture,
174
+ custom_set_trace_id=self.options.get("set_trace_id")
175
+ )
176
+
177
+ def _start_background_loops(self):
178
+ self.stop_event.clear()
179
+ self.safety_monitor.start()
180
+
181
+ self.sync_thread = threading.Thread(target=self._sync_loop, name="hyperprobe-sync")
182
+ self.sync_thread.daemon = True
183
+ self.sync_thread.start()
184
+
185
+ self.flush_thread = threading.Thread(target=self._flush_loop, name="hyperprobe-flush")
186
+ self.flush_thread.daemon = True
187
+ self.flush_thread.start()
188
+
189
+ self.stats_thread = threading.Thread(target=self._stats_loop, name="hyperprobe-stats")
190
+ self.stats_thread.daemon = True
191
+ self.stats_thread.start()
192
+
193
+ def _stop(self):
194
+ self.is_shutdown = True
195
+ self.stop_event.set()
196
+ self.safety_monitor.stop()
197
+
198
+ # Stop tracing immediately
199
+ self.engine.set_probes([])
200
+ self.engine.close()
201
+
202
+ with self._agent_lock:
203
+ self._cooldown_generation += 1
204
+ cooldown_timer = self.cooldown_timer
205
+ self.cooldown_timer = None
206
+ if cooldown_timer:
207
+ cooldown_timer.cancel()
208
+
209
+ self.broker_client.shutdown()
210
+
211
+ current_thread = threading.current_thread()
212
+ for thread in (self.sync_thread, self.flush_thread, self.stats_thread):
213
+ if thread and thread is not current_thread:
214
+ thread.join(timeout=2.0)
215
+
216
+ def _sync_loop(self):
217
+ while not self.stop_event.is_set():
218
+ try:
219
+ self._sync_with_broker()
220
+ except Exception as e:
221
+ logger.error(f"[HyperProbe] Sync loop encountered error: {str(e)}")
222
+ self.stop_event.wait(self.sync_interval_sec)
223
+
224
+ def _sync_with_broker(self):
225
+ response = self.broker_client.get_probes()
226
+ if not response:
227
+ return
228
+
229
+ # Update global config if returned
230
+ if response.HasField("global_config"):
231
+ gc = response.global_config
232
+ with self._agent_lock:
233
+ self.global_config.update({
234
+ "redact_keys": list(gc.redact_keys) if gc.redact_keys else self.global_config["redact_keys"],
235
+ "redact_values": list(gc.redact_values) if gc.redact_values else self.global_config["redact_values"],
236
+ "max_object_depth": gc.max_object_depth if gc.HasField("max_object_depth") else self.global_config["max_object_depth"],
237
+ "max_array_length": gc.max_array_length if gc.HasField("max_array_length") else self.global_config["max_array_length"],
238
+ "stack_frame_depth": gc.stack_frame_depth if gc.HasField("stack_frame_depth") else self.global_config["stack_frame_depth"],
239
+ "max_object_properties": gc.max_object_properties if gc.HasField("max_object_properties") else self.global_config["max_object_properties"],
240
+ "max_string_length": gc.max_string_length if gc.HasField("max_string_length") else self.global_config["max_string_length"],
241
+ })
242
+ self.engine.set_global_config(self.global_config)
243
+
244
+ # Only activate probe types implemented by this SDK.
245
+ server_probes = {}
246
+ for probe in response.probes:
247
+ if probe.type in SUPPORTED_PROBE_TYPES:
248
+ server_probes[probe.id] = probe
249
+ else:
250
+ msg = f"[HyperProbe] Ignoring unsupported probe type {probe.type} for probe {probe.id}."
251
+ logger.error(msg)
252
+
253
+ now_ms = int(time.time() * 1000)
254
+
255
+ with self._agent_lock:
256
+ # Cleanup stale state
257
+ self.active_probes = {pid: p for pid, p in self.active_probes.items() if pid in server_probes}
258
+ self.local_hits = {pid: count for pid, count in self.local_hits.items() if pid in server_probes}
259
+ self.pending_hits = {pid: count for pid, count in self.pending_hits.items() if pid in server_probes}
260
+
261
+ to_apply = []
262
+ for pid, p in server_probes.items():
263
+ hits = self.local_hits.get(pid, 0)
264
+ is_expired = p.expiry_time <= now_ms if p.expiry_time else False
265
+
266
+ if hits < p.hit_limit and not is_expired:
267
+ to_apply.append(p)
268
+ self.active_probes[pid] = p
269
+ else:
270
+ self.active_probes.pop(pid, None)
271
+
272
+ # set_probes updates the configured probes; the engine decides whether
273
+ # instrumentation may run while suspended.
274
+ self.engine.set_probes(to_apply)
275
+
276
+ def _flush_loop(self):
277
+ while not self.stop_event.is_set():
278
+ try:
279
+ self._flush_telemetry()
280
+ except Exception as e:
281
+ logger.error(f"[HyperProbe] Flush loop encountered error: {str(e)}")
282
+ self.stop_event.wait(self.flush_interval_sec)
283
+
284
+ def _stats_loop(self):
285
+ while not self.stop_event.is_set():
286
+ try:
287
+ stats = self.engine.get_stats()
288
+ if stats["hits"] > 0 or stats["skips"] > 0:
289
+ logger.info(f"[HyperProbe] Probes Hit: {stats['hits']}, Probes Skipped: {stats['skips']}")
290
+ except Exception as e:
291
+ logger.error(f"[HyperProbe] Stats loop encountered error: {str(e)}")
292
+ self.stop_event.wait(5.0)
293
+
294
+ def _flush_telemetry(self):
295
+ batch = self._inflight_batch
296
+ if batch is None:
297
+ batch = []
298
+ # Pull all currently queued events up to capacity. Keep the batch
299
+ # in-flight until the broker call succeeds so retries cannot lose it.
300
+ while not self.telemetry_queue.empty():
301
+ try:
302
+ item = self.telemetry_queue.get_nowait()
303
+ if not isinstance(item, _TelemetryItem):
304
+ item = _TelemetryItem(item)
305
+ batch.append(item)
306
+ except queue.Empty:
307
+ break
308
+
309
+ if not batch:
310
+ return
311
+
312
+ self._inflight_batch = batch
313
+ try:
314
+ finished_probe_ids = self.broker_client.report_telemetry(
315
+ [item.event for item in batch]
316
+ )
317
+ except Exception as e:
318
+ logger.error(
319
+ f"[HyperProbe] Failed to flush telemetry: {str(e)}. "
320
+ f"Retaining {len(batch)} events for retry."
321
+ )
322
+ return
323
+
324
+ for item in batch:
325
+ if item.bandwidth_reservation is not None:
326
+ item.bandwidth_reservation.commit()
327
+ self._inflight_batch = None
328
+
329
+ # If broker confirmed certain probes reached global limit, clear them locally
330
+ if finished_probe_ids:
331
+ changed = False
332
+ with self._agent_lock:
333
+ for pid in finished_probe_ids:
334
+ if pid in self.active_probes:
335
+ self.active_probes.pop(pid, None)
336
+ changed = True
337
+ if changed:
338
+ probes_list = list(self.active_probes.values())
339
+ if changed:
340
+ self.engine.set_probes(probes_list)
341
+
342
+ def _reserve_hit_slot(self, probe_id, probe):
343
+ with self._agent_lock:
344
+ if self.active_probes.get(probe_id) is not probe:
345
+ return False
346
+ committed_hits = self.local_hits.get(probe_id, 0)
347
+ pending_hits = self.pending_hits.get(probe_id, 0)
348
+ if committed_hits + pending_hits >= probe.hit_limit:
349
+ return False
350
+ self.pending_hits[probe_id] = pending_hits + 1
351
+ return True
352
+
353
+ def _release_hit_slot(self, probe_id):
354
+ with self._agent_lock:
355
+ pending_hits = self.pending_hits.get(probe_id, 0)
356
+ if pending_hits <= 1:
357
+ self.pending_hits.pop(probe_id, None)
358
+ else:
359
+ self.pending_hits[probe_id] = pending_hits - 1
360
+
361
+ def _commit_hit_slot(self, probe_id, probe):
362
+ with self._agent_lock:
363
+ pending_hits = self.pending_hits.get(probe_id, 0)
364
+ if pending_hits <= 1:
365
+ self.pending_hits.pop(probe_id, None)
366
+ else:
367
+ self.pending_hits[probe_id] = pending_hits - 1
368
+
369
+ hits = self.local_hits.get(probe_id, 0) + 1
370
+ self.local_hits[probe_id] = hits
371
+ if hits < probe.hit_limit:
372
+ return None
373
+
374
+ self.active_probes.pop(probe_id, None)
375
+ return list(self.active_probes.values())
376
+
377
+ def _handle_capture(self, event):
378
+ probe_id = event["probe_id"]
379
+ with self._agent_lock:
380
+ probe = self.active_probes.get(probe_id)
381
+ if not probe:
382
+ return
383
+
384
+ if probe.type not in SUPPORTED_PROBE_TYPES:
385
+ return
386
+
387
+ if not self._reserve_hit_slot(probe_id, probe):
388
+ return
389
+
390
+ try:
391
+ event_size = self.broker_client.estimate_event_size(event)
392
+ logger.info(f"[HyperProbe] Got event size : {event_size} Bytes")
393
+ except Exception as exc:
394
+ logger.error(
395
+ f"[HyperProbe] Failed to size telemetry for probe {probe_id}: {exc}"
396
+ )
397
+ self._release_hit_slot(probe_id)
398
+ return
399
+
400
+ bandwidth_reservation = self.quota_manager.reserve_bandwidth(event_size)
401
+ if bandwidth_reservation is None:
402
+ msg = f"[HyperProbe] Bandwidth quota exceeded; dropping event for probe {probe_id} , event size was : {event_size} bytes."
403
+ logger.error(msg)
404
+ self._release_hit_slot(probe_id)
405
+ return
406
+
407
+ # Hold the bandwidth reservation with the event until the broker
408
+ # confirms the batch. Release it if local queue admission fails.
409
+ item = _TelemetryItem(event, bandwidth_reservation)
410
+ try:
411
+ self.telemetry_queue.put_nowait(item)
412
+ except queue.Full:
413
+ bandwidth_reservation.release()
414
+ self._release_hit_slot(probe_id)
415
+ return
416
+ except Exception as exc:
417
+ bandwidth_reservation.release()
418
+ self._release_hit_slot(probe_id)
419
+ logger.error(
420
+ f"[HyperProbe] Failed to queue telemetry for probe {probe_id}: {exc}"
421
+ )
422
+ return
423
+
424
+ probes_list = self._commit_hit_slot(probe_id, probe)
425
+ if probes_list is not None:
426
+ self.engine.set_probes(probes_list)
427
+
428
+ def _handle_health_change(self, health, reason):
429
+ if self.is_shutdown:
430
+ return
431
+
432
+ if health == AgentHealth.RED:
433
+ logger.error(f"[HyperProbe] Safety Shield Triggered (RED): {reason or ''}. Suspending tracing.")
434
+ with self._agent_lock:
435
+ self.engine.suspend()
436
+
437
+ if self.cooldown_timer is not None:
438
+ self.cooldown_timer.cancel()
439
+
440
+ self._cooldown_generation += 1
441
+ cooldown_generation = self._cooldown_generation
442
+ cooldown_sec = self._cooldown_sec
443
+ self.cooldown_timer = threading.Timer(
444
+ cooldown_sec,
445
+ lambda: self._cooldown_expired(cooldown_generation),
446
+ )
447
+ self.cooldown_timer.daemon = True
448
+ self.cooldown_timer.start()
449
+
450
+ elif health == AgentHealth.YELLOW:
451
+ logger.error(f"[HyperProbe] Safety Warning (YELLOW): {reason or ''}.")
452
+ elif health == AgentHealth.GREEN:
453
+ with self._agent_lock:
454
+ cooldown_expired = self.cooldown_timer is None
455
+ if cooldown_expired:
456
+ self._resume_instrumentation()
457
+
458
+ def _cooldown_expired(self, cooldown_generation):
459
+ with self._agent_lock:
460
+ if cooldown_generation != self._cooldown_generation:
461
+ return
462
+ self.cooldown_timer = None
463
+ self._resume_instrumentation()
464
+
465
+ def _resume_instrumentation(self):
466
+ with self._agent_lock:
467
+ if (
468
+ self.is_shutdown
469
+ or self.safety_monitor.get_health() != AgentHealth.GREEN
470
+ or self.cooldown_timer is not None
471
+ ):
472
+ return
473
+ logger.info("[HyperProbe] Cooldown window expired. Resuming probes from local cache.")
474
+ probes_list = list(self.active_probes.values())
475
+ self.engine.set_probes(probes_list)
476
+ self.engine.resume()
@@ -0,0 +1,40 @@
1
+ import sys
2
+ import os
3
+
4
+ def main():
5
+ if len(sys.argv) < 2:
6
+ print("Usage: hyperprobe-run <python_script_or_command> [args...]")
7
+ sys.exit(1)
8
+
9
+ target_args = sys.argv[1:]
10
+ cmd = target_args[0]
11
+
12
+ # Locate the SDK and injection directories
13
+ sdk_path = os.path.dirname(os.path.abspath(__file__))
14
+ parent_path = os.path.dirname(sdk_path)
15
+ injection_path = os.path.join(sdk_path, "core", "injection")
16
+
17
+ # We inject two things into PYTHONPATH:
18
+ # 1. The injection_path (which contains sitecustomize.py) so Python runs it natively at boot
19
+ # 2. The parent_path so that sitecustomize.py can successfully `import hyperprobe.agent`
20
+
21
+ old_pythonpath = os.environ.get("PYTHONPATH", "")
22
+ new_paths = os.pathsep.join((injection_path, parent_path))
23
+
24
+ if old_pythonpath:
25
+ os.environ["PYTHONPATH"] = os.pathsep.join((new_paths, old_pythonpath))
26
+ else:
27
+ os.environ["PYTHONPATH"] = new_paths
28
+
29
+ # Standard execution replacement
30
+ # The current hyperprobe-run process is completely replaced by the target application.
31
+ # Because we manipulated PYTHONPATH, Python will natively execute our sitecustomize.py
32
+ # before running the user's application, resulting in perfect, zero-code-change auto-instrumentation!
33
+ try:
34
+ os.execvp(cmd, target_args)
35
+ except FileNotFoundError:
36
+ print(f"[HyperProbe] Error: Executable '{cmd}' not found in PATH.", file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+ if __name__ == "__main__":
40
+ main()
@@ -0,0 +1 @@
1
+ # HyperProbe Core Module
@@ -0,0 +1,131 @@
1
+ import os
2
+ import sys
3
+ import time
4
+
5
+ try:
6
+ # Shaded/vendored fallback
7
+ import _vendor.grpc as grpc
8
+ except ImportError:
9
+ import grpc
10
+
11
+ # Import our generated protos
12
+ from hyperprobe.protos import agent_pb2
13
+ from hyperprobe.protos import agent_pb2_grpc
14
+ from hyperprobe.core.logger import get_logger
15
+
16
+ logger = get_logger("hyperprobe:broker")
17
+
18
+
19
+ class BrokerTransportError(RuntimeError):
20
+ """Raised when a broker RPC cannot be completed."""
21
+
22
+ class BrokerClient:
23
+ def __init__(
24
+ self,
25
+ broker_url,
26
+ service_id,
27
+ environment,
28
+ commit_sha,
29
+ agent_id,
30
+ agent_version=None,
31
+ rpc_timeout_sec=10.0,
32
+ ):
33
+ self.broker_url = broker_url
34
+ self.service_id = service_id
35
+ self.environment = environment
36
+ self.commit_sha = commit_sha
37
+ self.agent_id = agent_id
38
+ if agent_version is None:
39
+ try:
40
+ from importlib.metadata import version
41
+ agent_version = version("hyperprobe-agent")
42
+ except Exception:
43
+ agent_version = "unknown"
44
+ self.agent_version = agent_version
45
+ self.rpc_timeout_sec = max(float(rpc_timeout_sec), 0.1)
46
+ self.metadata = (("x-hp-service-id", str(service_id)),)
47
+ self.hostname = os.uname().nodename if hasattr(os, 'uname') else 'unknown'
48
+
49
+ # Establish channel depending on URL protocol to support both secure (SSL) and insecure brokers
50
+ if broker_url.startswith("https://"):
51
+ clean_url = broker_url[8:]
52
+ self.channel = grpc.secure_channel(clean_url, grpc.ssl_channel_credentials())
53
+ elif broker_url.startswith("http://"):
54
+ clean_url = broker_url[7:]
55
+ self.channel = grpc.insecure_channel(clean_url)
56
+ else:
57
+ self.channel = grpc.insecure_channel(broker_url)
58
+
59
+ self.stub = agent_pb2_grpc.AgentBrokerStub(self.channel)
60
+
61
+ def shutdown(self):
62
+ self.channel.close()
63
+
64
+ def get_probes(self):
65
+ """Fetches active probes from the HyperProbe Broker."""
66
+ request = agent_pb2.GetProbesRequest(
67
+ agent_id=self.agent_id,
68
+ service_id=self.service_id,
69
+ environment=self.environment,
70
+ commit_sha=self.commit_sha,
71
+ language="python",
72
+ agent_version=self.agent_version,
73
+ hostname=self.hostname
74
+ )
75
+ try:
76
+ response = self.stub.GetProbes(
77
+ request,
78
+ timeout=self.rpc_timeout_sec,
79
+ metadata=self.metadata,
80
+ )
81
+ return response
82
+ except Exception as e:
83
+ raise BrokerTransportError("GetProbes failed") from e
84
+
85
+ @staticmethod
86
+ def _to_proto_event(event):
87
+ stack_frames = [
88
+ agent_pb2.StackFrame(
89
+ function_name=frame["function_name"],
90
+ file_name=frame["file_name"],
91
+ line_number=frame["line_number"],
92
+ column_number=frame["column_number"],
93
+ )
94
+ for frame in event.get("stack_frames", [])
95
+ ]
96
+
97
+ proto_event = agent_pb2.TelemetryEvent(
98
+ probe_id=event["probe_id"],
99
+ timestamp_ms=event["timestamp_ms"],
100
+ stack_frames=stack_frames,
101
+ captured_vars_json=event.get("captured_vars_json", ""),
102
+ watch_results_json=event.get("watch_results_json", ""),
103
+ evaluated_log=event.get("evaluated_log", ""),
104
+ metric_value=event.get("metric_value", 0.0),
105
+ capture_error=event.get("capture_error", ""),
106
+ )
107
+ if event.get("trace_id"):
108
+ proto_event.trace_id = event["trace_id"]
109
+ return proto_event
110
+
111
+ def estimate_event_size(self, event):
112
+ return len(self._to_proto_event(event).SerializeToString())
113
+
114
+ def report_telemetry(self, events):
115
+ """Sends a batch of captured Telemetry Events to the HyperProbe Broker."""
116
+ batch_events = [self._to_proto_event(event) for event in events]
117
+
118
+ batch = agent_pb2.TelemetryBatch(
119
+ agent_id=self.agent_id,
120
+ events=batch_events
121
+ )
122
+
123
+ try:
124
+ response = self.stub.ReportTelemetry(
125
+ batch,
126
+ timeout=self.rpc_timeout_sec,
127
+ metadata=self.metadata,
128
+ )
129
+ return response.finished_probe_ids
130
+ except Exception as e:
131
+ raise BrokerTransportError("ReportTelemetry failed") from e