hyperprobe-agent 1.2.30b4__tar.gz → 1.2.31__tar.gz

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.
Files changed (56) hide show
  1. {hyperprobe_agent-1.2.30b4/hyperprobe_agent.egg-info → hyperprobe_agent-1.2.31}/PKG-INFO +81 -13
  2. hyperprobe_agent-1.2.30b4/PKG-INFO → hyperprobe_agent-1.2.31/README.md +67 -44
  3. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/agent.py +143 -27
  4. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/broker.py +4 -6
  5. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/__init__.py +1 -0
  6. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/controller.py +300 -0
  7. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/discovery.py +71 -0
  8. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/engine.py +674 -0
  9. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/imports.py +60 -0
  10. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/metric_processor.py +262 -0
  11. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/records.py +51 -0
  12. hyperprobe_agent-1.2.31/hyperprobe/core/bytecode/rewriting.py +102 -0
  13. hyperprobe_agent-1.2.31/hyperprobe/core/engine_base.py +165 -0
  14. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/evaluator.py +17 -11
  15. hyperprobe_agent-1.2.31/hyperprobe/core/locations.py +16 -0
  16. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/logger.py +15 -3
  17. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/monitoring_engine.py +68 -294
  18. hyperprobe_agent-1.2.31/hyperprobe/core/probe_processor.py +163 -0
  19. hyperprobe_agent-1.2.31/hyperprobe/core/redaction.py +117 -0
  20. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/serializer.py +6 -12
  21. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/protos/agent_pb2.py +2 -3
  22. hyperprobe_agent-1.2.30b4/README.md → hyperprobe_agent-1.2.31/hyperprobe_agent.egg-info/PKG-INFO +112 -9
  23. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe_agent.egg-info/SOURCES.txt +19 -0
  24. hyperprobe_agent-1.2.31/hyperprobe_agent.egg-info/requires.txt +19 -0
  25. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/setup.py +14 -4
  26. hyperprobe_agent-1.2.31/tests/test_agent_backend_integration.py +229 -0
  27. hyperprobe_agent-1.2.31/tests/test_bytecode_backend.py +397 -0
  28. hyperprobe_agent-1.2.31/tests/test_bytecode_lifecycle.py +400 -0
  29. hyperprobe_agent-1.2.31/tests/test_bytecode_token_lifecycle.py +312 -0
  30. hyperprobe_agent-1.2.31/tests/test_engine_selection.py +37 -0
  31. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_fork.py +5 -5
  32. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_logger.py +21 -0
  33. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_metric_probes.py +8 -2
  34. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_multithreading_integration.py +6 -2
  35. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_probe_output.py +28 -0
  36. hyperprobe_agent-1.2.31/tests/test_redaction.py +78 -0
  37. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_serializer.py +26 -0
  38. hyperprobe_agent-1.2.31/tests/test_shared_processor.py +220 -0
  39. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_startup.py +18 -0
  40. hyperprobe_agent-1.2.30b4/hyperprobe_agent.egg-info/requires.txt +0 -8
  41. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/LICENSE +0 -0
  42. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/__init__.py +0 -0
  43. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/__init__.py +0 -0
  44. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/probe_output.py +0 -0
  45. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/quota.py +0 -0
  46. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/safety.py +0 -0
  47. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/core/trace_extractor.py +0 -0
  48. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/protos/__init__.py +0 -0
  49. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe/protos/agent_pb2_grpc.py +0 -0
  50. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe_agent.egg-info/dependency_links.txt +0 -0
  51. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/hyperprobe_agent.egg-info/top_level.txt +0 -0
  52. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/setup.cfg +0 -0
  53. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_agent.py +0 -0
  54. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_evaluator.py +0 -0
  55. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_p1_regressions.py +0 -0
  56. {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.31}/tests/test_safety.py +0 -0
@@ -1,20 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperprobe-agent
3
- Version: 1.2.30b4
3
+ Version: 1.2.31
4
4
  Summary: Production-grade, non-breaking live debugger and telemetry agent for Python.
5
5
  Home-page: https://www.hyperprobe.co
6
6
  Author: Arif, Saksham, Karan
7
7
  Author-email: arif@hypertest.co, saksham@hypertest.co, karan@hypertest.co
8
8
  License: Hyperprobe Proprietary License
9
9
  Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.7
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
10
15
  Classifier: Programming Language :: Python :: 3.12
11
16
  Classifier: Programming Language :: Python :: 3.13
12
17
  Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.12
18
+ Requires-Python: >=3.7
14
19
  Description-Content-Type: text/markdown
15
20
  License-File: LICENSE
16
- Requires-Dist: protobuf<6.0.0,>=4.25.3
17
- Requires-Dist: grpcio>=1.62.2
21
+ Requires-Dist: protobuf<4.25,>=4.24.4; python_version < "3.8"
22
+ Requires-Dist: protobuf<6.0.0,>=4.25.3; python_version >= "3.8"
23
+ Requires-Dist: grpcio<1.63,>=1.62.2; python_version < "3.8"
24
+ Requires-Dist: grpcio>=1.62.2; python_version >= "3.8"
25
+ Requires-Dist: importlib-metadata<7,>=6; python_version < "3.8"
26
+ Requires-Dist: bytecode==0.13.0; python_version < "3.8"
27
+ Requires-Dist: bytecode==0.16.2; python_version >= "3.8" and python_version < "3.12"
18
28
  Provides-Extra: dev
