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.
- hyperprobe/__init__.py +7 -0
- hyperprobe/agent.py +499 -0
- hyperprobe/bootstrap.py +40 -0
- hyperprobe/core/__init__.py +1 -0
- hyperprobe/core/broker.py +131 -0
- hyperprobe/core/evaluator.py +119 -0
- hyperprobe/core/injection/__init__.py +0 -0
- hyperprobe/core/injection/sitecustomize.py +18 -0
- hyperprobe/core/logger.py +121 -0
- hyperprobe/core/monitoring_engine.py +788 -0
- hyperprobe/core/probe_output.py +177 -0
- hyperprobe/core/quota.py +73 -0
- hyperprobe/core/safety.py +136 -0
- hyperprobe/core/serializer.py +214 -0
- hyperprobe/core/trace_extractor.py +93 -0
- hyperprobe/protos/__init__.py +7 -0
- hyperprobe/protos/agent_pb2.py +57 -0
- hyperprobe/protos/agent_pb2_grpc.py +199 -0
- hyperprobe_agent-1.2.24.dist-info/METADATA +154 -0
- hyperprobe_agent-1.2.24.dist-info/RECORD +24 -0
- hyperprobe_agent-1.2.24.dist-info/WHEEL +5 -0
- hyperprobe_agent-1.2.24.dist-info/entry_points.txt +2 -0
- hyperprobe_agent-1.2.24.dist-info/licenses/LICENSE +13 -0
- hyperprobe_agent-1.2.24.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import ast
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from hyperprobe.core.logger import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger("hyperprobe:evaluator")
|
|
8
|
+
|
|
9
|
+
SAFE_BUILTINS = {
|
|
10
|
+
"len": len, "str": str, "int": int, "float": float,
|
|
11
|
+
"bool": bool, "list": list, "dict": dict, "set": set,
|
|
12
|
+
"max": max, "min": min, "abs": abs, "round": round,
|
|
13
|
+
"isinstance": isinstance, "type": type
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
COMPILED_CACHE_SIZE = 200
|
|
17
|
+
|
|
18
|
+
SAFE_METHODS = {
|
|
19
|
+
# Dictionary/List read-only methods
|
|
20
|
+
"keys", "values", "items", "count",
|
|
21
|
+
# String read-only methods
|
|
22
|
+
"lower", "upper",
|
|
23
|
+
"isdigit", "isalpha", "isnumeric", "isalnum",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class SafeASTVisitor(ast.NodeVisitor):
|
|
27
|
+
def visit_Call(self, node):
|
|
28
|
+
# 1. Is it a direct function call? e.g. len(items)
|
|
29
|
+
if isinstance(node.func, ast.Name):
|
|
30
|
+
if node.func.id not in SAFE_BUILTINS:
|
|
31
|
+
raise ValueError(f"Function call '{node.func.id}' is not allowed.")
|
|
32
|
+
|
|
33
|
+
# 2. Is it a method call on an object? e.g. user.get_id() or my_dict.keys()
|
|
34
|
+
elif isinstance(node.func, ast.Attribute):
|
|
35
|
+
method_name = node.func.attr
|
|
36
|
+
if method_name not in SAFE_METHODS:
|
|
37
|
+
raise ValueError(f"Method call '.{method_name}()' is strictly forbidden.")
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("Complex dynamic calls are strictly forbidden.")
|
|
41
|
+
|
|
42
|
+
self.generic_visit(node)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProbeEvaluator:
|
|
46
|
+
_placeholder_regex = re.compile(r'\$?\{([^}]+)\}')
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _get_or_compile_safe(cls, expr: str):
|
|
50
|
+
# Normalize the string to prevent duplicate cache entries
|
|
51
|
+
clean_expr = expr.strip()
|
|
52
|
+
|
|
53
|
+
return cls._compile_safe(clean_expr)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
@lru_cache(maxsize=COMPILED_CACHE_SIZE)
|
|
57
|
+
def _compile_safe(clean_expr: str):
|
|
58
|
+
# Cache misses are parsed and compiled once; least-recently-used
|
|
59
|
+
# expressions are evicted after the bounded cache reaches capacity.
|
|
60
|
+
if len(clean_expr) > 256:
|
|
61
|
+
raise ValueError("Expression is too long (max 256 characters).")
|
|
62
|
+
|
|
63
|
+
tree = ast.parse(clean_expr, mode='eval')
|
|
64
|
+
visitor = SafeASTVisitor()
|
|
65
|
+
visitor.visit(tree)
|
|
66
|
+
|
|
67
|
+
# Compile it to bytecode
|
|
68
|
+
compiled_code = compile(tree, filename="<string>", mode="eval")
|
|
69
|
+
|
|
70
|
+
return compiled_code
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def safe_eval(cls, expr: str, globals_dict: dict, locals_dict: dict):
|
|
74
|
+
# Create a shadow copy of globals, forcefully removing dangerous builtins
|
|
75
|
+
safe_globals = globals_dict.copy()
|
|
76
|
+
safe_globals["__builtins__"] = SAFE_BUILTINS
|
|
77
|
+
|
|
78
|
+
# Grab the pre-compiled bytecode instantly
|
|
79
|
+
compiled = cls._get_or_compile_safe(expr)
|
|
80
|
+
|
|
81
|
+
# Execute the bytecode
|
|
82
|
+
return eval(compiled, safe_globals, locals_dict)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def evaluate_log_template(cls, template: str, globals_dict: dict, locals_dict: dict) -> str:
|
|
86
|
+
"""Parses and safely formats a log template string."""
|
|
87
|
+
if not template:
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
result = []
|
|
91
|
+
last_idx = 0
|
|
92
|
+
for match in cls._placeholder_regex.finditer(template):
|
|
93
|
+
# Append plain text preceding placeholder
|
|
94
|
+
result.append(template[last_idx:match.start()])
|
|
95
|
+
expr = match.group(1).strip()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Safely execute expression
|
|
99
|
+
val = cls.safe_eval(expr, globals_dict, locals_dict)
|
|
100
|
+
result.append(str(val))
|
|
101
|
+
except Exception as e:
|
|
102
|
+
result.append(f"<Error: {type(e).__name__}: {str(e)}>")
|
|
103
|
+
last_idx = match.end()
|
|
104
|
+
|
|
105
|
+
result.append(template[last_idx:])
|
|
106
|
+
return "".join(result)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def evaluate_watches(cls, watch_expressions: list, globals_dict: dict, locals_dict: dict) -> dict:
|
|
110
|
+
"""Evaluates a set of independent watch expressions."""
|
|
111
|
+
results = {}
|
|
112
|
+
if not watch_expressions:
|
|
113
|
+
return results
|
|
114
|
+
for expr in watch_expressions:
|
|
115
|
+
try:
|
|
116
|
+
results[expr] = cls.safe_eval(expr, globals_dict, locals_dict)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
results[expr] = f"Error: {type(e).__name__}: {str(e)}"
|
|
119
|
+
return results
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# This file acts as an automatic injection point for Python.
|
|
5
|
+
# When a directory containing this file is injected into PYTHONPATH,
|
|
6
|
+
# Python will automatically execute it before the main application code starts.
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from hyperprobe.agent import HyperProbeAgent
|
|
10
|
+
|
|
11
|
+
HyperProbeAgent.start({
|
|
12
|
+
"broker_url": os.getenv("HYPERPROBE_BROKER_URL"),
|
|
13
|
+
"service_id": os.getenv("HYPERPROBE_SERVICE_ID"),
|
|
14
|
+
"environment": os.getenv("HYPERPROBE_ENVIRONMENT"),
|
|
15
|
+
"commit_sha": os.getenv("HYPERPROBE_COMMIT_SHA") or os.getenv("GIT_COMMIT")
|
|
16
|
+
})
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print(f"\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Agent failed to initialize ({type(e).__name__}: {e}). Running application uninstrumented.", file=sys.stderr)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Small, dependency-free namespace debugging for the HyperProbe SDK.
|
|
2
|
+
|
|
3
|
+
The ``DEBUG`` environment variable accepts comma- or whitespace-separated
|
|
4
|
+
namespace patterns. ``*`` matches any characters and a pattern prefixed with
|
|
5
|
+
``-`` excludes a namespace.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SELECTOR_SEPARATOR = re.compile(r"[\s,]+")
|
|
15
|
+
_COLOR_CODES = (31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96)
|
|
16
|
+
_DISABLED_COLOR_VALUES = {"0", "false", "no", "off"}
|
|
17
|
+
_logger_cache: dict[str, "HyperProbeLogger"] = {}
|
|
18
|
+
_logger_cache_lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compile_selector(pattern: str) -> re.Pattern[str]:
|
|
22
|
+
"""Compile a Node-style namespace selector where only ``*`` is special."""
|
|
23
|
+
return re.compile("^" + re.escape(pattern).replace(r"\*", ".*?") + "$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_selectors(value: str | None) -> tuple[list[re.Pattern[str]], list[re.Pattern[str]]]:
|
|
27
|
+
enabled: list[re.Pattern[str]] = []
|
|
28
|
+
skipped: list[re.Pattern[str]] = []
|
|
29
|
+
|
|
30
|
+
for selector in _SELECTOR_SEPARATOR.split(value or ""):
|
|
31
|
+
if not selector:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
is_skip = selector.startswith("-")
|
|
35
|
+
pattern = selector[1:] if is_skip else selector
|
|
36
|
+
if not pattern:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
(skipped if is_skip else enabled).append(_compile_selector(pattern))
|
|
40
|
+
|
|
41
|
+
return enabled, skipped
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _matches_selector(pattern: re.Pattern[str], namespace: str) -> bool:
|
|
45
|
+
return pattern.fullmatch(namespace) is not None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_namespace_enabled(namespace: str, debug_value: str | None = None) -> bool:
|
|
49
|
+
"""Return whether ``namespace`` is selected by ``debug_value`` or ``DEBUG``."""
|
|
50
|
+
enabled, skipped = _parse_selectors(
|
|
51
|
+
os.getenv("DEBUG") if debug_value is None else debug_value
|
|
52
|
+
)
|
|
53
|
+
return (
|
|
54
|
+
bool(enabled)
|
|
55
|
+
and not any(_matches_selector(pattern, namespace) for pattern in skipped)
|
|
56
|
+
and any(_matches_selector(pattern, namespace) for pattern in enabled)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _namespace_color(namespace: str) -> int:
|
|
61
|
+
# Avoid Python's randomized hash so a namespace keeps its color between runs.
|
|
62
|
+
value = sum((index + 1) * ord(character) for index, character in enumerate(namespace))
|
|
63
|
+
return _COLOR_CODES[value % len(_COLOR_CODES)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _colors_enabled(stream: object) -> bool:
|
|
67
|
+
configured = os.getenv("DEBUG_COLORS")
|
|
68
|
+
if configured is not None:
|
|
69
|
+
return configured.strip().lower() not in _DISABLED_COLOR_VALUES
|
|
70
|
+
return bool(getattr(stream, "isatty", lambda: False)())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HyperProbeLogger:
|
|
74
|
+
"""A namespace logger controlled by the ``DEBUG`` environment variable."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, namespace: str = "hyperprobe:agent"):
|
|
77
|
+
self.namespace = namespace
|
|
78
|
+
self.enabled = is_namespace_enabled(namespace)
|
|
79
|
+
self.color = _namespace_color(namespace)
|
|
80
|
+
|
|
81
|
+
def _prefix(self, stream: object) -> str:
|
|
82
|
+
if _colors_enabled(stream):
|
|
83
|
+
return f"\033[{self.color}m{self.namespace}\033[0m"
|
|
84
|
+
return self.namespace
|
|
85
|
+
|
|
86
|
+
def _write(self, message: str, stream: object) -> None:
|
|
87
|
+
print(f"{self._prefix(stream)} {message}", file=stream, flush=True)
|
|
88
|
+
|
|
89
|
+
def info(self, message: str) -> None:
|
|
90
|
+
if self.enabled:
|
|
91
|
+
self._write(message, sys.stdout)
|
|
92
|
+
|
|
93
|
+
def error(self, message: str) -> None:
|
|
94
|
+
if self.enabled:
|
|
95
|
+
self._write(message, sys.stderr)
|
|
96
|
+
|
|
97
|
+
def forceInfo(self, message: str) -> None:
|
|
98
|
+
"""Write an informational message without applying the ``DEBUG`` gate."""
|
|
99
|
+
print(message, file=sys.stdout, flush=True)
|
|
100
|
+
|
|
101
|
+
def forceError(self, message: str) -> None:
|
|
102
|
+
"""Write an error message without applying the ``DEBUG`` gate."""
|
|
103
|
+
print(message, file=sys.stderr, flush=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_logger(namespace: str) -> HyperProbeLogger:
|
|
107
|
+
"""Return the process-wide logger for a namespace.
|
|
108
|
+
|
|
109
|
+
``DEBUG`` is intentionally read when a namespace is first created, matching
|
|
110
|
+
the launch-time configuration behavior of the previous logger.
|
|
111
|
+
"""
|
|
112
|
+
with _logger_cache_lock:
|
|
113
|
+
logger = _logger_cache.get(namespace)
|
|
114
|
+
if logger is None:
|
|
115
|
+
logger = HyperProbeLogger(namespace)
|
|
116
|
+
_logger_cache[namespace] = logger
|
|
117
|
+
return logger
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Existing imports continue to work and now use the primary agent namespace.
|
|
121
|
+
logger = get_logger("hyperprobe:agent")
|