beacon-profiling 1.2.0__tar.gz → 1.2.2__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.
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/PKG-INFO +54 -6
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/README.rst +49 -1
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/pyproject.toml +4 -4
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/collector/base.py +12 -0
- beacon_profiling-1.2.2/src/opentelemetry/sdk/extension/profiling/collector/buffering.py +207 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/collector/exception.py +26 -9
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/collector/lock.py +172 -58
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/collector/memory.py +54 -43
- beacon_profiling-1.2.2/src/opentelemetry/sdk/extension/profiling/collector/stack.py +167 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/context_bridge.py +41 -7
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/environment_variables.py +5 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/pprof_http.py +41 -13
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/model/builder.py +10 -6
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/model/pprof_builder.py +4 -8
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/runtime.py +79 -22
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/scheduler.py +15 -2
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/version.py +1 -1
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_asyncio_lock_collector.py +9 -4
- beacon_profiling-1.2.2/tests/test_asyncio_lock_counts.py +102 -0
- beacon_profiling-1.2.2/tests/test_buffer_budget.py +257 -0
- beacon_profiling-1.2.2/tests/test_dictionary_contract.py +92 -0
- beacon_profiling-1.2.2/tests/test_local_root_context.py +156 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_lock_collector.py +16 -5
- beacon_profiling-1.2.2/tests/test_lock_sampling.py +161 -0
- beacon_profiling-1.2.2/tests/test_pprof_http_exporter.py +158 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_profiler_integration.py +4 -1
- beacon_profiling-1.2.2/tests/test_protobuf_environment.py +71 -0
- beacon_profiling-1.2.2/tests/test_scheduler_resilience.py +46 -0
- beacon_profiling-1.2.2/tests/test_stop_lifecycle.py +155 -0
- beacon_profiling-1.2.0/src/opentelemetry/sdk/extension/profiling/collector/stack.py +0 -152
- beacon_profiling-1.2.0/tests/test_pprof_http_exporter.py +0 -74
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/.gitignore +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/LICENSE +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/__init__.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/bootstrap.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/collector/__init__.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/__init__.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/grpc.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/http.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/pprof.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/pprof_pb2.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/export/result.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/src/opentelemetry/sdk/extension/profiling/model/__init__.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_asyncio_context_bridge.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_auto_instrumentation_integration.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_bootstrap.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_builder.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_context_bridge.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_exception_collector.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_exporters.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_fork_runtime.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_memory_collector.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_package_metadata.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_pprof_builder.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_pprof_exporter.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_runtime_exporter_selection.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_runtime_scheduling.py +0 -0
- {beacon_profiling-1.2.0 → beacon_profiling-1.2.2}/tests/test_stack_collector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: beacon-profiling
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Beacon Profiling for OpenTelemetry Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/beacon-observability/beacon-python/tree/main/sdk-extension/beacon-profiling
|
|
6
6
|
Project-URL: Repository, https://github.com/beacon-observability/beacon-python
|
|
@@ -18,17 +18,17 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist: opentelemetry-api==1.
|
|
22
|
-
Requires-Dist: opentelemetry-exporter-otlp==1.
|
|
23
|
-
Requires-Dist: opentelemetry-instrumentation==0.
|
|
24
|
-
Requires-Dist: opentelemetry-sdk==1.
|
|
21
|
+
Requires-Dist: opentelemetry-api==1.45.1
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-otlp==1.45.1
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation==0.66b1
|
|
24
|
+
Requires-Dist: opentelemetry-sdk==1.45.1
|
|
25
25
|
Requires-Dist: requests>=2.20
|
|
26
26
|
Description-Content-Type: text/x-rst
|
|
27
27
|
|
|
28
28
|
Beacon Profiling for Python
|
|
29
29
|
===========================
|
|
30
30
|
|
|
31
|
-
The current stable version is ``beacon-profiling==1.1
|
|
31
|
+
The current stable version is ``beacon-profiling==1.2.1``.
|
|
32
32
|
|
|
33
33
|
This package provides a Python profiling runtime for OpenTelemetry
|
|
34
34
|
auto-instrumentation. The current implementation exports profiles via OTLP
|
|
@@ -44,6 +44,13 @@ or a compatible pprof endpoint and supports multiple collector types:
|
|
|
44
44
|
Handled-exception collection requires Python ``sys.monitoring`` (Python
|
|
45
45
|
3.12+); on Python 3.10/3.11 the other collectors remain available.
|
|
46
46
|
|
|
47
|
+
Trace-correlated samples retain the current span ID separately from the
|
|
48
|
+
local root span ID. Endpoint and span kind come from that local root, so a
|
|
49
|
+
database child span does not replace the request endpoint. Root identity
|
|
50
|
+
travels with the OpenTelemetry context across tasks and propagated threads.
|
|
51
|
+
If profiling starts inside a pre-existing local child span whose root was
|
|
52
|
+
not observed, the root and endpoint remain unknown.
|
|
53
|
+
|
|
47
54
|
Usage
|
|
48
55
|
-----
|
|
49
56
|
|
|
@@ -75,11 +82,17 @@ The following environment variables are supported:
|
|
|
75
82
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
76
83
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
77
84
|
* ``OTEL_PROFILING_MAX_FRAMES``
|
|
85
|
+
* ``OTEL_PROFILING_MAX_BUFFER_SAMPLES`` (defaults to 10000)
|
|
86
|
+
* ``OTEL_PROFILING_MAX_BUFFER_BYTES`` (defaults to 8388608 accounted bytes)
|
|
78
87
|
* ``OTEL_PROFILING_INCLUDE_TRACE_CONTEXT``
|
|
79
88
|
* ``OTEL_PROFILING_EXCEPTION_ENABLED``
|
|
80
89
|
* ``OTEL_PROFILING_EXCEPTION_SAMPLING_INTERVAL``
|
|
81
90
|
* ``OTEL_PROFILING_EXCEPTION_COLLECT_MESSAGE``
|
|
82
91
|
* ``OTEL_PROFILING_LOCK_ENABLED``
|
|
92
|
+
* ``OTEL_PROFILING_LOCK_SAMPLING_INTERVAL`` (defaults to 100; one in N
|
|
93
|
+
short acquisitions is sampled; 1 records every operation)
|
|
94
|
+
* ``OTEL_PROFILING_LOCK_MIN_DURATION_NS`` (defaults to 1000000; operations
|
|
95
|
+
at or above this duration are recorded without probabilistic sampling)
|
|
83
96
|
* ``OTEL_PROFILING_MEMORY_ENABLED``
|
|
84
97
|
* ``OTEL_PROFILING_MEMORY_INTERVAL`` (defaults to the profile export interval
|
|
85
98
|
and can be overridden by the ``Profiler(memory_interval=...)`` argument)
|
|
@@ -94,6 +107,8 @@ The following environment variables are supported:
|
|
|
94
107
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
|
|
95
108
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
|
|
96
109
|
* ``OTEL_PROFILING_PPROF_PATH``
|
|
110
|
+
* ``OTEL_PROFILING_PPROF_MAX_FILES`` (HTTP upload local copies only; defaults
|
|
111
|
+
to 3 retained files per exporter instance)
|
|
97
112
|
* ``OTEL_PROFILING_PPROF_UPLOAD_URL``
|
|
98
113
|
* ``OTEL_PROFILING_PPROF_HEADERS``
|
|
99
114
|
|
|
@@ -101,6 +116,28 @@ Service metadata should be configured with standard OpenTelemetry
|
|
|
101
116
|
resource settings such as ``OTEL_SERVICE_NAME`` and
|
|
102
117
|
``OTEL_RESOURCE_ATTRIBUTES``.
|
|
103
118
|
|
|
119
|
+
Short lock acquisition/hold pairs share one sampling decision. Slow holds
|
|
120
|
+
and waits are retained even when their acquisition was not sampled. Sampling
|
|
121
|
+
is decided before stack walking and trace lookup; filename normalization
|
|
122
|
+
uses a bounded cache. OTLP samples carry their interval as ``period`` and
|
|
123
|
+
``lock.sampling.interval``. Compatible pprof lock counts and durations are
|
|
124
|
+
weighted by that interval; they are estimates when the interval exceeds 1.
|
|
125
|
+
|
|
126
|
+
One profiler-wide budget covers raw samples in all collectors and the
|
|
127
|
+
pending export buffer. Both limits also accept ``Profiler(max_buffer_samples=,
|
|
128
|
+
max_buffer_bytes=)`` arguments. Excess samples are dropped, counted in
|
|
129
|
+
``profiler.buffer_statistics``, and reported in a warning at flush. The next
|
|
130
|
+
window can collect again after draining; delivery is bounded, not lossless.
|
|
131
|
+
Retained samples preserve their individual timestamps, trace identity, values,
|
|
132
|
+
and CPU deltas. Immutable stack frames use a bounded cache.
|
|
133
|
+
|
|
134
|
+
Byte accounting conservatively includes sample metadata and frame references,
|
|
135
|
+
including shared objects. It bounds queued raw data, not total process RSS;
|
|
136
|
+
heap snapshots and serialization also need temporary memory. Capture/build/
|
|
137
|
+
export work is excluded from the profiler's own exception and lock collection.
|
|
138
|
+
``memory_interval=0`` keeps manual memory capture available on each call;
|
|
139
|
+
periodic memory scheduling uses the positive export cadence instead of spinning.
|
|
140
|
+
|
|
104
141
|
Examples
|
|
105
142
|
--------
|
|
106
143
|
|
|
@@ -131,8 +168,19 @@ copy:
|
|
|
131
168
|
|
|
132
169
|
export OTEL_PROFILING_PPROF_UPLOAD_URL=http://localhost:9529/profiling/v1/input
|
|
133
170
|
export OTEL_PROFILING_PPROF_HEADERS="X-API-Key:xxx"
|
|
171
|
+
export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
|
|
134
172
|
beacon python app.py
|
|
135
173
|
|
|
174
|
+
HTTP upload does not write local files unless ``OTEL_PROFILING_PPROF_PATH``
|
|
175
|
+
is set. An empty ``pprof_path`` argument disables copies even when that
|
|
176
|
+
environment variable is set. Local write/cleanup failures are logged and do
|
|
177
|
+
not prevent upload. Retention removes only files written by this exporter
|
|
178
|
+
instance; it never scans or removes files owned by other processes. The
|
|
179
|
+
file-only ``pprof`` exporter retains its existing output behavior.
|
|
180
|
+
|
|
181
|
+
Capture and export exceptions are logged; subsequent scheduled captures
|
|
182
|
+
and export attempts continue. Failed batches are not replayed automatically.
|
|
183
|
+
|
|
136
184
|
When ``OTEL_PROFILING_EXPORTER`` is unset and
|
|
137
185
|
``OTEL_PROFILING_PPROF_UPLOAD_URL`` is configured, the runtime
|
|
138
186
|
defaults to a legacy-compatible ``pprof`` upload layout. Set
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Beacon Profiling for Python
|
|
2
2
|
===========================
|
|
3
3
|
|
|
4
|
-
The current stable version is ``beacon-profiling==1.1
|
|
4
|
+
The current stable version is ``beacon-profiling==1.2.1``.
|
|
5
5
|
|
|
6
6
|
This package provides a Python profiling runtime for OpenTelemetry
|
|
7
7
|
auto-instrumentation. The current implementation exports profiles via OTLP
|
|
@@ -17,6 +17,13 @@ or a compatible pprof endpoint and supports multiple collector types:
|
|
|
17
17
|
Handled-exception collection requires Python ``sys.monitoring`` (Python
|
|
18
18
|
3.12+); on Python 3.10/3.11 the other collectors remain available.
|
|
19
19
|
|
|
20
|
+
Trace-correlated samples retain the current span ID separately from the
|
|
21
|
+
local root span ID. Endpoint and span kind come from that local root, so a
|
|
22
|
+
database child span does not replace the request endpoint. Root identity
|
|
23
|
+
travels with the OpenTelemetry context across tasks and propagated threads.
|
|
24
|
+
If profiling starts inside a pre-existing local child span whose root was
|
|
25
|
+
not observed, the root and endpoint remain unknown.
|
|
26
|
+
|
|
20
27
|
Usage
|
|
21
28
|
-----
|
|
22
29
|
|
|
@@ -48,11 +55,17 @@ The following environment variables are supported:
|
|
|
48
55
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
49
56
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
50
57
|
* ``OTEL_PROFILING_MAX_FRAMES``
|
|
58
|
+
* ``OTEL_PROFILING_MAX_BUFFER_SAMPLES`` (defaults to 10000)
|
|
59
|
+
* ``OTEL_PROFILING_MAX_BUFFER_BYTES`` (defaults to 8388608 accounted bytes)
|
|
51
60
|
* ``OTEL_PROFILING_INCLUDE_TRACE_CONTEXT``
|
|
52
61
|
* ``OTEL_PROFILING_EXCEPTION_ENABLED``
|
|
53
62
|
* ``OTEL_PROFILING_EXCEPTION_SAMPLING_INTERVAL``
|
|
54
63
|
* ``OTEL_PROFILING_EXCEPTION_COLLECT_MESSAGE``
|
|
55
64
|
* ``OTEL_PROFILING_LOCK_ENABLED``
|
|
65
|
+
* ``OTEL_PROFILING_LOCK_SAMPLING_INTERVAL`` (defaults to 100; one in N
|
|
66
|
+
short acquisitions is sampled; 1 records every operation)
|
|
67
|
+
* ``OTEL_PROFILING_LOCK_MIN_DURATION_NS`` (defaults to 1000000; operations
|
|
68
|
+
at or above this duration are recorded without probabilistic sampling)
|
|
56
69
|
* ``OTEL_PROFILING_MEMORY_ENABLED``
|
|
57
70
|
* ``OTEL_PROFILING_MEMORY_INTERVAL`` (defaults to the profile export interval
|
|
58
71
|
and can be overridden by the ``Profiler(memory_interval=...)`` argument)
|
|
@@ -67,6 +80,8 @@ The following environment variables are supported:
|
|
|
67
80
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
|
|
68
81
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
|
|
69
82
|
* ``OTEL_PROFILING_PPROF_PATH``
|
|
83
|
+
* ``OTEL_PROFILING_PPROF_MAX_FILES`` (HTTP upload local copies only; defaults
|
|
84
|
+
to 3 retained files per exporter instance)
|
|
70
85
|
* ``OTEL_PROFILING_PPROF_UPLOAD_URL``
|
|
71
86
|
* ``OTEL_PROFILING_PPROF_HEADERS``
|
|
72
87
|
|
|
@@ -74,6 +89,28 @@ Service metadata should be configured with standard OpenTelemetry
|
|
|
74
89
|
resource settings such as ``OTEL_SERVICE_NAME`` and
|
|
75
90
|
``OTEL_RESOURCE_ATTRIBUTES``.
|
|
76
91
|
|
|
92
|
+
Short lock acquisition/hold pairs share one sampling decision. Slow holds
|
|
93
|
+
and waits are retained even when their acquisition was not sampled. Sampling
|
|
94
|
+
is decided before stack walking and trace lookup; filename normalization
|
|
95
|
+
uses a bounded cache. OTLP samples carry their interval as ``period`` and
|
|
96
|
+
``lock.sampling.interval``. Compatible pprof lock counts and durations are
|
|
97
|
+
weighted by that interval; they are estimates when the interval exceeds 1.
|
|
98
|
+
|
|
99
|
+
One profiler-wide budget covers raw samples in all collectors and the
|
|
100
|
+
pending export buffer. Both limits also accept ``Profiler(max_buffer_samples=,
|
|
101
|
+
max_buffer_bytes=)`` arguments. Excess samples are dropped, counted in
|
|
102
|
+
``profiler.buffer_statistics``, and reported in a warning at flush. The next
|
|
103
|
+
window can collect again after draining; delivery is bounded, not lossless.
|
|
104
|
+
Retained samples preserve their individual timestamps, trace identity, values,
|
|
105
|
+
and CPU deltas. Immutable stack frames use a bounded cache.
|
|
106
|
+
|
|
107
|
+
Byte accounting conservatively includes sample metadata and frame references,
|
|
108
|
+
including shared objects. It bounds queued raw data, not total process RSS;
|
|
109
|
+
heap snapshots and serialization also need temporary memory. Capture/build/
|
|
110
|
+
export work is excluded from the profiler's own exception and lock collection.
|
|
111
|
+
``memory_interval=0`` keeps manual memory capture available on each call;
|
|
112
|
+
periodic memory scheduling uses the positive export cadence instead of spinning.
|
|
113
|
+
|
|
77
114
|
Examples
|
|
78
115
|
--------
|
|
79
116
|
|
|
@@ -104,8 +141,19 @@ copy:
|
|
|
104
141
|
|
|
105
142
|
export OTEL_PROFILING_PPROF_UPLOAD_URL=http://localhost:9529/profiling/v1/input
|
|
106
143
|
export OTEL_PROFILING_PPROF_HEADERS="X-API-Key:xxx"
|
|
144
|
+
export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
|
|
107
145
|
beacon python app.py
|
|
108
146
|
|
|
147
|
+
HTTP upload does not write local files unless ``OTEL_PROFILING_PPROF_PATH``
|
|
148
|
+
is set. An empty ``pprof_path`` argument disables copies even when that
|
|
149
|
+
environment variable is set. Local write/cleanup failures are logged and do
|
|
150
|
+
not prevent upload. Retention removes only files written by this exporter
|
|
151
|
+
instance; it never scans or removes files owned by other processes. The
|
|
152
|
+
file-only ``pprof`` exporter retains its existing output behavior.
|
|
153
|
+
|
|
154
|
+
Capture and export exceptions are logged; subsequent scheduled captures
|
|
155
|
+
and export attempts continue. Failed batches are not replayed automatically.
|
|
156
|
+
|
|
109
157
|
When ``OTEL_PROFILING_EXPORTER`` is unset and
|
|
110
158
|
``OTEL_PROFILING_PPROF_UPLOAD_URL`` is configured, the runtime
|
|
111
159
|
defaults to a legacy-compatible ``pprof`` upload layout. Set
|
|
@@ -25,10 +25,10 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3.14",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"opentelemetry-api == 1.
|
|
29
|
-
"opentelemetry-sdk == 1.
|
|
30
|
-
"opentelemetry-instrumentation == 0.
|
|
31
|
-
"opentelemetry-exporter-otlp == 1.
|
|
28
|
+
"opentelemetry-api == 1.45.1",
|
|
29
|
+
"opentelemetry-sdk == 1.45.1",
|
|
30
|
+
"opentelemetry-instrumentation == 0.66b1",
|
|
31
|
+
"opentelemetry-exporter-otlp == 1.45.1",
|
|
32
32
|
"requests >= 2.20",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
+
from functools import lru_cache
|
|
18
19
|
from typing import Protocol
|
|
19
20
|
|
|
20
21
|
|
|
@@ -25,6 +26,17 @@ class CapturedFrame:
|
|
|
25
26
|
lineno: int
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@lru_cache(maxsize=4096)
|
|
30
|
+
def _cached_frame(function: str, filename: str, lineno: int) -> CapturedFrame:
|
|
31
|
+
return CapturedFrame(function, filename, lineno)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def captured_frame(function: str, filename: str, lineno: int) -> CapturedFrame:
|
|
35
|
+
if len(function) <= 512 and len(filename) <= 2048:
|
|
36
|
+
return _cached_frame(function, filename, lineno)
|
|
37
|
+
return CapturedFrame(function, filename, lineno)
|
|
38
|
+
|
|
39
|
+
|
|
28
40
|
@dataclass(frozen=True, slots=True)
|
|
29
41
|
class CapturedAttribute:
|
|
30
42
|
key: str
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from _thread import allocate_lock
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import fields
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from os import environ
|
|
10
|
+
from typing import Iterator
|
|
11
|
+
|
|
12
|
+
from opentelemetry.sdk.extension.profiling.collector.base import (
|
|
13
|
+
CapturedAttribute,
|
|
14
|
+
CapturedFrame,
|
|
15
|
+
CapturedSample,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_INTERNAL = ContextVar("beacon_profiling_internal_work", default=False)
|
|
19
|
+
_FIELD_NAMES = {
|
|
20
|
+
cls: tuple(field.name for field in fields(cls))
|
|
21
|
+
for cls in (CapturedSample, CapturedAttribute)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@lru_cache(maxsize=4096)
|
|
26
|
+
def _frame_size(frame: CapturedFrame) -> int:
|
|
27
|
+
return (
|
|
28
|
+
sys.getsizeof(frame)
|
|
29
|
+
+ sys.getsizeof(frame.function)
|
|
30
|
+
+ sys.getsizeof(frame.filename)
|
|
31
|
+
+ sys.getsizeof(frame.lineno)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def internal_work() -> bool:
|
|
36
|
+
return _INTERNAL.get()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@contextmanager
|
|
40
|
+
def suppress_collection() -> Iterator[None]:
|
|
41
|
+
token = _INTERNAL.set(True)
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
finally:
|
|
45
|
+
_INTERNAL.reset(token)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sample_size(sample: CapturedSample, limit: int) -> int:
|
|
49
|
+
# Charge shared frame/string references conservatively per sample. The
|
|
50
|
+
# budget bounds producer-owned raw data, not application heap snapshots
|
|
51
|
+
# or temporary serialized messages. Never invoke user __sizeof__ hooks.
|
|
52
|
+
pending: list[object] = [sample]
|
|
53
|
+
seen: set[int] = set()
|
|
54
|
+
total = 96 # Buffer entry and accounting overhead.
|
|
55
|
+
while pending:
|
|
56
|
+
value = pending.pop()
|
|
57
|
+
if id(value) in seen:
|
|
58
|
+
continue
|
|
59
|
+
seen.add(id(value))
|
|
60
|
+
if len(seen) > 4096:
|
|
61
|
+
return limit + 1
|
|
62
|
+
cls = type(value)
|
|
63
|
+
if cls is CapturedFrame:
|
|
64
|
+
if (
|
|
65
|
+
type(value.function) is not str
|
|
66
|
+
or type(value.filename) is not str
|
|
67
|
+
or type(value.lineno) is not int
|
|
68
|
+
):
|
|
69
|
+
return limit + 1
|
|
70
|
+
if len(value.function) <= 512 and len(value.filename) <= 2048:
|
|
71
|
+
total += _frame_size(value)
|
|
72
|
+
else:
|
|
73
|
+
total += sum(
|
|
74
|
+
sys.getsizeof(item)
|
|
75
|
+
for item in (
|
|
76
|
+
value,
|
|
77
|
+
value.function,
|
|
78
|
+
value.filename,
|
|
79
|
+
value.lineno,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
if total > limit:
|
|
83
|
+
return total
|
|
84
|
+
continue
|
|
85
|
+
if cls is CapturedSample or cls is CapturedAttribute:
|
|
86
|
+
pending.extend(getattr(value, name) for name in _FIELD_NAMES[cls])
|
|
87
|
+
elif cls is tuple or cls is list:
|
|
88
|
+
if len(value) > 4096:
|
|
89
|
+
return limit + 1
|
|
90
|
+
pending.extend(value)
|
|
91
|
+
elif cls is dict:
|
|
92
|
+
if len(value) > 2048:
|
|
93
|
+
return limit + 1
|
|
94
|
+
pending.extend(value.keys())
|
|
95
|
+
pending.extend(value.values())
|
|
96
|
+
elif not any(
|
|
97
|
+
cls is scalar
|
|
98
|
+
for scalar in (str, bytes, bytearray, int, float, bool, type(None))
|
|
99
|
+
):
|
|
100
|
+
return limit + 1
|
|
101
|
+
total += sys.getsizeof(value)
|
|
102
|
+
if total > limit:
|
|
103
|
+
return total
|
|
104
|
+
return total
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SampleBudget:
|
|
108
|
+
def __init__(
|
|
109
|
+
self, max_samples: int | None = None, max_bytes: int | None = None
|
|
110
|
+
) -> None:
|
|
111
|
+
self.max_samples = (
|
|
112
|
+
max_samples
|
|
113
|
+
if max_samples is not None
|
|
114
|
+
else int(environ.get("OTEL_PROFILING_MAX_BUFFER_SAMPLES", "10000"))
|
|
115
|
+
)
|
|
116
|
+
self.max_bytes = (
|
|
117
|
+
max_bytes
|
|
118
|
+
if max_bytes is not None
|
|
119
|
+
else int(
|
|
120
|
+
environ.get(
|
|
121
|
+
"OTEL_PROFILING_MAX_BUFFER_BYTES", str(8 * 1024 * 1024)
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
if self.max_samples < 1 or self.max_bytes < 1:
|
|
126
|
+
raise ValueError("profile buffer limits must be positive")
|
|
127
|
+
self.reset_after_fork()
|
|
128
|
+
|
|
129
|
+
def reset_after_fork(self) -> None:
|
|
130
|
+
self._lock = allocate_lock()
|
|
131
|
+
self.generation = getattr(self, "generation", 0) + 1
|
|
132
|
+
self._used_samples = 0
|
|
133
|
+
self._used_bytes = 0
|
|
134
|
+
self._dropped_samples = 0
|
|
135
|
+
|
|
136
|
+
def reserve(self, sample: CapturedSample, generation: int) -> int | None:
|
|
137
|
+
size = _sample_size(sample, self.max_bytes)
|
|
138
|
+
with self._lock:
|
|
139
|
+
if (
|
|
140
|
+
generation != self.generation
|
|
141
|
+
or self._used_samples >= self.max_samples
|
|
142
|
+
or self._used_bytes + size > self.max_bytes
|
|
143
|
+
):
|
|
144
|
+
self._dropped_samples += 1
|
|
145
|
+
return None
|
|
146
|
+
self._used_samples += 1
|
|
147
|
+
self._used_bytes += size
|
|
148
|
+
return size
|
|
149
|
+
|
|
150
|
+
def release(self, count: int, size: int, generation: int) -> None:
|
|
151
|
+
with self._lock:
|
|
152
|
+
if generation != self.generation:
|
|
153
|
+
return
|
|
154
|
+
self._used_samples -= count
|
|
155
|
+
self._used_bytes -= size
|
|
156
|
+
|
|
157
|
+
def snapshot(self) -> dict[str, int]:
|
|
158
|
+
with self._lock:
|
|
159
|
+
return {
|
|
160
|
+
"max_samples": self.max_samples,
|
|
161
|
+
"max_bytes": self.max_bytes,
|
|
162
|
+
"used_samples": self._used_samples,
|
|
163
|
+
"used_bytes": self._used_bytes,
|
|
164
|
+
"dropped_samples": self._dropped_samples,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SampleBuffer:
|
|
169
|
+
"""Caller-locked buffer sharing a single profiler-wide raw-data budget."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, budget: SampleBudget) -> None:
|
|
172
|
+
self._budget = budget
|
|
173
|
+
self._generation = budget.generation
|
|
174
|
+
self._items: list[tuple[CapturedSample, int]] = []
|
|
175
|
+
|
|
176
|
+
def __len__(self) -> int:
|
|
177
|
+
return len(self._items)
|
|
178
|
+
|
|
179
|
+
def append(self, sample: CapturedSample) -> None:
|
|
180
|
+
size = self._budget.reserve(sample, self._generation)
|
|
181
|
+
if size is not None:
|
|
182
|
+
try:
|
|
183
|
+
self._items.append((sample, size))
|
|
184
|
+
except BaseException:
|
|
185
|
+
self._budget.release(1, size, self._generation)
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
def extend(self, samples: list[CapturedSample]) -> None:
|
|
189
|
+
for sample in samples:
|
|
190
|
+
self.append(sample)
|
|
191
|
+
|
|
192
|
+
def drain(self) -> list[CapturedSample]:
|
|
193
|
+
result = [sample for sample, _ in self._items]
|
|
194
|
+
self.clear()
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
def clear(self) -> None:
|
|
198
|
+
items, self._items = self._items, []
|
|
199
|
+
self._budget.release(
|
|
200
|
+
len(items), sum(size for _, size in items), self._generation
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def __enter__(self) -> SampleBuffer:
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def __exit__(self, *_: object) -> None:
|
|
207
|
+
self.clear()
|
|
@@ -27,6 +27,12 @@ from opentelemetry.sdk.extension.profiling.collector.base import (
|
|
|
27
27
|
CapturedAttribute,
|
|
28
28
|
CapturedFrame,
|
|
29
29
|
CapturedSample,
|
|
30
|
+
captured_frame,
|
|
31
|
+
)
|
|
32
|
+
from opentelemetry.sdk.extension.profiling.collector.buffering import (
|
|
33
|
+
SampleBudget,
|
|
34
|
+
SampleBuffer,
|
|
35
|
+
internal_work,
|
|
30
36
|
)
|
|
31
37
|
from opentelemetry.sdk.extension.profiling.context_bridge import ContextBridge
|
|
32
38
|
from opentelemetry.trace import INVALID_SPAN_CONTEXT
|
|
@@ -66,17 +72,22 @@ class ExceptionCollector:
|
|
|
66
72
|
include_trace_context: bool = True,
|
|
67
73
|
sampling_interval: int = 100,
|
|
68
74
|
collect_message: bool = False,
|
|
75
|
+
sample_budget: SampleBudget | None = None,
|
|
69
76
|
) -> None:
|
|
70
77
|
if sampling_interval < 1:
|
|
71
78
|
raise ValueError("sampling_interval must be >= 1")
|
|
72
79
|
|
|
80
|
+
self._owns_sample_budget = sample_budget is None
|
|
81
|
+
self._sample_budget = (
|
|
82
|
+
sample_budget if sample_budget is not None else SampleBudget()
|
|
83
|
+
)
|
|
73
84
|
self._context_bridge = context_bridge
|
|
74
85
|
self._max_frames = max_frames
|
|
75
86
|
self._include_trace_context = include_trace_context
|
|
76
87
|
self._sampling_interval = sampling_interval
|
|
77
88
|
self._collect_message = collect_message
|
|
78
89
|
|
|
79
|
-
self._buffer
|
|
90
|
+
self._buffer = SampleBuffer(self._sample_budget)
|
|
80
91
|
self._buffer_lock = threading.Lock()
|
|
81
92
|
self._sampler = _PoissonSampler()
|
|
82
93
|
self._counter = 0
|
|
@@ -134,24 +145,26 @@ class ExceptionCollector:
|
|
|
134
145
|
|
|
135
146
|
def stop(self) -> None:
|
|
136
147
|
tool_id = self._monitoring_tool_id
|
|
148
|
+
with self._buffer_lock:
|
|
149
|
+
self._monitoring_tool_id = None
|
|
150
|
+
self._buffer.clear()
|
|
137
151
|
if tool_id is None:
|
|
138
152
|
return
|
|
139
153
|
|
|
140
154
|
self._cleanup_monitoring(tool_id)
|
|
141
|
-
self._monitoring_tool_id = None
|
|
142
155
|
|
|
143
156
|
def capture(self) -> list[CapturedSample]:
|
|
144
157
|
with self._buffer_lock:
|
|
145
|
-
|
|
146
|
-
self._buffer = []
|
|
147
|
-
return captured
|
|
158
|
+
return self._buffer.drain()
|
|
148
159
|
|
|
149
160
|
def reset_after_fork(self) -> None:
|
|
150
161
|
tool_id = self._monitoring_tool_id
|
|
151
162
|
if tool_id is not None:
|
|
152
163
|
self._cleanup_monitoring(tool_id)
|
|
153
164
|
self._buffer_lock = threading.Lock()
|
|
154
|
-
self.
|
|
165
|
+
if self._owns_sample_budget:
|
|
166
|
+
self._sample_budget.reset_after_fork()
|
|
167
|
+
self._buffer = SampleBuffer(self._sample_budget)
|
|
155
168
|
self._counter = 0
|
|
156
169
|
self._next_sample = self._sampling_interval
|
|
157
170
|
self._collecting_state = threading.local()
|
|
@@ -197,7 +210,10 @@ class ExceptionCollector:
|
|
|
197
210
|
) -> None:
|
|
198
211
|
del code, instruction_offset
|
|
199
212
|
|
|
200
|
-
if self._is_collecting():
|
|
213
|
+
if self._is_collecting() or internal_work():
|
|
214
|
+
return
|
|
215
|
+
tool_id = self._monitoring_tool_id
|
|
216
|
+
if tool_id is None:
|
|
201
217
|
return
|
|
202
218
|
|
|
203
219
|
self._counter += 1
|
|
@@ -213,7 +229,8 @@ class ExceptionCollector:
|
|
|
213
229
|
if sample is None:
|
|
214
230
|
return
|
|
215
231
|
with self._buffer_lock:
|
|
216
|
-
self.
|
|
232
|
+
if self._monitoring_tool_id == tool_id:
|
|
233
|
+
self._buffer.append(sample)
|
|
217
234
|
except Exception:
|
|
218
235
|
_logger.debug("Failed to capture handled exception", exc_info=True)
|
|
219
236
|
finally:
|
|
@@ -287,7 +304,7 @@ class ExceptionCollector:
|
|
|
287
304
|
while current is not None:
|
|
288
305
|
frame = current.tb_frame
|
|
289
306
|
frames.append(
|
|
290
|
-
|
|
307
|
+
captured_frame(
|
|
291
308
|
function=frame.f_code.co_name,
|
|
292
309
|
filename=frame.f_code.co_filename,
|
|
293
310
|
lineno=current.tb_lineno,
|