19
29
  Requires-Dist: grpcio>=1.50.0; extra == "dev"
20
30
  Requires-Dist: grpcio-tools>=1.50.0; extra == "dev"
@@ -37,17 +47,64 @@ Dynamic: summary
37
47
 
38
48
  Production-grade, non-breaking live debugger and telemetry agent for Python.
39
49
 
40
- HyperProbe allows you to debug running Python applications in real-time without breaking them, restarting them, or altering production traffic. It leverages modern Python's fast, low-overhead monitoring APIs (`sys.monitoring`) to securely and dynamically extract stack frames, local variables, and logs at targeted lines.
50
+ HyperProbe captures stack frames, local variables, and logs at selected source
51
+ lines without stopping the application in a debugger. At agent initialization,
52
+ Python 3.12+ selects the `sys.monitoring` backend. CPython 3.7–3.11 selects the
53
+ bytecode backend. Both support snapshot, log, counter, metric, and duration
54
+ probes and use the same broker, evaluator, serializer, quotas, safety monitor,
55
+ and output pipeline.
56
+ Only the selected backend is imported; `bytecode` is not a dependency on 3.12+.
41
57
 
42
58
  ## Features
43
59
 
44
60
  - **Non-Breaking Snapshot Probes:** Capture local variables and stack traces dynamically at any line of code.
45
61
  - **Dynamic Log Probes:** Inject live log templates to print formatted output without redeploying code.
46
62
  - **Counter Probes:** Emit a value of `1` whenever a configured source line is reached.
47
- - **Metric Probes:** Safely evaluate a numerical Python expression and emit its value.
63
+ - **Metric Probes:** Evaluate a numerical Python expression and emit its value.
48
64
  - **Duration Probes:** Measure elapsed monotonic time in milliseconds between two source lines, with optional request correlation.
49
65
  - **Circular Reference & Deep Object Protection:** Memory-safe serialization of deeply nested or cyclic variables.
50
- - **High-Performance Safety Guard:** Automatically pauses monitoring and goes idle if CPU/memory boundaries or pause budgets are exceeded.
66
+ - **Safety Guard:** Suspends instrumentation when scheduler lag or the capture pause budget crosses its configured threshold.
67
+
68
+ ## Bytecode backend (CPython 3.7–3.11)
69
+
70
+ The broker still supplies `runtime_location` and `runtime_line`. The backend
71
+ uses the existing path resolver, locates functions through their loaded modules,
72
+ and replaces only the selected functions' code objects. It does not use
73
+ `sys.settrace`, `threading.settrace`, or a heap-wide `gc.get_objects()` scan.
74
+ Pending probes can watch standard file imports; the normal loader executes the
75
+ module after the SDK prepares its code. Loaded factories can register and
76
+ instrument nested functions they create later. Conditions, watches, redaction,
77
+ trace IDs, hit limits, and telemetry use the shared services. Duration probes
78
+ index both source locations, including an end line in a later-imported module.
79
+
80
+ This backend also selects its own agent lifecycle controller at startup; the
81
+ Python 3.12+ lifecycle and `sys.monitoring` implementation remain separate.
82
+ Broker responses are compared against stable local definition records. An
83
+ unchanged response (including reordered probes) does no module discovery,
84
+ function planning, or bytecode rewriting. Condition, watch, and template changes
85
+ replace evaluation state without rebuilding bytecode. Location changes discover
86
+ the new target and replan affected functions; removal uses tracked functions.
87
+ Callback-site tokens are held weakly, so short-lived nested functions do not
88
+ accumulate permanent lookup entries.
89
+ Broker-confirmed completed probes stay disabled until the broker removes them.
90
+
91
+ On RED, the backend closes its capture gate and synchronously restores the
92
+ saved original code for functions it still owns, including factories. Pending
93
+ import work cannot install new probes while suspended. Restoration runs before
94
+ agent bookkeeping or logging, and recovery builds bytecode without holding the
95
+ agent lock. Capture delivery checks the original definition identity and
96
+ activation generation before queue admission, so an old capture cannot be
97
+ accepted under an updated definition or after RED/recovery. A frame already running
98
+ keeps its existing code, but its callbacks become no-ops. An expression already
99
+ running cannot be forcibly interrupted safely. Instrumentation failures are
100
+ reported through bounded engine diagnostics and best-effort error logging;
101
+ normal exceptions from the application's own function or module still propagate.
102
+
103
+ Discovery supports module/class namespaces and bounded built-in containers.
104
+ Arbitrary generated code, custom loaders, hidden pre-existing closures, and
105
+ automatic reload coverage are not guaranteed. Function prologues are not probe
106
+ locations. Performance, including a 2% overhead target, requires workload-based
107
+ measurement; the shared evaluator is not a security sandbox.
51
108
 
52
109
  ## Metric-family probes
53
110
 
@@ -59,9 +116,9 @@ Counter, metric, and duration probes are created through HyperProbe in the same
59
116
  | Metric | `metric_name`, `metric_expression`, and one runtime location | The finite numerical expression result |
