hyperprobe-agent 1.2.30b4__tar.gz → 1.2.30b6__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.
- {hyperprobe_agent-1.2.30b4/hyperprobe_agent.egg-info → hyperprobe_agent-1.2.30b6}/PKG-INFO +81 -13
- hyperprobe_agent-1.2.30b4/PKG-INFO → hyperprobe_agent-1.2.30b6/README.md +67 -44
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/agent.py +143 -27
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/broker.py +4 -6
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/__init__.py +1 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/controller.py +300 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/discovery.py +71 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/engine.py +674 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/imports.py +60 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/metric_processor.py +262 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/records.py +51 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/bytecode/rewriting.py +102 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/engine_base.py +165 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/evaluator.py +17 -11
- hyperprobe_agent-1.2.30b6/hyperprobe/core/locations.py +16 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/logger.py +15 -3
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/monitoring_engine.py +68 -294
- hyperprobe_agent-1.2.30b6/hyperprobe/core/probe_processor.py +163 -0
- hyperprobe_agent-1.2.30b6/hyperprobe/core/redaction.py +117 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/serializer.py +6 -12
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/protos/agent_pb2.py +2 -3
- hyperprobe_agent-1.2.30b4/README.md → hyperprobe_agent-1.2.30b6/hyperprobe_agent.egg-info/PKG-INFO +112 -9
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe_agent.egg-info/SOURCES.txt +19 -0
- hyperprobe_agent-1.2.30b6/hyperprobe_agent.egg-info/requires.txt +19 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/setup.py +14 -4
- hyperprobe_agent-1.2.30b6/tests/test_agent_backend_integration.py +229 -0
- hyperprobe_agent-1.2.30b6/tests/test_bytecode_backend.py +397 -0
- hyperprobe_agent-1.2.30b6/tests/test_bytecode_lifecycle.py +400 -0
- hyperprobe_agent-1.2.30b6/tests/test_bytecode_token_lifecycle.py +312 -0
- hyperprobe_agent-1.2.30b6/tests/test_engine_selection.py +37 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_fork.py +5 -5
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_logger.py +21 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_metric_probes.py +8 -2
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_multithreading_integration.py +6 -2
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_probe_output.py +28 -0
- hyperprobe_agent-1.2.30b6/tests/test_redaction.py +78 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_serializer.py +26 -0
- hyperprobe_agent-1.2.30b6/tests/test_shared_processor.py +220 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_startup.py +18 -0
- hyperprobe_agent-1.2.30b4/hyperprobe_agent.egg-info/requires.txt +0 -8
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/LICENSE +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/__init__.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/__init__.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/probe_output.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/quota.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/safety.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/core/trace_extractor.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/protos/__init__.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe/protos/agent_pb2_grpc.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe_agent.egg-info/dependency_links.txt +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/hyperprobe_agent.egg-info/top_level.txt +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/setup.cfg +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_agent.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_evaluator.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/tests/test_p1_regressions.py +0 -0
- {hyperprobe_agent-1.2.30b4 → hyperprobe_agent-1.2.30b6}/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.
|
|
3
|
+
Version: 1.2.30b6
|
|
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.
|
|
18
|
+
Requires-Python: >=3.7
|
|
14
19
|
Description-Content-Type: text/markdown
|
|
15
20
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: protobuf<
|
|
17
|
-
Requires-Dist:
|
|
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
|
|
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:**
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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:**
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
#
|
|
70
|
-
self.sync_interval_sec = (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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__}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
#
|
|
586
|
-
# in
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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."""
|