hyperprobe-agent 1.2.24__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,177 @@
1
+ import queue
2
+ import sys
3
+ import threading
4
+ from datetime import datetime, timezone
5
+
6
+
7
+ _COLOR_RESET = "\x1b[0m"
8
+ _COLOR_BOLD = "\x1b[1m"
9
+ _COLOR_GRAY = "\x1b[90m"
10
+ _COLOR_MAGENTA = "\x1b[35m"
11
+ _LEVEL_COLORS = {
12
+ "ERROR": "\x1b[31m",
13
+ "INFO": "\x1b[32m",
14
+ }
15
+ _STOP = object()
16
+
17
+
18
+ def normalize_probe_log_level(value):
19
+ candidate = value.strip().upper() if isinstance(value, str) else ""
20
+ return candidate if candidate in _LEVEL_COLORS else "INFO"
21
+
22
+
23
+ def sanitize_probe_log_message(value):
24
+ message = value if isinstance(value, str) else str(value)
25
+ sanitized = []
26
+ escapes = {
27
+ "\n": r"\n",
28
+ "\r": r"\r",
29
+ "\t": r"\t",
30
+ }
31
+
32
+ for character in message:
33
+ if character in escapes:
34
+ sanitized.append(escapes[character])
35
+ elif character.isprintable():
36
+ sanitized.append(character)
37
+ else:
38
+ codepoint = ord(character)
39
+ if codepoint <= 0xFF:
40
+ sanitized.append(f"\\x{codepoint:02x}")
41
+ elif codepoint <= 0xFFFF:
42
+ sanitized.append(f"\\u{codepoint:04x}")
43
+ else:
44
+ sanitized.append(f"\\U{codepoint:08x}")
45
+
46
+ return "".join(sanitized)
47
+
48
+
49
+ def format_probe_log(timestamp_ms, level, message, use_color=False):
50
+ normalized_level = normalize_probe_log_level(level)
51
+ timestamp = datetime.fromtimestamp(
52
+ timestamp_ms / 1000,
53
+ tz=timezone.utc,
54
+ ).isoformat(timespec="milliseconds").replace("+00:00", "Z")
55
+ safe_message = sanitize_probe_log_message(message)
56
+
57
+ if not use_color:
58
+ return f"[{timestamp}] [{normalized_level}] [HyperProbe LOG] {safe_message}\n"
59
+
60
+ level_color = _LEVEL_COLORS[normalized_level]
61
+ return (
62
+ f"{_COLOR_GRAY}[{timestamp}]{_COLOR_RESET} "
63
+ f"{_COLOR_BOLD}{level_color}[{normalized_level}]{_COLOR_RESET} "
64
+ f"{_COLOR_BOLD}{_COLOR_MAGENTA}[HyperProbe LOG]{_COLOR_RESET} "
65
+ f"{safe_message}\n"
66
+ )
67
+
68
+
69
+ class ProbeLogWriter:
70
+ """Best-effort probe output that never blocks capture threads."""
71
+
72
+ def __init__(self, max_queue_size, stdout=None, stderr=None):
73
+ self._queue = queue.Queue(maxsize=max_queue_size)
74
+ self._stdout = stdout
75
+ self._stderr = stderr
76
+ self._lifecycle_lock = threading.RLock()
77
+ self._thread = None
78
+ self._closed = False
79
+ self.dropped_count = 0
80
+ self.error_count = 0
81
+
82
+ def start(self):
83
+ with self._lifecycle_lock:
84
+ return self._start_locked()
85
+
86
+ def _start_locked(self):
87
+ if self._closed:
88
+ return False
89
+ if self._thread and self._thread.is_alive():
90
+ return True
91
+ self._thread = threading.Thread(
92
+ target=self._run,
93
+ name="hyperprobe-probe-output",
94
+ daemon=True,
95
+ )
96
+ self._thread.start()
97
+ return True
98
+
99
+ def is_running(self):
100
+ with self._lifecycle_lock:
101
+ return bool(self._thread and self._thread.is_alive())
102
+
103
+ def submit(self, timestamp_ms, level, message):
104
+ record = (timestamp_ms, normalize_probe_log_level(level), message)
105
+ with self._lifecycle_lock:
106
+ if self._closed:
107
+ return False
108
+ try:
109
+ self._queue.put_nowait(record)
110
+ self._start_locked()
111
+ return True
112
+ except queue.Full:
113
+ self.dropped_count += 1
114
+ return False
115
+ except Exception:
116
+ self.error_count += 1
117
+ return False
118
+
119
+ def stop(self, timeout=2.0):
120
+ with self._lifecycle_lock:
121
+ self._closed = True
122
+ thread = self._thread
123
+ if thread:
124
+ try:
125
+ self._queue.put_nowait(_STOP)
126
+ except queue.Full:
127
+ pass
128
+
129
+ if thread and thread is not threading.current_thread():
130
+ thread.join(timeout=timeout)
131
+
132
+ def _run(self):
133
+ while True:
134
+ try:
135
+ record = self._queue.get(timeout=0.1)
136
+ except queue.Empty:
137
+ if self._retire_if_empty():
138
+ return
139
+ continue
140
+
141
+ try:
142
+ if record is not _STOP:
143
+ self._write(record)
144
+ finally:
145
+ self._queue.task_done()
146
+
147
+ if self._retire_if_empty():
148
+ return
149
+
150
+ def _retire_if_empty(self):
151
+ with self._lifecycle_lock:
152
+ if not self._queue.empty():
153
+ return False
154
+ if self._thread is threading.current_thread():
155
+ self._thread = None
156
+ return True
157
+
158
+ def _write(self, record):
159
+ timestamp_ms, level, message = record
160
+ stream = (
161
+ self._stderr if self._stderr is not None else sys.stderr
162
+ ) if level == "ERROR" else (
163
+ self._stdout if self._stdout is not None else sys.stdout
164
+ )
165
+
166
+ try:
167
+ use_color = bool(getattr(stream, "isatty", lambda: False)())
168
+ output = format_probe_log(timestamp_ms, level, message, use_color)
169
+ written = stream.write(output)
170
+ if written is not None and written != len(output):
171
+ with self._lifecycle_lock:
172
+ self.error_count += 1
173
+ return
174
+ stream.flush()
175
+ except Exception:
176
+ with self._lifecycle_lock:
177
+ self.error_count += 1
@@ -0,0 +1,73 @@
1
+ import time
2
+ import threading
3
+
4
+
5
+ class QuotaReservation:
6
+ def __init__(self, bucket, count: float):
7
+ self._bucket = bucket
8
+ self._count = count
9
+ self._closed = False
10
+ self._lock = threading.Lock()
11
+
12
+ def commit(self):
13
+ with self._lock:
14
+ self._closed = True
15
+
16
+ def release(self):
17
+ with self._lock:
18
+ if self._closed:
19
+ return
20
+ self._closed = True
21
+ self._bucket.refund(self._count)
22
+
23
+
24
+ class TokenBucket:
25
+ def __init__(self, capacity: float, refill_rate_per_sec: float):
26
+ self.capacity = capacity
27
+ self.refill_rate = refill_rate_per_sec # tokens per second
28
+ self.tokens = capacity
29
+ self.last_refill = time.monotonic()
30
+ self.lock = threading.Lock()
31
+
32
+ def try_consume(self, count: float = 1.0) -> bool:
33
+ reservation = self.reserve(count)
34
+ if reservation is None:
35
+ return False
36
+ reservation.commit()
37
+ return True
38
+
39
+ def reserve(self, count: float = 1.0):
40
+ with self.lock:
41
+ self._refill()
42
+ if self.tokens >= count:
43
+ self.tokens -= count
44
+ return QuotaReservation(self, count)
45
+ return None
46
+
47
+ def refund(self, count: float):
48
+ with self.lock:
49
+ self._refill()
50
+ self.tokens = min(self.capacity, self.tokens + count)
51
+
52
+ def _refill(self):
53
+ now = time.monotonic()
54
+ delta = now - self.last_refill
55
+ amount = delta * self.refill_rate
56
+ self.tokens = min(self.capacity, self.tokens + amount)
57
+ self.last_refill = now
58
+
59
+
60
+ class QuotaManager:
61
+ def __init__(self, hits_per_sec: float = 10.0, bytes_per_sec: float = 200.0 * 1024.0):
62
+ # Burst capacity of hits_per_sec and bytes_per_sec (1 second worth)
63
+ self.eval_bucket = TokenBucket(hits_per_sec, hits_per_sec)
64
+ self.bandwidth_bucket = TokenBucket(bytes_per_sec, bytes_per_sec)
65
+
66
+ def can_evaluate(self) -> bool:
67
+ return self.eval_bucket.try_consume(1.0)
68
+
69
+ def can_send(self, bytes_count: float) -> bool:
70
+ return self.bandwidth_bucket.try_consume(bytes_count)
71
+
72
+ def reserve_bandwidth(self, bytes_count: float):
73
+ return self.bandwidth_bucket.reserve(bytes_count)
@@ -0,0 +1,136 @@
1
+ import time
2
+ import threading
3
+
4
+ class AgentHealth:
5
+ GREEN = 'GREEN' # All good
6
+ YELLOW = 'YELLOW' # Rate limited (Reactive Shedding)
7
+ RED = 'RED' # Critical Lag (Proactive Shedding - Detach)
8
+
9
+
10
+ class SafetyMonitor:
11
+ def __init__(self, on_state_change, max_lag_ms=50.0, pause_budget_ms=15.0):
12
+ self.on_state_change = on_state_change
13
+ self.max_lag_ms = max_lag_ms
14
+ self.pause_budget_ms = pause_budget_ms
15
+
16
+ self.health = AgentHealth.GREEN
17
+ self.cumulative_pause_time = 0.0
18
+ self.last_window_reset = time.monotonic()
19
+ self.last_thread_lag_ms = 0.0
20
+
21
+ self.lock = threading.RLock()
22
+ self._notification_lock = threading.RLock()
23
+
24
+ self.is_running = False
25
+ self.thread = None
26
+ self.stop_event = threading.Event()
27
+
28
+ def start(self):
29
+ self.stop_event.clear()
30
+ self.is_running = True
31
+ self.thread = threading.Thread(target=self._run_loop, name="hyperprobe-safety")
32
+ self.thread.daemon = True
33
+ self.thread.start()
34
+
35
+ def stop(self):
36
+ self.is_running = False
37
+ self.stop_event.set()
38
+ if self.thread and self.thread is not threading.current_thread():
39
+ self.thread.join(timeout=2.0)
40
+ self.thread = None
41
+
42
+ def report_pause_duration(self, ms: float):
43
+ with self.lock:
44
+ self.cumulative_pause_time += ms
45
+ transition = self._evaluate_health_locked(self.last_thread_lag_ms)
46
+
47
+ self._notify_state_change(transition)
48
+
49
+ def _run_loop(self):
50
+ last_heartbeat = time.monotonic()
51
+
52
+ while self.is_running and not self.stop_event.wait(0.1):
53
+ now = time.monotonic()
54
+ thread_lag_ms = (now - last_heartbeat - 0.1) * 1000.0
55
+ last_heartbeat = now
56
+
57
+ with self.lock:
58
+ self.last_thread_lag_ms = thread_lag_ms
59
+ if now - self.last_window_reset > 1.0:
60
+ self.cumulative_pause_time = 0.0
61
+ self.last_window_reset = now
62
+
63
+ transition = self._evaluate_health_locked(thread_lag_ms)
64
+
65
+ self._notify_state_change(transition)
66
+
67
+ def _check_health(self, thread_lag_ms: float = 0.0):
68
+ """
69
+ Preserves the existing method signature while ensuring that
70
+ callbacks execute outside the state lock.
71
+ """
72
+ with self.lock:
73
+ transition = self._evaluate_health_locked(thread_lag_ms)
74
+
75
+ self._notify_state_change(transition)
76
+
77
+ def _evaluate_health_locked(self, thread_lag_ms: float = 0.0):
78
+ previous_health = self.health
79
+ reason = None
80
+
81
+ if thread_lag_ms > self.max_lag_ms:
82
+ self.health = AgentHealth.RED
83
+ reason = (
84
+ f"Execution Thread Lag ({thread_lag_ms:.1f}ms) exceeded "
85
+ f"limit ({self.max_lag_ms}ms)"
86
+ )
87
+ elif self.cumulative_pause_time > self.pause_budget_ms:
88
+ self.health = AgentHealth.RED
89
+ reason = (
90
+ f"Cumulative Pause Budget ({self.cumulative_pause_time:.1f}ms) "
91
+ f"exceeded limit ({self.pause_budget_ms}ms)"
92
+ )
93
+ elif thread_lag_ms > self.max_lag_ms / 2.0:
94
+ self.health = AgentHealth.YELLOW
95
+ reason = (
96
+ f"Moderate impact: Execution Thread Lag "
97
+ f"({thread_lag_ms:.1f}ms) reached 50% of limit "
98
+ f"({self.max_lag_ms}ms)"
99
+ )
100
+ elif self.cumulative_pause_time > self.pause_budget_ms / 2.0:
101
+ self.health = AgentHealth.YELLOW
102
+ reason = (
103
+ f"Moderate impact: Cumulative Pause Budget "
104
+ f"({self.cumulative_pause_time:.1f}ms) reached 50% of limit "
105
+ f"({self.pause_budget_ms}ms)"
106
+ )
107
+ else:
108
+ self.health = AgentHealth.GREEN
109
+ if previous_health != AgentHealth.GREEN:
110
+ reason = "System stabilized."
111
+
112
+ if self.health != previous_health:
113
+ return self.health, reason
114
+
115
+ return None
116
+
117
+ def _notify_state_change(self, transition):
118
+ if transition is None:
119
+ return
120
+
121
+ health, reason = transition
122
+
123
+ with self._notification_lock:
124
+ with self.lock:
125
+ if self.health != health:
126
+ return
127
+
128
+ try:
129
+ self.on_state_change(health, reason)
130
+ except Exception:
131
+ # Safety callbacks must never break application execution.
132
+ pass
133
+
134
+ def get_health(self) -> str:
135
+ with self.lock:
136
+ return self.health
@@ -0,0 +1,214 @@
1
+ import math
2
+
3
+
4
+ def _type_summary(obj):
5
+ type_name = type(obj).__name__
6
+ if isinstance(obj, (bytes, bytearray, memoryview)):
7
+ try:
8
+ return f"[{type_name}: {len(obj)} bytes]"
9
+ except Exception:
10
+ return f"[{type_name}]"
11
+ if isinstance(obj, dict):
12
+ try:
13
+ return f"[Dict: {len(obj)} keys]"
14
+ except Exception:
15
+ return "[Dict]"
16
+ if isinstance(obj, (list, tuple, set)):
17
+ try:
18
+ return f"[List: {len(obj)} items]"
19
+ except Exception:
20
+ return "[List]"
21
+ return f"[Obj: {type_name}]"
22
+
23
+
24
+ def _safe_key(key):
25
+ if isinstance(key, (str, int, float, bool)) or key is None:
26
+ return str(key)
27
+ return f"[{type(key).__name__} key]"
28
+
29
+
30
+ def _safe_length(value):
31
+ try:
32
+ return len(value)
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def serialize(
38
+ obj,
39
+ max_depth=3,
40
+ max_array_length=3,
41
+ max_object_properties=50,
42
+ max_string_length=1024,
43
+ redact_keys_re=None,
44
+ redact_values_re=None,
45
+ visited=None,
46
+ depth=0,
47
+ path="$",
48
+ ):
49
+ """Serialize a bounded object graph without invoking arbitrary repr()."""
50
+ if visited is None:
51
+ visited = {}
52
+
53
+ if obj is None:
54
+ return None
55
+ if isinstance(obj, bool):
56
+ return obj
57
+ if isinstance(obj, int):
58
+ return obj
59
+ if isinstance(obj, float):
60
+ if not math.isfinite(obj):
61
+ return str(obj)
62
+ return obj
63
+ if isinstance(obj, str):
64
+ value = obj
65
+ if len(value) > max_string_length:
66
+ value = (
67
+ value[:max_string_length]
68
+ + f"... [Truncated: +{len(value) - max_string_length} more chars]"
69
+ )
70
+ if redact_values_re and redact_values_re.search(value):
71
+ value = redact_values_re.sub("[REDACTED Value]", value)
72
+ return value
73
+ if isinstance(obj, (bytes, bytearray, memoryview)):
74
+ return _type_summary(obj)
75
+
76
+ obj_id = id(obj)
77
+ previous = visited.get(obj_id)
78
+ if previous is not None:
79
+ previous_obj, previous_path = previous
80
+ if previous_obj is obj:
81
+ return f"[REF - {previous_path}]"
82
+
83
+ if depth > max_depth:
84
+ return _type_summary(obj)
85
+
86
+ # Retain the object for the whole serialization operation. This preserves
87
+ # aliases and prevents stale id() reuse while traversing generators.
88
+ visited[obj_id] = (obj, path)
89
+
90
+ if isinstance(obj, dict):
91
+ result = {}
92
+ processed = 0
93
+ try:
94
+ iterator = iter(obj.items())
95
+ while processed < max_object_properties:
96
+ try:
97
+ key, value = next(iterator)
98
+ except StopIteration:
99
+ break
100
+
101
+ key_string = _safe_key(key)
102
+ if redact_keys_re and redact_keys_re.search(key_string):
103
+ result[key_string] = "[REDACTED Key]"
104
+ else:
105
+ child_path = f"{path}.{key_string}" if path else key_string
106
+ result[key_string] = serialize(
107
+ value,
108
+ max_depth=max_depth,
109
+ max_array_length=max_array_length,
110
+ max_object_properties=max_object_properties,
111
+ max_string_length=max_string_length,
112
+ redact_keys_re=redact_keys_re,
113
+ redact_values_re=redact_values_re,
114
+ visited=visited,
115
+ depth=depth + 1,
116
+ path=child_path,
117
+ )
118
+ processed += 1
119
+
120
+ total = _safe_length(obj)
121
+ if total is not None and total > processed:
122
+ result["__probe_meta"] = (
123
+ f"+ {total - processed} more properties truncated"
124
+ )
125
+ elif total is None:
126
+ try:
127
+ next(iterator)
128
+ except StopIteration:
129
+ pass
130
+ else:
131
+ result["__probe_meta"] = "+ more properties truncated"
132
+ except Exception:
133
+ result["__probe_meta"] = "[Dictionary traversal failed]"
134
+ return result
135
+
136
+ if isinstance(obj, (list, tuple, set)):
137
+ result = []
138
+ processed = 0
139
+ try:
140
+ iterator = iter(obj)
141
+ while processed < max_array_length:
142
+ try:
143
+ value = next(iterator)
144
+ except StopIteration:
145
+ break
146
+ result.append(
147
+ serialize(
148
+ value,
149
+ max_depth=max_depth,
150
+ max_array_length=max_array_length,
151
+ max_object_properties=max_object_properties,
152
+ max_string_length=max_string_length,
153
+ redact_keys_re=redact_keys_re,
154
+ redact_values_re=redact_values_re,
155
+ visited=visited,
156
+ depth=depth + 1,
157
+ path=f"{path}[{processed}]",
158
+ )
159
+ )
160
+ processed += 1
161
+
162
+ total = _safe_length(obj)
163
+ if total is not None and total > processed:
164
+ result.append(f"[+ {total - processed} more items truncated]")
165
+ elif total is None:
166
+ try:
167
+ next(iterator)
168
+ except StopIteration:
169
+ pass
170
+ else:
171
+ result.append("[+ more items truncated]")
172
+ except Exception:
173
+ result.append("[Iterable traversal failed]")
174
+ return result
175
+
176
+ try:
177
+ properties = vars(obj)
178
+ except Exception:
179
+ return _type_summary(obj)
180
+
181
+ result = {}
182
+ try:
183
+ keys = [key for key in properties if not key.startswith("_")]
184
+ except Exception:
185
+ return _type_summary(obj)
186
+
187
+ for index, key in enumerate(keys[:max_object_properties]):
188
+ if redact_keys_re and redact_keys_re.search(key):
189
+ result[key] = "[REDACTED Key]"
190
+ continue
191
+
192
+ child_path = f"{path}.{key}" if path else key
193
+ try:
194
+ value = properties[key]
195
+ result[key] = serialize(
196
+ value,
197
+ max_depth=max_depth,
198
+ max_array_length=max_array_length,
199
+ max_object_properties=max_object_properties,
200
+ max_string_length=max_string_length,
201
+ redact_keys_re=redact_keys_re,
202
+ redact_values_re=redact_values_re,
203
+ visited=visited,
204
+ depth=depth + 1,
205
+ path=child_path,
206
+ )
207
+ except Exception:
208
+ result[key] = "[Inaccessible]"
209
+
210
+ if len(keys) > max_object_properties:
211
+ result["__probe_meta"] = (
212
+ f"+ {len(keys) - max_object_properties} more properties truncated"
213
+ )
214
+ return result
@@ -0,0 +1,93 @@
1
+ import sys
2
+ import os
3
+ import importlib
4
+ from hyperprobe.core.logger import get_logger
5
+
6
+ logger = get_logger("hyperprobe:trace")
7
+
8
+ def extract_trace_context(custom_get_trace_id=None):
9
+ """
10
+ Safely extract the active trace ID from standard APMs via sys.modules reflection
11
+ or utilize a custom user-defined hook.
12
+ """
13
+ # 1. User-defined custom hook (Highest Priority - passed via code)
14
+ if custom_get_trace_id and callable(custom_get_trace_id):
15
+ try:
16
+ trace_id = custom_get_trace_id()
17
+ if trace_id:
18
+ return str(trace_id)
19
+ except Exception as e:
20
+ logger.info(f"[HyperProbe] Custom trace extractor failed: {e}")
21
+
22
+ # 1.5. Environment variable hook
23
+ hook_path = os.getenv("HYPERPROBE_TRACE_HOOK")
24
+ if hook_path and ":" in hook_path:
25
+ try:
26
+ module_name, func_name = hook_path.split(":", 1)
27
+ # Safely ensure CWD is in the import path
28
+ cwd = os.getcwd()
29
+ added_to_path = False
30
+
31
+ if cwd not in sys.path:
32
+ sys.path.insert(0, cwd)
33
+ added_to_path = True
34
+
35
+ try:
36
+ user_module = importlib.import_module(module_name)
37
+ user_func = getattr(user_module, func_name)
38
+ trace_id = user_func()
39
+ if trace_id:
40
+ return str(trace_id)
41
+ finally:
42
+ # Clean up if we modified sys.path
43
+ if added_to_path:
44
+ sys.path.remove(cwd)
45
+
46
+ except Exception as e:
47
+ logger.info(f"[HyperProbe] Failed to execute HYPERPROBE_TRACE_HOOK: {e}")
48
+
49
+ # 2. OpenTelemetry
50
+ try:
51
+ if 'opentelemetry.trace' in sys.modules:
52
+ otel_trace = sys.modules['opentelemetry.trace']
53
+ span = otel_trace.get_current_span()
54
+ if span:
55
+ ctx = span.get_span_context()
56
+ if ctx and ctx.is_valid:
57
+ return format(ctx.trace_id, '032x')
58
+ except Exception:
59
+ pass
60
+
61
+ # 3. Datadog (ddtrace)
62
+ try:
63
+ if 'ddtrace' in sys.modules:
64
+ ddtrace = sys.modules['ddtrace']
65
+ span = ddtrace.tracer.current_span()
66
+ if span and span.trace_id:
67
+ return str(span.trace_id)
68
+ except Exception:
69
+ pass
70
+
71
+ # 4. New Relic
72
+ try:
73
+ if 'newrelic.agent' in sys.modules:
74
+ nr = sys.modules['newrelic.agent']
75
+ if hasattr(nr, 'current_trace_id'):
76
+ trace_id = nr.current_trace_id()
77
+ if trace_id:
78
+ return str(trace_id)
79
+ except Exception:
80
+ pass
81
+
82
+ # 5. Elastic APM
83
+ try:
84
+ if 'elasticapm' in sys.modules:
85
+ elasticapm = sys.modules['elasticapm']
86
+ trace_id = elasticapm.get_trace_id()
87
+ if trace_id:
88
+ return str(trace_id)
89
+ except Exception:
90
+ pass
91
+
92
+ logger.info("[HyperProbe] trace id could not be extracted.")
93
+ return None
@@ -0,0 +1,7 @@
1
+ import sys
2
+ import os
3
+
4
+ # Inject current directory into sys.path temporarily so generated protobuf imports resolve correctly
5
+ current_dir = os.path.dirname(os.path.abspath(__file__))
6
+ if current_dir not in sys.path:
7
+ sys.path.insert(0, current_dir)