60
117
  | Duration | `metric_name`, primary and secondary runtime locations, and optional `correlation_expression` | Monotonic elapsed milliseconds |
61
118
 
62
- Conditions are evaluated at every configured location. Metric expressions use the same restricted, read-only Python evaluator as conditions and watches. Numerical strings follow Node SDK `parseFloat` behavior for compatibility; invalid or non-finite values are exported with `metric_value` set to `0` and a populated `capture_error`.
119
+ Conditions are evaluated at every configured location. Metric expressions use the same restricted Python evaluator as conditions and watches. Numerical strings follow Node SDK `parseFloat` prefix behavior for compatibility; invalid or non-finite values are exported with `metric_value` set to `0` and a populated `capture_error`. The bytecode backend examines at most the first 1,024 characters of a metric string.
63
120
 
64
- Duration correlation expressions must evaluate to a string or number. When omitted or when they evaluate to `None`, a singleton correlation key is used. Unmatched duration ends are ignored, and pending starts expire after 60 seconds.
121
+ Duration correlation expressions must evaluate to a string or number. When omitted or when they evaluate to `None`, a singleton correlation key is used. Unmatched duration ends are ignored, and pending starts expire after 60 seconds. On CPython 3.7–3.11, correlation keys are limited to 256 characters and at most 4,096 starts are kept per process; the oldest start is evicted if that limit is reached. Duration starts never consume a probe hit. A matching end consumes the hit when its event is admitted to the telemetry queue.
65
122
 
66
123
  ## Installation
67
124
 
@@ -106,7 +163,7 @@ Configure the agent using the following environment variables:
106
163
  | `HYPERPROBE_SYNC_INTERVAL_MS` | Rate at which the agent syncs probe definitions from the broker. | `60000` (1 min) |
107
164
  | `HYPERPROBE_FLUSH_INTERVAL_MS` | Delay before sending telemetry events to the broker. | `1000` (1 sec) |
108
165
  | `HYPERPROBE_MAX_QUEUE_SIZE` | Maximum number of telemetry events held before flush. | `100` |
109
- | `HYPERPROBE_HITS_PER_SEC` | Max snapshot capture requests per second (quota). | `10` |
166
+ | `HYPERPROBE_HITS_PER_SEC` | Max quota-charged probe evaluations per second. | `10` |
110
167
  | `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Max bandwidth limit for telemetry transmissions (quota). | `1024` (1 MB) |
111
168
  | `HYPERPROBE_RPC_TIMEOUT_SEC` | Deadline for broker RPCs. | `10` |
112
169
  | `HYPERPROBE_COOLDOWN_SEC` | Tracing suspension duration after a RED safety state. | `10` |
@@ -128,9 +185,20 @@ startup while the host application continues normally.
128
185
 
129
186
  ### Safe Evaluation & Security Considerations
130
187
 
131
- By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`). In safe mode:
132
- - Method and function calls are strictly restricted to a narrow allowlist of side-effect free standard operations.
133
- - State mutations, assignments, and arbitrary function invocations (such as database calls, network I/O, file operations, or state modifications) are prevented.
188
+ Captured strings are checked against configured value-redaction patterns before
189
+ truncation. When value rules are configured, a string longer than 16,384
190
+ characters is masked rather than scanned partially. Log substitutions use the
191
+ bounded serializer, so nested sensitive keys are masked before an object becomes
192
+ log text. Telemetry errors carry categories rather than raw runtime values or
193
+ exception messages. These rules do not infer the origin of an arbitrary watch
194
+ result: a scalar watch expression such as `password` can still expose its value
195
+ when no value pattern matches it.
196
+
197
+ By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`).
198
+ The existing evaluator checks function and method names against an allowlist.
199
+ This reduces available operations but is not a sandbox: properties, overloaded
200
+ operators, or allowed method names on custom objects can still execute
201
+ application code. There is no hard timeout that can interrupt such code safely.
134
202
 
135
203
  **Disabling Safe Evaluation (`HYPERPROBE_DISABLE_SAFE_EVALUATION=true`):**
136
204
  > ⚠️ **Security Warning**: Setting `HYPERPROBE_DISABLE_SAFE_EVALUATION=true` allows conditions, watch expressions, and log templates to execute arbitrary functions and methods in the host application process. This may lead to unintended side effects, state mutations, performance degradation, or security risks if expressions invoke mutating or blocking operations. Only disable safe evaluation in trusted environments where dynamic method evaluation is strictly required.
@@ -1,53 +1,65 @@
1
- Metadata-Version: 2.4
2
- Name: hyperprobe-agent
3
- Version: 1.2.30b4
4
- Summary: Production-grade, non-breaking live debugger and telemetry agent for Python.
5
- Home-page: https://www.hyperprobe.co
6
- Author: Arif, Saksham, Karan
7
- Author-email: arif@hypertest.co, saksham@hypertest.co, karan@hypertest.co
8
- License: Hyperprobe Proprietary License
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.12
11
- Classifier: Programming Language :: Python :: 3.13
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.12
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: protobuf<6.0.0,>=4.25.3
17
- Requires-Dist: grpcio>=1.62.2
18
- Provides-Extra: dev
19
- Requires-Dist: grpcio>=1.50.0; extra == "dev"
20
- Requires-Dist: grpcio-tools>=1.50.0; extra == "dev"
21
- Requires-Dist: protobuf>=4.21.0; extra == "dev"
22
- Requires-Dist: pytest>=7.0.0; extra == "dev"
23
- Dynamic: author
24
- Dynamic: author-email
25
- Dynamic: classifier
26
- Dynamic: description
27
- Dynamic: description-content-type
28
- Dynamic: home-page
29
- Dynamic: license
30
- Dynamic: license-file
31
- Dynamic: provides-extra
32
- Dynamic: requires-dist
33
- Dynamic: requires-python
34
- Dynamic: summary
35
-
36
1
  # HyperProbe Python Agent
37
2
 
38
3
  Production-grade, non-breaking live debugger and telemetry agent for Python.
39
4
 
40
- HyperProbe allows you to debug running Python applications in real-time without breaking them, restarting them, or altering production traffic. It leverages modern Python's fast, low-overhead monitoring APIs (`sys.monitoring`) to securely and dynamically extract stack frames, local variables, and logs at targeted lines.
5
+ HyperProbe captures stack frames, local variables, and logs at selected source
6
+ lines without stopping the application in a debugger. At agent initialization,
7
+ Python 3.12+ selects the `sys.monitoring` backend. CPython 3.7–3.11 selects the
8
+ bytecode backend. Both support snapshot, log, counter, metric, and duration
9
+ probes and use the same broker, evaluator, serializer, quotas, safety monitor,
10
+ and output pipeline.
11
+ Only the selected backend is imported; `bytecode` is not a dependency on 3.12+.
41
12
 
42
13
  ## Features
43
14
 
44
15
  - **Non-Breaking Snapshot Probes:** Capture local variables and stack traces dynamically at any line of code.
45
16
  - **Dynamic Log Probes:** Inject live log templates to print formatted output without redeploying code.
46
17
  - **Counter Probes:** Emit a value of `1` whenever a configured source line is reached.
47
- - **Metric Probes:** Safely evaluate a numerical Python expression and emit its value.
18
+ - **Metric Probes:** Evaluate a numerical Python expression and emit its value.
48
19
  - **Duration Probes:** Measure elapsed monotonic time in milliseconds between two source lines, with optional request correlation.
49
20
  - **Circular Reference & Deep Object Protection:** Memory-safe serialization of deeply nested or cyclic variables.
50
- - **High-Performance Safety Guard:** Automatically pauses monitoring and goes idle if CPU/memory boundaries or pause budgets are exceeded.
21
+ - **Safety Guard:** Suspends instrumentation when scheduler lag or the capture pause budget crosses its configured threshold.
22
+
23
+ ## Bytecode backend (CPython 3.7–3.11)
24
+
25
+ The broker still supplies `runtime_location` and `runtime_line`. The backend
26
+ uses the existing path resolver, locates functions through their loaded modules,
27
+ and replaces only the selected functions' code objects. It does not use
28
+ `sys.settrace`, `threading.settrace`, or a heap-wide `gc.get_objects()` scan.
29
+ Pending probes can watch standard file imports; the normal loader executes the
30
+ module after the SDK prepares its code. Loaded factories can register and
31
+ instrument nested functions they create later. Conditions, watches, redaction,
32
+ trace IDs, hit limits, and telemetry use the shared services. Duration probes
33
+ index both source locations, including an end line in a later-imported module.
34
+
35
+ This backend also selects its own agent lifecycle controller at startup; the
36
+ Python 3.12+ lifecycle and `sys.monitoring` implementation remain separate.
37
+ Broker responses are compared against stable local definition records. An
38
+ unchanged response (including reordered probes) does no module discovery,
39
+ function planning, or bytecode rewriting. Condition, watch, and template changes
40
+ replace evaluation state without rebuilding bytecode. Location changes discover
41
+ the new target and replan affected functions; removal uses tracked functions.
42
+ Callback-site tokens are held weakly, so short-lived nested functions do not
43
+ accumulate permanent lookup entries.
44
+ Broker-confirmed completed probes stay disabled until the broker removes them.
45
+
46
+ On RED, the backend closes its capture gate and synchronously restores the
47
+ saved original code for functions it still owns, including factories. Pending
48
+ import work cannot install new probes while suspended. Restoration runs before
49
+ agent bookkeeping or logging, and recovery builds bytecode without holding the
50
+ agent lock. Capture delivery checks the original definition identity and
51
+ activation generation before queue admission, so an old capture cannot be
52
+ accepted under an updated definition or after RED/recovery. A frame already running
53
+ keeps its existing code, but its callbacks become no-ops. An expression already
54
+ running cannot be forcibly interrupted safely. Instrumentation failures are
55
+ reported through bounded engine diagnostics and best-effort error logging;
56
+ normal exceptions from the application's own function or module still propagate.
57
+
58
+ Discovery supports module/class namespaces and bounded built-in containers.
59
+ Arbitrary generated code, custom loaders, hidden pre-existing closures, and
60
+ automatic reload coverage are not guaranteed. Function prologues are not probe
61
+ locations. Performance, including a 2% overhead target, requires workload-based
62
+ measurement; the shared evaluator is not a security sandbox.
51
63
 
52
64
  ## Metric-family probes
53
65
 
@@ -59,9 +71,9 @@ Counter, metric, and duration probes are created through HyperProbe in the same
59
71
  | Metric | `metric_name`, `metric_expression`, and one runtime location | The finite numerical expression result |
60
72
  | Duration | `metric_name`, primary and secondary runtime locations, and optional `correlation_expression` | Monotonic elapsed milliseconds |
61
73
 
62
- Conditions are evaluated at every configured location. Metric expressions use the same restricted, read-only Python evaluator as conditions and watches. Numerical strings follow Node SDK `parseFloat` behavior for compatibility; invalid or non-finite values are exported with `metric_value` set to `0` and a populated `capture_error`.
74
+ Conditions are evaluated at every configured location. Metric expressions use the same restricted Python evaluator as conditions and watches. Numerical strings follow Node SDK `parseFloat` prefix behavior for compatibility; invalid or non-finite values are exported with `metric_value` set to `0` and a populated `capture_error`. The bytecode backend examines at most the first 1,024 characters of a metric string.
63
75
 
64
- Duration correlation expressions must evaluate to a string or number. When omitted or when they evaluate to `None`, a singleton correlation key is used. Unmatched duration ends are ignored, and pending starts expire after 60 seconds.
76
+ Duration correlation expressions must evaluate to a string or number. When omitted or when they evaluate to `None`, a singleton correlation key is used. Unmatched duration ends are ignored, and pending starts expire after 60 seconds. On CPython 3.7–3.11, correlation keys are limited to 256 characters and at most 4,096 starts are kept per process; the oldest start is evicted if that limit is reached. Duration starts never consume a probe hit. A matching end consumes the hit when its event is admitted to the telemetry queue.
65
77
 
66
78
  ## Installation
67
79
 
@@ -106,7 +118,7 @@ Configure the agent using the following environment variables:
106
118
  | `HYPERPROBE_SYNC_INTERVAL_MS` | Rate at which the agent syncs probe definitions from the broker. | `60000` (1 min) |
107
119
  | `HYPERPROBE_FLUSH_INTERVAL_MS` | Delay before sending telemetry events to the broker. | `1000` (1 sec) |
108
120
  | `HYPERPROBE_MAX_QUEUE_SIZE` | Maximum number of telemetry events held before flush. | `100` |
109
- | `HYPERPROBE_HITS_PER_SEC` | Max snapshot capture requests per second (quota). | `10` |
121
+ | `HYPERPROBE_HITS_PER_SEC` | Max quota-charged probe evaluations per second. | `10` |
110
122
  | `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Max bandwidth limit for telemetry transmissions (quota). | `1024` (1 MB) |
111
123
  | `HYPERPROBE_RPC_TIMEOUT_SEC` | Deadline for broker RPCs. | `10` |
112
124
  | `HYPERPROBE_COOLDOWN_SEC` | Tracing suspension duration after a RED safety state. | `10` |
@@ -128,9 +140,20 @@ startup while the host application continues normally.
128
140
 
129
141
  ### Safe Evaluation & Security Considerations
130
142
 
131
- By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`). In safe mode:
132
- - Method and function calls are strictly restricted to a narrow allowlist of side-effect free standard operations.
133
- - State mutations, assignments, and arbitrary function invocations (such as database calls, network I/O, file operations, or state modifications) are prevented.
143
+ Captured strings are checked against configured value-redaction patterns before
144
+ truncation. When value rules are configured, a string longer than 16,384
145
+ characters is masked rather than scanned partially. Log substitutions use the
146
+ bounded serializer, so nested sensitive keys are masked before an object becomes
147
+ log text. Telemetry errors carry categories rather than raw runtime values or
148
+ exception messages. These rules do not infer the origin of an arbitrary watch
149
+ result: a scalar watch expression such as `password` can still expose its value
150
+ when no value pattern matches it.
151
+
152
+ By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`).
153
+ The existing evaluator checks function and method names against an allowlist.
154
+ This reduces available operations but is not a sandbox: properties, overloaded
155
+ operators, or allowed method names on custom objects can still execute
156
+ application code. There is no hard timeout that can interrupt such code safely.
134
157
 
135
158
  **Disabling Safe Evaluation (`HYPERPROBE_DISABLE_SAFE_EVALUATION=true`):**
136
159
  > ⚠️ **Security Warning**: Setting `HYPERPROBE_DISABLE_SAFE_EVALUATION=true` allows conditions, watch expressions, and log templates to execute arbitrary functions and methods in the host application process. This may lead to unintended side effects, state mutations, performance degradation, or security risks if expressions invoke mutating or blocking operations. Only disable safe evaluation in trusted environments where dynamic method evaluation is strictly required.
@@ -5,6 +5,7 @@ import time
5
5
  import threading
6
6
  import queue
7
7
  import re
8
+ import math
8
9
  from urllib.parse import urlparse
9
10
 
10
11
  from hyperprobe.core.quota import QuotaManager
@@ -28,6 +29,48 @@ class _TelemetryItem:
28
29
  self.bandwidth_reservation = bandwidth_reservation
29
30
 
30
31
 
32
+ def _numeric_option(
33
+ options, option_name, env_name, default, *, integer=False,
34
+ allow_zero=False, scale=1.0, max_value=None,
35
+ ):
36
+ """Validate an option or environment value before allocating SDK resources."""
37
+ value = options.get(option_name)
38
+ if value is None:
39
+ value = os.getenv(env_name, default)
40
+
41
+ requirement = "a positive integer" if integer else (
42
+ "a non-negative finite number" if allow_zero else "a positive finite number"
43
+ )
44
+ if max_value is not None:
45
+ requirement += " within the platform's timeout range"
46
+
47
+ try:
48
+ if isinstance(value, bool):
49
+ raise ValueError("Boolean configuration is not numeric")
50
+ if integer:
51
+ if not isinstance(value, (int, str)):
52
+ raise ValueError("Integer configuration must not be fractional")
53
+ parsed = int(value)
54
+ else:
55
+ parsed = float(value)
56
+ if not math.isfinite(parsed):
57
+ raise ValueError("Configuration must be finite")
58
+ parsed *= scale
59
+ if not math.isfinite(parsed):
60
+ raise ValueError("Converted configuration must be finite")
61
+ if parsed < 0 or (not allow_zero and parsed == 0):
62
+ raise ValueError("Configuration is outside its allowed range")
63
+ if max_value is not None and parsed > max_value:
64
+ raise ValueError("Configuration exceeds the platform's timeout range")
65
+ return parsed
66
+ except (TypeError, ValueError, OverflowError):
67
+ message = "[HyperProbe] Invalid configuration: %s (%s) must be %s." % (
68
+ option_name, env_name, requirement,
69
+ )
70
+ logger.forceError(message)
71
+ raise ValueError("Invalid numeric configuration: " + option_name) from None
72
+
73
+
31
74
  def is_valid_broker_url(url):
32
75
  if not url or not str(url).strip():
33
76
  return False
@@ -66,17 +109,39 @@ class HyperProbeAgent:
66
109
  if self.fork_mode not in {"none", "worker"}:
67
110
  raise ValueError('fork_mode must be either "none" or "worker"')
68
111
 
69
- # Options & Envs Parsing
70
- self.sync_interval_sec = (options.get("sync_interval_ms") or int(os.getenv("HYPERPROBE_SYNC_INTERVAL_MS", 60000))) / 1000.0
71
- self.flush_interval_sec = (options.get("flush_interval_ms") or int(os.getenv("HYPERPROBE_FLUSH_INTERVAL_MS", 1000))) / 1000.0
72
- self.max_queue_size = options.get("max_queue_size") or int(os.getenv("HYPERPROBE_MAX_QUEUE_SIZE", 100))
73
- self._cooldown_sec = options.get("hyperprobe_cooldown_sec") or int(os.getenv("HYPERPROBE_COOLDOWN_SEC", 10))
74
-
75
- hits_per_sec = options.get("hits_per_sec") or int(os.getenv("HYPERPROBE_HITS_PER_SEC", 10))
76
- bandwidth_kb = options.get("bandwidth_kb_per_sec") or int(os.getenv("HYPERPROBE_BANDWIDTH_KB_PER_SEC", 1024))
77
- rpc_timeout_sec = options.get("hyperprobe_rpc_timeout_sec") or float(os.getenv("HYPERPROBE_RPC_TIMEOUT_SEC", 10.0))
78
- max_lag_ms = options.get("hyperprobe_max_lag_ms") or float(os.getenv("HYPERPROBE_MAX_LAG_MS", 50.0))
79
- pause_budget_ms = options.get("hyperprobe_pause_budget_ms") or float(os.getenv("HYPERPROBE_PAUSE_BUDGET_MS", 15.0))
112
+ # Validate operational settings before creating process resources.
113
+ self.sync_interval_sec = _numeric_option(
114
+ options, "sync_interval_ms", "HYPERPROBE_SYNC_INTERVAL_MS", 60000,
115
+ scale=0.001, max_value=threading.TIMEOUT_MAX,
116
+ )
117
+ self.flush_interval_sec = _numeric_option(
118
+ options, "flush_interval_ms", "HYPERPROBE_FLUSH_INTERVAL_MS", 1000,
119
+ scale=0.001, max_value=threading.TIMEOUT_MAX,
120
+ )
121
+ self.max_queue_size = _numeric_option(
122
+ options, "max_queue_size", "HYPERPROBE_MAX_QUEUE_SIZE", 100,
123
+ integer=True,
124
+ )
125
+ self._cooldown_sec = _numeric_option(
126
+ options, "hyperprobe_cooldown_sec", "HYPERPROBE_COOLDOWN_SEC", 10,
127
+ allow_zero=True, max_value=threading.TIMEOUT_MAX,
128
+ )
129
+ hits_per_sec = _numeric_option(
130
+ options, "hits_per_sec", "HYPERPROBE_HITS_PER_SEC", 10,
131
+ )
132
+ bandwidth_bytes = _numeric_option(
133
+ options, "bandwidth_kb_per_sec", "HYPERPROBE_BANDWIDTH_KB_PER_SEC", 1024,
134
+ scale=1024.0,
135
+ )
136
+ rpc_timeout_sec = _numeric_option(
137
+ options, "hyperprobe_rpc_timeout_sec", "HYPERPROBE_RPC_TIMEOUT_SEC", 10.0,
138
+ )
139
+ max_lag_ms = _numeric_option(
140
+ options, "hyperprobe_max_lag_ms", "HYPERPROBE_MAX_LAG_MS", 50.0,
141
+ )
142
+ pause_budget_ms = _numeric_option(
143
+ options, "hyperprobe_pause_budget_ms", "HYPERPROBE_PAUSE_BUDGET_MS", 15.0,
144
+ )
80
145
 
81
146
  opt_disable_safe_eval = options.get("disable_safe_evaluation", False)
82
147
  disable_safe_eval = (
@@ -110,7 +175,7 @@ class HyperProbeAgent:
110
175
  self.global_config["redact_values"] = [v.strip() for v in self.global_config["redact_values"] if v.strip()]
111
176
 
112
177
  # Initialize core telemetry components
113
- self.quota_manager = QuotaManager(hits_per_sec, bandwidth_kb * 1024)
178
+ self.quota_manager = QuotaManager(hits_per_sec, bandwidth_bytes)
114
179
  self.safety_monitor = SafetyMonitor(
115
180
  self._handle_health_change,
116
181
  max_lag_ms=max_lag_ms,
@@ -140,7 +205,9 @@ class HyperProbeAgent:
140
205
 
141
206
  # Determine and initialize dynamic instrumentation engine
142
207
  self.engine = self._init_instrumentation_engine()
143
- self.engine.set_global_config(self.global_config)
208
+ self.supported_probe_types = self.engine.supported_probe_types
209
+ if not self.engine.set_global_config(self.global_config):
210
+ raise ValueError("Initial capture configuration could not be applied")
144
211
 
145
212
  # Background worker threads
146
213
  self.sync_thread = None
@@ -150,7 +217,11 @@ class HyperProbeAgent:
150
217
  @classmethod
151
218
  def start(cls, options):
152
219
  """Starts the global HyperProbe Agent singleton."""
153
- options = dict(options or {})
220
+ try:
221
+ options = dict(options or {})
222
+ except Exception:
223
+ logger.forceError("[HyperProbe] Agent options must be a mapping. Running application uninstrumented.")
224
+ return None
154
225
  with cls._lock:
155
226
  if (
156
227
  cls._fork_owner_pid == os.getpid()
@@ -241,7 +312,7 @@ class HyperProbeAgent:
241
312
  cls._instance = None
242
313
  logger.forceError(
243
314
  f"[HyperProbe] Agent failed to initialize "
244
- f"({type(exc).__name__}: {exc}). "
315
+ f"({type(exc).__name__}). "
245
316
  "Running application uninstrumented."
246
317
  )
247
318
  return None
@@ -335,7 +406,7 @@ class HyperProbeAgent:
335
406
  instance._cleanup_failed_start()
336
407
  cls._instance = None
337
408
  logger.forceError(
338
- f"[HyperProbe] Failed to restart the agent after fork: {exc}"
409
+ f"[HyperProbe] Failed to restart the agent after fork ({type(exc).__name__})."
339
410
  )
340
411
  return
341
412
 
@@ -360,7 +431,7 @@ class HyperProbeAgent:
360
431
  cls._instance = None
361
432
  if cls._fork_owner_pid == os.getpid():
362
433
  cls._clear_fork_state_locked()
363
- print("[HyperProbe] Python agent successfully shut down.")
434
+ logger.forceInfo("[HyperProbe] Python agent successfully shut down.")
364
435
 
365
436
  @classmethod
366
437
  def _clear_fork_state_locked(cls):
@@ -370,8 +441,20 @@ class HyperProbeAgent:
370
441
  cls._fork_pending = False
371
442
 
372
443
  def _init_instrumentation_engine(self):
373
- from hyperprobe.core.monitoring_engine import MonitoringEngine
374
- return MonitoringEngine(
444
+ if sys.version_info >= (3, 12):
445
+ from hyperprobe.core.monitoring_engine import MonitoringEngine
446
+ engine_class = MonitoringEngine
447
+ elif sys.version_info >= (3, 7):
448
+ from hyperprobe.core.bytecode.controller import BytecodeController
449
+ from hyperprobe.core.bytecode.engine import BytecodeEngine
450
+ # Select lifecycle handlers once. Python 3.12+ keeps the existing
451
+ # monitoring handlers, without runtime branches on captures/polls.
452
+ self._bytecode_controller = BytecodeController(self)
453
+ self._bytecode_controller.bind()
454
+ engine_class = BytecodeEngine
455
+ else:
456
+ raise RuntimeError("HyperProbe requires Python 3.7 or newer")
457
+ return engine_class(
375
458
  self.quota_manager,
376
459
  self.safety_monitor,
377
460
  self._handle_capture,
@@ -531,7 +614,7 @@ class HyperProbeAgent:
531
614
  # Only activate probe types implemented by this SDK.
532
615
  server_probes = {}
533
616
  for probe in response.probes:
534
- if probe.type in SUPPORTED_PROBE_TYPES:
617
+ if probe.type in getattr(self, "supported_probe_types", SUPPORTED_PROBE_TYPES):
535
618
  server_probes[probe.id] = probe
536
619
  else:
537
620
  msg = f"[HyperProbe] Ignoring unsupported probe type {probe.type} for probe {probe.id}."
@@ -582,8 +665,8 @@ class HyperProbeAgent:
582
665
  batch = self._inflight_batch
583
666
  if batch is None:
584
667
  batch = []
585
- # Pull all currently queued events up to capacity. Keep the batch
586
- # in-flight until the broker call succeeds so retries cannot lose it.
668
+ # Concurrent producers can keep this drain going beyond queue capacity.
669
+ # Keep the batch in flight until the broker confirms delivery.
587
670
  while not self.telemetry_queue.empty():
588
671
  try:
589
672
  item = self.telemetry_queue.get_nowait()
@@ -670,18 +753,37 @@ class HyperProbeAgent:
670
753
  return list(self.active_probes.values())
671
754
 
672
755
  def _handle_capture(self, event):
756
+ try:
757
+ return self._handle_capture_impl(event)
758
+ except BaseException as error:
759
+ # This callback runs inside an application frame via either backend.
760
+ # Even a broken diagnostic stream must not let SDK errors escape.
761
+ try:
762
+ report = getattr(self.engine, "_record_failure", None)
763
+ if report is not None:
764
+ report("capture admission", error)
765
+ return
766
+ logger.forceError(
767
+ "[HyperProbe] Capture admission failed (%s)." % type(error).__name__
768
+ )
769
+ except BaseException:
770
+ pass
771
+
772
+ def _handle_capture_impl(self, event):
673
773
  if (
674
774
  getattr(self, "owner_pid", os.getpid()) != os.getpid()
675
775
  or getattr(self, "is_shutdown", False)
776
+ or getattr(self.engine, "is_suspended", False)
676
777
  ):
677
778
  return
779
+ generation = getattr(self.engine, "_generation", None)
678
780
  probe_id = event["probe_id"]
679
781
  with self._agent_lock:
680
782
  probe = self.active_probes.get(probe_id)
681
783
  if not probe:
682
784
  return
683
785
 
684
- if probe.type not in SUPPORTED_PROBE_TYPES:
786
+ if probe.type not in getattr(self, "supported_probe_types", SUPPORTED_PROBE_TYPES):
685
787
  return
686
788
 
687
789
  if not self._reserve_hit_slot(probe_id, probe):
@@ -697,7 +799,11 @@ class HyperProbeAgent:
697
799
  self._release_hit_slot(probe_id)
698
800
  return
699
801
 
700
- bandwidth_reservation = self.quota_manager.reserve_bandwidth(event_size)
802
+ try:
803
+ bandwidth_reservation = self.quota_manager.reserve_bandwidth(event_size)
804
+ except BaseException:
805
+ self._release_hit_slot(probe_id)
806
+ raise
701
807
  if bandwidth_reservation is None:
702
808
  msg = f"[HyperProbe] Bandwidth quota exceeded; dropping event for probe {probe_id} , event size was : {event_size} bytes."
703
809
  logger.error(msg)
@@ -706,14 +812,24 @@ class HyperProbeAgent:
706
812
 
707
813
  # Hold the bandwidth reservation with the event until the broker
708
814
  # confirms the batch. Release it if local queue admission fails.
709
- item = _TelemetryItem(event, bandwidth_reservation)
710
815
  try:
711
- self.telemetry_queue.put_nowait(item)
816
+ item = _TelemetryItem(event, bandwidth_reservation)
817
+ with self._agent_lock:
818
+ if (
819
+ getattr(self, "is_shutdown", False)
820
+ or getattr(self.engine, "is_suspended", False)
821
+ or generation != getattr(self.engine, "_generation", None)
822
+ or self.active_probes.get(probe_id) is not probe
823
+ ):
824
+ bandwidth_reservation.release()
825
+ self._release_hit_slot(probe_id)
826
+ return
827
+ self.telemetry_queue.put_nowait(item)
712
828
  except queue.Full:
713
829
  bandwidth_reservation.release()
714
830
  self._release_hit_slot(probe_id)
715
831
  return
716
- except Exception as exc:
832
+ except BaseException as exc:
717
833
  bandwidth_reservation.release()
718
834
  self._release_hit_slot(probe_id)
719
835
  logger.error(
@@ -1,6 +1,4 @@
1
1
  import os
2
- import sys
3
- import time
4
2
 
5
3
  try:
6
4
  # Shaded/vendored fallback
@@ -11,9 +9,6 @@ except ImportError:
11
9
  # Import our generated protos
12
10
  from hyperprobe.protos import agent_pb2
13
11
  from hyperprobe.protos import agent_pb2_grpc
14
- from hyperprobe.core.logger import get_logger
15
-
16
- logger = get_logger("hyperprobe:broker")
17
12
 
18
13
 
19
14
  class BrokerTransportError(RuntimeError):
@@ -37,7 +32,10 @@ class BrokerClient:
37
32
  self.agent_id = agent_id
38
33
  if agent_version is None:
39
34
  try:
40
- from importlib.metadata import version
35
+ try:
36
+ from importlib.metadata import version
37
+ except ImportError:
38
+ from importlib_metadata import version
41
39
  agent_version = version("hyperprobe-agent")
42
40
  except Exception:
43
41
  agent_version = "unknown"
@@ -0,0 +1 @@
1
+ """CPython 3.7–3.11 instrumentation; imported only by the startup selector."""