beacon-profiling 1.2.3__tar.gz → 1.2.6__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.3 → beacon_profiling-1.2.6}/PKG-INFO +22 -35
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/README.rst +17 -30
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/pyproject.toml +4 -4
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/environment_variables.py +1 -4
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/__init__.py +1 -4
- beacon_profiling-1.2.3/src/opentelemetry/sdk/extension/profiling/export/pprof.py → beacon_profiling-1.2.6/src/opentelemetry/sdk/extension/profiling/export/file.py +12 -10
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/runtime.py +9 -39
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/version.py +1 -1
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_pprof_exporter.py +4 -4
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_runtime_exporter_selection.py +24 -49
- beacon_profiling-1.2.3/src/opentelemetry/sdk/extension/profiling/export/pprof_http.py +0 -310
- beacon_profiling-1.2.3/tests/test_pprof_http_exporter.py +0 -158
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/.gitignore +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/LICENSE +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/__init__.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/bootstrap.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/__init__.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/base.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/buffering.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/exception.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/lock.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/memory.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/stack.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/context_bridge.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/grpc.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/http.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/pprof_pb2.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/result.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/__init__.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/builder.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/pprof_builder.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/scheduler.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_asyncio_context_bridge.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_asyncio_lock_collector.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_asyncio_lock_counts.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_auto_instrumentation_integration.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_bootstrap.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_buffer_budget.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_builder.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_context_bridge.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_dictionary_contract.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_exception_collector.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_exporters.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_fork_runtime.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_local_root_context.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_lock_collector.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_lock_sampling.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_memory_collector.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_package_metadata.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_pprof_builder.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_profiler_integration.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_protobuf_environment.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_runtime_scheduling.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_scheduler_resilience.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_stack_collector.py +0 -0
- {beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_stop_lifecycle.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.6
|
|
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,10 +18,10 @@ 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.45.
|
|
22
|
-
Requires-Dist: opentelemetry-exporter-otlp==1.45.
|
|
23
|
-
Requires-Dist: opentelemetry-instrumentation==0.
|
|
24
|
-
Requires-Dist: opentelemetry-sdk==1.45.
|
|
21
|
+
Requires-Dist: opentelemetry-api==1.45.2
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-otlp==1.45.2
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation==0.66b2
|
|
24
|
+
Requires-Dist: opentelemetry-sdk==1.45.2
|
|
25
25
|
Requires-Dist: requests>=2.20
|
|
26
26
|
Description-Content-Type: text/x-rst
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ 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
|
|
35
|
-
or
|
|
35
|
+
or local pprof files and supports multiple collector types:
|
|
36
36
|
|
|
37
37
|
* stack sampling
|
|
38
38
|
* handled exceptions
|
|
@@ -77,7 +77,7 @@ Configuration
|
|
|
77
77
|
The following environment variables are supported:
|
|
78
78
|
|
|
79
79
|
* ``OTEL_PROFILING_ENABLED``
|
|
80
|
-
* ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``
|
|
80
|
+
* ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``file``; defaults to ``otlp``)
|
|
81
81
|
* ``OTEL_PROFILING_SAMPLE_INTERVAL``
|
|
82
82
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
83
83
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
@@ -106,11 +106,7 @@ The following environment variables are supported:
|
|
|
106
106
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CERTIFICATE``
|
|
107
107
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
|
|
108
108
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
|
|
109
|
-
* ``
|
|
110
|
-
* ``OTEL_PROFILING_PPROF_MAX_FILES`` (HTTP upload local copies only; defaults
|
|
111
|
-
to 3 retained files per exporter instance)
|
|
112
|
-
* ``OTEL_PROFILING_PPROF_UPLOAD_URL``
|
|
113
|
-
* ``OTEL_PROFILING_PPROF_HEADERS``
|
|
109
|
+
* ``OTEL_PROFILING_FILE_PATH`` (directory; defaults to ``otel-profiles``)
|
|
114
110
|
|
|
115
111
|
Service metadata should be configured with standard OpenTelemetry
|
|
116
112
|
resource settings such as ``OTEL_SERVICE_NAME`` and
|
|
@@ -158,30 +154,21 @@ To dump pprof files instead of OTLP, set:
|
|
|
158
154
|
|
|
159
155
|
::
|
|
160
156
|
|
|
161
|
-
export OTEL_PROFILING_EXPORTER=
|
|
162
|
-
export
|
|
157
|
+
export OTEL_PROFILING_EXPORTER=file
|
|
158
|
+
export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
The ``file`` exporter ignores all OTLP endpoint and transport settings.
|
|
161
|
+
It writes gzip pprof files to the configured directory, without network uploads.
|
|
166
162
|
|
|
167
|
-
::
|
|
168
|
-
|
|
169
|
-
export OTEL_PROFILING_PPROF_UPLOAD_URL=http://localhost:9529/profiling/v1/input
|
|
170
|
-
export OTEL_PROFILING_PPROF_HEADERS="X-API-Key:xxx"
|
|
171
|
-
export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
|
|
172
|
-
beacon python app.py
|
|
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.
|
|
163
|
+
For OTLP/gRPC, set::
|
|
180
164
|
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
export OTEL_PROFILING_EXPORTER=otlp
|
|
166
|
+
export OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL=grpc
|
|
167
|
+
export OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT=http://localhost:4317
|
|
183
168
|
|
|
184
|
-
|
|
185
|
-
``
|
|
186
|
-
|
|
187
|
-
``
|
|
169
|
+
OTLP/HTTP uses ``http/protobuf`` and an endpoint such as
|
|
170
|
+
``http://localhost:4318/v1development/profiles``. Profile-specific OTLP
|
|
171
|
+
settings take precedence over shared ``OTEL_EXPORTER_OTLP_*`` settings.
|
|
172
|
+
Only ``otlp`` and ``file`` exporter identifiers are accepted; disable
|
|
173
|
+
profiling with ``OTEL_PROFILING_ENABLED=false``. These source changes
|
|
174
|
+
require a new Beacon release. The previous multipart pprof exporter is removed.
|
|
@@ -5,7 +5,7 @@ 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
|
|
8
|
-
or
|
|
8
|
+
or local pprof files and supports multiple collector types:
|
|
9
9
|
|
|
10
10
|
* stack sampling
|
|
11
11
|
* handled exceptions
|
|
@@ -50,7 +50,7 @@ Configuration
|
|
|
50
50
|
The following environment variables are supported:
|
|
51
51
|
|
|
52
52
|
* ``OTEL_PROFILING_ENABLED``
|
|
53
|
-
* ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``
|
|
53
|
+
* ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``file``; defaults to ``otlp``)
|
|
54
54
|
* ``OTEL_PROFILING_SAMPLE_INTERVAL``
|
|
55
55
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
56
56
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
@@ -79,11 +79,7 @@ The following environment variables are supported:
|
|
|
79
79
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CERTIFICATE``
|
|
80
80
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
|
|
81
81
|
* ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
|
|
82
|
-
* ``
|
|
83
|
-
* ``OTEL_PROFILING_PPROF_MAX_FILES`` (HTTP upload local copies only; defaults
|
|
84
|
-
to 3 retained files per exporter instance)
|
|
85
|
-
* ``OTEL_PROFILING_PPROF_UPLOAD_URL``
|
|
86
|
-
* ``OTEL_PROFILING_PPROF_HEADERS``
|
|
82
|
+
* ``OTEL_PROFILING_FILE_PATH`` (directory; defaults to ``otel-profiles``)
|
|
87
83
|
|
|
88
84
|
Service metadata should be configured with standard OpenTelemetry
|
|
89
85
|
resource settings such as ``OTEL_SERVICE_NAME`` and
|
|
@@ -131,30 +127,21 @@ To dump pprof files instead of OTLP, set:
|
|
|
131
127
|
|
|
132
128
|
::
|
|
133
129
|
|
|
134
|
-
export OTEL_PROFILING_EXPORTER=
|
|
135
|
-
export
|
|
130
|
+
export OTEL_PROFILING_EXPORTER=file
|
|
131
|
+
export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
|
|
136
132
|
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
The ``file`` exporter ignores all OTLP endpoint and transport settings.
|
|
134
|
+
It writes gzip pprof files to the configured directory, without network uploads.
|
|
139
135
|
|
|
140
|
-
::
|
|
141
|
-
|
|
142
|
-
export OTEL_PROFILING_PPROF_UPLOAD_URL=http://localhost:9529/profiling/v1/input
|
|
143
|
-
export OTEL_PROFILING_PPROF_HEADERS="X-API-Key:xxx"
|
|
144
|
-
export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
|
|
145
|
-
beacon python app.py
|
|
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.
|
|
136
|
+
For OTLP/gRPC, set::
|
|
153
137
|
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
export OTEL_PROFILING_EXPORTER=otlp
|
|
139
|
+
export OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL=grpc
|
|
140
|
+
export OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT=http://localhost:4317
|
|
156
141
|
|
|
157
|
-
|
|
158
|
-
``
|
|
159
|
-
|
|
160
|
-
``
|
|
142
|
+
OTLP/HTTP uses ``http/protobuf`` and an endpoint such as
|
|
143
|
+
``http://localhost:4318/v1development/profiles``. Profile-specific OTLP
|
|
144
|
+
settings take precedence over shared ``OTEL_EXPORTER_OTLP_*`` settings.
|
|
145
|
+
Only ``otlp`` and ``file`` exporter identifiers are accepted; disable
|
|
146
|
+
profiling with ``OTEL_PROFILING_ENABLED=false``. These source changes
|
|
147
|
+
require a new Beacon release. The previous multipart pprof exporter is removed.
|
|
@@ -25,10 +25,10 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3.14",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"opentelemetry-api == 1.45.
|
|
29
|
-
"opentelemetry-sdk == 1.45.
|
|
30
|
-
"opentelemetry-instrumentation == 0.
|
|
31
|
-
"opentelemetry-exporter-otlp == 1.45.
|
|
28
|
+
"opentelemetry-api == 1.45.2",
|
|
29
|
+
"opentelemetry-sdk == 1.45.2",
|
|
30
|
+
"opentelemetry-instrumentation == 0.66b2",
|
|
31
|
+
"opentelemetry-exporter-otlp == 1.45.2",
|
|
32
32
|
"requests >= 2.20",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -34,10 +34,7 @@ OTEL_PROFILING_MEMORY_ENABLED = "OTEL_PROFILING_MEMORY_ENABLED"
|
|
|
34
34
|
OTEL_PROFILING_MEMORY_INTERVAL = "OTEL_PROFILING_MEMORY_INTERVAL"
|
|
35
35
|
OTEL_PROFILING_MEMORY_TOP_STATS = "OTEL_PROFILING_MEMORY_TOP_STATS"
|
|
36
36
|
OTEL_PROFILING_MEMORY_IGNORE_PROFILER = "OTEL_PROFILING_MEMORY_IGNORE_PROFILER"
|
|
37
|
-
|
|
38
|
-
OTEL_PROFILING_PPROF_MAX_FILES = "OTEL_PROFILING_PPROF_MAX_FILES"
|
|
39
|
-
OTEL_PROFILING_PPROF_UPLOAD_URL = "OTEL_PROFILING_PPROF_UPLOAD_URL"
|
|
40
|
-
OTEL_PROFILING_PPROF_HEADERS = "OTEL_PROFILING_PPROF_HEADERS"
|
|
37
|
+
OTEL_PROFILING_FILE_PATH = "OTEL_PROFILING_FILE_PATH"
|
|
41
38
|
|
|
42
39
|
OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL = "OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL"
|
|
43
40
|
OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT = "OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT"
|
|
@@ -12,11 +12,8 @@ from opentelemetry.sdk.extension.profiling.export.grpc import (
|
|
|
12
12
|
from opentelemetry.sdk.extension.profiling.export.http import (
|
|
13
13
|
OTLPHTTPProfileExporter,
|
|
14
14
|
)
|
|
15
|
-
from opentelemetry.sdk.extension.profiling.export.pprof_http import (
|
|
16
|
-
PPROFHTTPExporter as CompatiblePPROFExporter,
|
|
17
|
-
)
|
|
18
15
|
|
|
19
|
-
__all__ = ("
|
|
16
|
+
__all__ = ("create_otlp_profile_exporter",)
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
def create_otlp_profile_exporter():
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import uuid4
|
|
5
6
|
|
|
6
7
|
from opentelemetry.sdk.extension.profiling.environment_variables import (
|
|
7
|
-
|
|
8
|
+
OTEL_PROFILING_FILE_PATH,
|
|
8
9
|
)
|
|
9
10
|
from opentelemetry.sdk.extension.profiling.export.result import (
|
|
10
11
|
ProfileExportResult,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class
|
|
15
|
+
class FileProfileExporter:
|
|
15
16
|
def __init__(self, *, directory: str | None = None) -> None:
|
|
16
|
-
self._directory =
|
|
17
|
-
|
|
17
|
+
self._directory = Path(
|
|
18
|
+
directory
|
|
19
|
+
or os.environ.get(OTEL_PROFILING_FILE_PATH, "otel-profiles")
|
|
18
20
|
)
|
|
19
|
-
|
|
20
|
-
self._counter = count(0)
|
|
21
|
-
self._pid = os.getpid()
|
|
21
|
+
self._directory.mkdir(parents=True, exist_ok=True)
|
|
22
22
|
|
|
23
23
|
def export(self, payload: bytes) -> ProfileExportResult:
|
|
24
|
-
filename =
|
|
25
|
-
|
|
24
|
+
filename = (
|
|
25
|
+
self._directory / f"profile-{os.getpid()}-{uuid4().hex}.pprof"
|
|
26
|
+
)
|
|
27
|
+
with filename.open("xb") as handle:
|
|
26
28
|
handle.write(payload)
|
|
27
29
|
return ProfileExportResult.SUCCESS
|
|
28
30
|
|
|
@@ -62,21 +62,18 @@ from opentelemetry.sdk.extension.profiling.environment_variables import (
|
|
|
62
62
|
OTEL_PROFILING_MEMORY_IGNORE_PROFILER,
|
|
63
63
|
OTEL_PROFILING_MEMORY_INTERVAL,
|
|
64
64
|
OTEL_PROFILING_MEMORY_TOP_STATS,
|
|
65
|
-
OTEL_PROFILING_PPROF_UPLOAD_URL,
|
|
66
65
|
OTEL_PROFILING_SAMPLE_INTERVAL,
|
|
67
66
|
)
|
|
68
67
|
from opentelemetry.sdk.extension.profiling.export import (
|
|
69
|
-
CompatiblePPROFExporter,
|
|
70
68
|
create_otlp_profile_exporter,
|
|
71
69
|
)
|
|
72
|
-
from opentelemetry.sdk.extension.profiling.export.
|
|
73
|
-
|
|
70
|
+
from opentelemetry.sdk.extension.profiling.export.file import (
|
|
71
|
+
FileProfileExporter,
|
|
74
72
|
)
|
|
75
73
|
from opentelemetry.sdk.extension.profiling.model.builder import (
|
|
76
74
|
ProfilesRequestBuilder,
|
|
77
75
|
)
|
|
78
76
|
from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
|
|
79
|
-
CompatiblePprofProfileBuilder,
|
|
80
77
|
PprofProfileBuilder,
|
|
81
78
|
)
|
|
82
79
|
from opentelemetry.sdk.extension.profiling.scheduler import ProfileScheduler
|
|
@@ -89,7 +86,7 @@ _DEFAULT_EXPORT_INTERVAL_SECONDS = 60.0
|
|
|
89
86
|
|
|
90
87
|
|
|
91
88
|
class Profiler:
|
|
92
|
-
_active_instance:
|
|
89
|
+
_active_instance: Profiler | None = None
|
|
93
90
|
_active_lock = Lock()
|
|
94
91
|
|
|
95
92
|
def __init__(
|
|
@@ -318,8 +315,7 @@ class Profiler:
|
|
|
318
315
|
active = self.__class__._active_instance
|
|
319
316
|
if active is not None and active is not self and active._started:
|
|
320
317
|
_logger.error(
|
|
321
|
-
"A profiler is already running. Only one profiler "
|
|
322
|
-
"instance can be active at a time."
|
|
318
|
+
"A profiler is already running. Only one profiler instance can be active at a time."
|
|
323
319
|
)
|
|
324
320
|
return
|
|
325
321
|
if self._started:
|
|
@@ -431,25 +427,11 @@ class Profiler:
|
|
|
431
427
|
batch = self._samples.drain()
|
|
432
428
|
|
|
433
429
|
resource = self._resolve_resource()
|
|
434
|
-
start_time_unix_nano = min(
|
|
435
|
-
sample.timestamp_unix_nano for sample in batch
|
|
436
|
-
)
|
|
437
|
-
end_time_unix_nano = max(
|
|
438
|
-
sample.timestamp_unix_nano for sample in batch
|
|
439
|
-
)
|
|
440
430
|
payload = self._builder.build(
|
|
441
431
|
samples=batch,
|
|
442
432
|
resource=resource,
|
|
443
433
|
sample_period_ns=int(self._sample_interval * 1_000_000_000),
|
|
444
434
|
)
|
|
445
|
-
if isinstance(self._exporter, CompatiblePPROFExporter):
|
|
446
|
-
self._exporter.export(
|
|
447
|
-
payload,
|
|
448
|
-
resource=resource,
|
|
449
|
-
start_time_unix_nano=start_time_unix_nano,
|
|
450
|
-
end_time_unix_nano=end_time_unix_nano,
|
|
451
|
-
)
|
|
452
|
-
return
|
|
453
435
|
self._exporter.export(payload)
|
|
454
436
|
|
|
455
437
|
def force_flush(self) -> None:
|
|
@@ -465,27 +447,15 @@ class Profiler:
|
|
|
465
447
|
return Resource.create()
|
|
466
448
|
|
|
467
449
|
def _create_builder(self):
|
|
468
|
-
if isinstance(self._exporter,
|
|
469
|
-
return CompatiblePprofProfileBuilder()
|
|
470
|
-
if isinstance(self._exporter, PPROFProfileExporter):
|
|
450
|
+
if isinstance(self._exporter, FileProfileExporter):
|
|
471
451
|
return PprofProfileBuilder()
|
|
472
452
|
return ProfilesRequestBuilder()
|
|
473
453
|
|
|
474
454
|
@staticmethod
|
|
475
455
|
def _create_exporter_from_env():
|
|
476
|
-
exporter_name = environ.get(OTEL_PROFILING_EXPORTER)
|
|
477
|
-
if exporter_name
|
|
478
|
-
|
|
479
|
-
return CompatiblePPROFExporter()
|
|
480
|
-
return create_otlp_profile_exporter()
|
|
481
|
-
|
|
482
|
-
exporter_name = exporter_name.strip()
|
|
483
|
-
if exporter_name == "none":
|
|
484
|
-
return None
|
|
485
|
-
if exporter_name == "pprof":
|
|
486
|
-
return PPROFProfileExporter()
|
|
487
|
-
if exporter_name == "pprof_http":
|
|
488
|
-
return CompatiblePPROFExporter()
|
|
456
|
+
exporter_name = environ.get(OTEL_PROFILING_EXPORTER, "otlp").strip()
|
|
457
|
+
if exporter_name == "file":
|
|
458
|
+
return FileProfileExporter()
|
|
489
459
|
if exporter_name != "otlp":
|
|
490
460
|
raise ValueError(
|
|
491
461
|
f"Unsupported profiling exporter '{exporter_name}'"
|
|
@@ -593,7 +563,7 @@ class Profiler:
|
|
|
593
563
|
after_fork()
|
|
594
564
|
|
|
595
565
|
@staticmethod
|
|
596
|
-
def _reset_after_fork(component) -> None:
|
|
566
|
+
def _reset_after_fork(component) -> None:
|
|
597
567
|
reset = getattr(component, "reset_after_fork", None)
|
|
598
568
|
if callable(reset):
|
|
599
569
|
reset()
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Generated from beacon/version.properties by beacon/scripts/check-version.py.
|
|
2
|
-
__version__ = "1.2.
|
|
2
|
+
__version__ = "1.2.6"
|
|
@@ -2,18 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from opentelemetry.sdk.extension.profiling.export.
|
|
6
|
-
|
|
5
|
+
from opentelemetry.sdk.extension.profiling.export.file import (
|
|
6
|
+
FileProfileExporter,
|
|
7
7
|
)
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_pprof_exporter_writes_profile(tmp_path: Path):
|
|
11
|
-
exporter =
|
|
11
|
+
exporter = FileProfileExporter(directory=str(tmp_path / "prof"))
|
|
12
12
|
payload = b"pprof-data"
|
|
13
13
|
|
|
14
14
|
result = exporter.export(payload)
|
|
15
15
|
|
|
16
|
-
files = list(tmp_path.glob("
|
|
16
|
+
files = list((tmp_path / "prof").glob("*.pprof"))
|
|
17
17
|
assert files
|
|
18
18
|
assert files[0].read_bytes() == payload
|
|
19
19
|
assert result.name == "SUCCESS"
|
|
@@ -1,73 +1,48 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
PPROFProfileExporter,
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from opentelemetry.sdk.extension.profiling.export.file import (
|
|
6
|
+
FileProfileExporter,
|
|
8
7
|
)
|
|
9
8
|
from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
|
|
10
|
-
CompatiblePprofProfileBuilder,
|
|
11
9
|
PprofProfileBuilder,
|
|
12
10
|
)
|
|
13
11
|
from opentelemetry.sdk.extension.profiling.runtime import Profiler
|
|
14
12
|
|
|
15
13
|
|
|
16
|
-
def
|
|
17
|
-
monkeypatch,
|
|
18
|
-
):
|
|
19
|
-
monkeypatch.delenv("OTEL_PROFILING_EXPORTER", raising=False)
|
|
20
|
-
monkeypatch.setenv(
|
|
21
|
-
"OTEL_PROFILING_PPROF_UPLOAD_URL",
|
|
22
|
-
"http://collector:9529/profiling/v1/input",
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
exporter = Profiler._create_exporter_from_env()
|
|
26
|
-
|
|
27
|
-
assert isinstance(exporter, CompatiblePPROFExporter)
|
|
28
|
-
exporter.shutdown()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_runtime_explicit_otlp_exporter_overrides_env_fallback(
|
|
32
|
-
monkeypatch,
|
|
33
|
-
):
|
|
14
|
+
def test_runtime_defaults_to_otlp(monkeypatch):
|
|
34
15
|
sentinel = object()
|
|
35
|
-
monkeypatch.
|
|
16
|
+
monkeypatch.delenv("OTEL_PROFILING_EXPORTER", raising=False)
|
|
36
17
|
monkeypatch.setenv(
|
|
37
|
-
"OTEL_PROFILING_PPROF_UPLOAD_URL",
|
|
38
|
-
"http://collector:9529/profiling/v1/input",
|
|
18
|
+
"OTEL_PROFILING_PPROF_UPLOAD_URL", "invalid old endpoint"
|
|
39
19
|
)
|
|
40
20
|
monkeypatch.setattr(
|
|
41
21
|
"opentelemetry.sdk.extension.profiling.runtime.create_otlp_profile_exporter",
|
|
42
22
|
lambda: sentinel,
|
|
43
23
|
)
|
|
44
|
-
|
|
45
|
-
exporter = Profiler._create_exporter_from_env()
|
|
46
|
-
|
|
47
|
-
assert exporter is sentinel
|
|
24
|
+
assert Profiler._create_exporter_from_env() is sentinel
|
|
48
25
|
|
|
49
26
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
def test_file_ignores_all_endpoint_settings(monkeypatch, tmp_path):
|
|
28
|
+
monkeypatch.setenv("OTEL_PROFILING_EXPORTER", "file")
|
|
29
|
+
monkeypatch.setenv("OTEL_PROFILING_FILE_PATH", str(tmp_path))
|
|
30
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT", "not a URL")
|
|
31
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL", "invalid")
|
|
32
|
+
exporter = Profiler._create_exporter_from_env()
|
|
33
|
+
assert isinstance(exporter, FileProfileExporter)
|
|
34
|
+
assert isinstance(
|
|
35
|
+
Profiler(exporter=exporter)._builder, PprofProfileBuilder
|
|
53
36
|
)
|
|
37
|
+
exporter.export(b"profile")
|
|
38
|
+
assert next(tmp_path.glob("*.pprof")).read_bytes() == b"profile"
|
|
54
39
|
|
|
55
|
-
try:
|
|
56
|
-
assert isinstance(profiler._builder, CompatiblePprofProfileBuilder)
|
|
57
|
-
finally:
|
|
58
|
-
profiler._exporter.shutdown()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_runtime_uses_standard_pprof_builder_for_file_exporter(tmp_path):
|
|
62
|
-
profiler = Profiler(
|
|
63
|
-
exporter=PPROFProfileExporter(directory=str(tmp_path / "prof"))
|
|
64
|
-
)
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
41
|
+
@pytest.mark.parametrize("name", ["datakit", "pprof", "pprof_http", "none"])
|
|
42
|
+
def test_removed_exporter_names_rejected(monkeypatch, name):
|
|
43
|
+
monkeypatch.setenv("OTEL_PROFILING_EXPORTER", name)
|
|
44
|
+
with pytest.raises(ValueError, match="Unsupported profiling exporter"):
|
|
45
|
+
Profiler._create_exporter_from_env()
|
|
71
46
|
|
|
72
47
|
|
|
73
48
|
def test_runtime_defaults_to_sixty_second_intervals(monkeypatch):
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import binascii
|
|
4
|
-
import datetime
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
import platform
|
|
9
|
-
from collections import deque
|
|
10
|
-
from itertools import count
|
|
11
|
-
from os import environ
|
|
12
|
-
from typing import Dict, Optional
|
|
13
|
-
from uuid import uuid4
|
|
14
|
-
|
|
15
|
-
import requests
|
|
16
|
-
|
|
17
|
-
from opentelemetry.sdk.extension.profiling.environment_variables import (
|
|
18
|
-
OTEL_PROFILING_PPROF_HEADERS,
|
|
19
|
-
OTEL_PROFILING_PPROF_MAX_FILES,
|
|
20
|
-
OTEL_PROFILING_PPROF_PATH,
|
|
21
|
-
OTEL_PROFILING_PPROF_UPLOAD_URL,
|
|
22
|
-
)
|
|
23
|
-
from opentelemetry.sdk.extension.profiling.export.result import (
|
|
24
|
-
ProfileExportResult,
|
|
25
|
-
)
|
|
26
|
-
from opentelemetry.sdk.extension.profiling.version import __version__
|
|
27
|
-
from opentelemetry.sdk.resources import Resource
|
|
28
|
-
|
|
29
|
-
_logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
_DEFAULT_AGENT_BASE_URL = "http://localhost:8126"
|
|
32
|
-
_DEFAULT_AGENT_PATH = "profiling/v1/input"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class PPROFHTTPExporter:
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
*,
|
|
39
|
-
upload_url: str | None = None,
|
|
40
|
-
headers: Optional[Dict[str, str]] = None,
|
|
41
|
-
timeout: float = 10.0,
|
|
42
|
-
session: Optional[requests.Session] = None,
|
|
43
|
-
pprof_path: str | None = None,
|
|
44
|
-
max_files: int | None = None,
|
|
45
|
-
service: str | None = None,
|
|
46
|
-
env_name: str | None = None,
|
|
47
|
-
version: str | None = None,
|
|
48
|
-
) -> None:
|
|
49
|
-
self._url = upload_url or _resolve_upload_url()
|
|
50
|
-
self._headers = _resolve_headers(headers)
|
|
51
|
-
self._timeout = timeout
|
|
52
|
-
self._session = session or requests.Session()
|
|
53
|
-
self._pprof_path = (
|
|
54
|
-
pprof_path
|
|
55
|
-
if pprof_path is not None
|
|
56
|
-
else environ.get(OTEL_PROFILING_PPROF_PATH)
|
|
57
|
-
)
|
|
58
|
-
self._max_files = (
|
|
59
|
-
max_files
|
|
60
|
-
if max_files is not None
|
|
61
|
-
else int(environ.get(OTEL_PROFILING_PPROF_MAX_FILES, "3"))
|
|
62
|
-
)
|
|
63
|
-
if self._max_files < 1:
|
|
64
|
-
raise ValueError("max_files must be >= 1")
|
|
65
|
-
self._written_paths: deque[str] = deque()
|
|
66
|
-
self._service = service
|
|
67
|
-
self._env = env_name
|
|
68
|
-
self._version = version
|
|
69
|
-
self._runtime_id = uuid4().hex
|
|
70
|
-
self._host = platform.node()
|
|
71
|
-
self._runtime = platform.python_implementation()
|
|
72
|
-
self._runtime_version = platform.python_version()
|
|
73
|
-
self._pid = os.getpid()
|
|
74
|
-
self._counter = count()
|
|
75
|
-
|
|
76
|
-
def export(
|
|
77
|
-
self,
|
|
78
|
-
payload: bytes,
|
|
79
|
-
*,
|
|
80
|
-
resource: Resource | None = None,
|
|
81
|
-
start_time_unix_nano: int | None = None,
|
|
82
|
-
end_time_unix_nano: int | None = None,
|
|
83
|
-
) -> ProfileExportResult:
|
|
84
|
-
try:
|
|
85
|
-
filename = self._write_payload(payload)
|
|
86
|
-
except OSError as exc:
|
|
87
|
-
# Local copies are diagnostic output, not a prerequisite for
|
|
88
|
-
# upload. Read-only/full disks must not discard a valid profile.
|
|
89
|
-
_logger.warning("Cannot save local pprof payload: %s", exc)
|
|
90
|
-
filename = None
|
|
91
|
-
try:
|
|
92
|
-
request_headers = dict(self._headers)
|
|
93
|
-
content_type, body = self._build_request(
|
|
94
|
-
payload=payload,
|
|
95
|
-
resource=resource,
|
|
96
|
-
start_time_unix_nano=start_time_unix_nano,
|
|
97
|
-
end_time_unix_nano=end_time_unix_nano,
|
|
98
|
-
)
|
|
99
|
-
request_headers["Content-Type"] = content_type
|
|
100
|
-
response = self._session.post(
|
|
101
|
-
url=self._url,
|
|
102
|
-
data=body,
|
|
103
|
-
headers=request_headers,
|
|
104
|
-
timeout=self._timeout,
|
|
105
|
-
)
|
|
106
|
-
except Exception as exc:
|
|
107
|
-
_logger.error("pprof HTTP export failed: %s", exc)
|
|
108
|
-
return ProfileExportResult.FAILURE
|
|
109
|
-
|
|
110
|
-
if response.status_code not in (200, 202):
|
|
111
|
-
_logger.error(
|
|
112
|
-
"pprof HTTP export failed status %s: %s",
|
|
113
|
-
response.status_code,
|
|
114
|
-
response.text,
|
|
115
|
-
)
|
|
116
|
-
return ProfileExportResult.FAILURE
|
|
117
|
-
|
|
118
|
-
_logger.debug(
|
|
119
|
-
"pprof payload saved to %s and posted to %s",
|
|
120
|
-
filename,
|
|
121
|
-
self._url,
|
|
122
|
-
)
|
|
123
|
-
return ProfileExportResult.SUCCESS
|
|
124
|
-
|
|
125
|
-
def _build_request(
|
|
126
|
-
self,
|
|
127
|
-
*,
|
|
128
|
-
payload: bytes,
|
|
129
|
-
resource: Resource | None,
|
|
130
|
-
start_time_unix_nano: int | None,
|
|
131
|
-
end_time_unix_nano: int | None,
|
|
132
|
-
) -> tuple[str, bytes]:
|
|
133
|
-
service = self._resolve_service(resource)
|
|
134
|
-
event = {
|
|
135
|
-
"version": "4",
|
|
136
|
-
"family": "python",
|
|
137
|
-
"attachments": ["auto.pprof"],
|
|
138
|
-
"tags_profiler": self._format_tags(service, resource),
|
|
139
|
-
"start": _format_timestamp(
|
|
140
|
-
start_time_unix_nano
|
|
141
|
-
or datetime.datetime.now(tz=datetime.timezone.utc).timestamp()
|
|
142
|
-
* 1_000_000_000
|
|
143
|
-
),
|
|
144
|
-
"end": _format_timestamp(
|
|
145
|
-
end_time_unix_nano
|
|
146
|
-
or start_time_unix_nano
|
|
147
|
-
or datetime.datetime.now(tz=datetime.timezone.utc).timestamp()
|
|
148
|
-
* 1_000_000_000
|
|
149
|
-
),
|
|
150
|
-
}
|
|
151
|
-
files = [
|
|
152
|
-
{
|
|
153
|
-
"name": "auto",
|
|
154
|
-
"filename": "auto.pprof",
|
|
155
|
-
"content_type": "application/octet-stream",
|
|
156
|
-
"data": payload,
|
|
157
|
-
}
|
|
158
|
-
]
|
|
159
|
-
return _encode_multipart_formdata(event=event, files=files)
|
|
160
|
-
|
|
161
|
-
def _format_tags(self, service: str, resource: Resource | None) -> str:
|
|
162
|
-
tags = {
|
|
163
|
-
"service": service,
|
|
164
|
-
"runtime-id": self._runtime_id,
|
|
165
|
-
"host": self._host,
|
|
166
|
-
"language": "python",
|
|
167
|
-
"runtime": self._runtime,
|
|
168
|
-
"runtime_version": self._runtime_version,
|
|
169
|
-
"profiler_version": __version__,
|
|
170
|
-
}
|
|
171
|
-
env_name = self._resolve_env(resource)
|
|
172
|
-
if env_name:
|
|
173
|
-
tags["env"] = env_name
|
|
174
|
-
version = self._resolve_version(resource)
|
|
175
|
-
if version:
|
|
176
|
-
tags["version"] = version
|
|
177
|
-
return ",".join(f"{key}:{value}" for key, value in tags.items())
|
|
178
|
-
|
|
179
|
-
def _resolve_service(self, resource: Resource | None) -> str:
|
|
180
|
-
return (
|
|
181
|
-
_resource_attribute(resource, "service.name")
|
|
182
|
-
or self._service
|
|
183
|
-
or "unknown_service"
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
def _resolve_env(self, resource: Resource | None) -> str | None:
|
|
187
|
-
return (
|
|
188
|
-
_resource_attribute(
|
|
189
|
-
resource,
|
|
190
|
-
"deployment.environment.name",
|
|
191
|
-
"deployment.environment",
|
|
192
|
-
)
|
|
193
|
-
or self._env
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
def _resolve_version(self, resource: Resource | None) -> str | None:
|
|
197
|
-
return (
|
|
198
|
-
_resource_attribute(resource, "service.version") or self._version
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
def _write_payload(self, payload: bytes) -> str | None:
|
|
202
|
-
if not self._pprof_path:
|
|
203
|
-
return None
|
|
204
|
-
filename = (
|
|
205
|
-
f"{self._pprof_path}.{self._pid}.{next(self._counter)}.pprof"
|
|
206
|
-
)
|
|
207
|
-
os.makedirs(os.path.dirname(filename) or ".", exist_ok=True)
|
|
208
|
-
with open(filename, "wb") as handle:
|
|
209
|
-
handle.write(payload)
|
|
210
|
-
self._written_paths.append(filename)
|
|
211
|
-
while len(self._written_paths) > self._max_files:
|
|
212
|
-
previous = self._written_paths.popleft()
|
|
213
|
-
try:
|
|
214
|
-
os.remove(previous)
|
|
215
|
-
except OSError as exc:
|
|
216
|
-
_logger.warning("Cannot remove local pprof payload: %s", exc)
|
|
217
|
-
return filename
|
|
218
|
-
|
|
219
|
-
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
220
|
-
del timeout_millis
|
|
221
|
-
return True
|
|
222
|
-
|
|
223
|
-
def shutdown(self) -> None:
|
|
224
|
-
self._session.close()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _resolve_headers(
|
|
228
|
-
headers: Optional[Dict[str, str]],
|
|
229
|
-
) -> Dict[str, str]:
|
|
230
|
-
resolved = _parse_headers(environ.get(OTEL_PROFILING_PPROF_HEADERS, ""))
|
|
231
|
-
if headers is not None:
|
|
232
|
-
resolved.update(headers)
|
|
233
|
-
return resolved
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _resolve_upload_url() -> str:
|
|
237
|
-
explicit = environ.get(OTEL_PROFILING_PPROF_UPLOAD_URL)
|
|
238
|
-
if explicit:
|
|
239
|
-
return explicit
|
|
240
|
-
return f"{_DEFAULT_AGENT_BASE_URL}/{_DEFAULT_AGENT_PATH}"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _parse_headers(value: str) -> Dict[str, str]:
|
|
244
|
-
headers = {}
|
|
245
|
-
for header in value.split(","):
|
|
246
|
-
if ":" not in header:
|
|
247
|
-
continue
|
|
248
|
-
key, header_value = header.split(":", 1)
|
|
249
|
-
key = key.strip()
|
|
250
|
-
header_value = header_value.strip()
|
|
251
|
-
if key:
|
|
252
|
-
headers[key] = header_value
|
|
253
|
-
return headers
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def _resource_attribute(resource: Resource | None, *keys: str) -> str | None:
|
|
257
|
-
if resource is None:
|
|
258
|
-
return None
|
|
259
|
-
for key in keys:
|
|
260
|
-
value = resource.attributes.get(key)
|
|
261
|
-
if isinstance(value, str) and value:
|
|
262
|
-
return value
|
|
263
|
-
return None
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _format_timestamp(timestamp_unix_nano: float | int) -> str:
|
|
267
|
-
timestamp = datetime.datetime.fromtimestamp(
|
|
268
|
-
float(timestamp_unix_nano) / 1e9,
|
|
269
|
-
tz=datetime.timezone.utc,
|
|
270
|
-
)
|
|
271
|
-
return timestamp.replace(microsecond=0).isoformat()[0:-6] + "Z"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _encode_multipart_formdata(
|
|
275
|
-
*,
|
|
276
|
-
event: dict[str, str | list[str]],
|
|
277
|
-
files: list[dict[str, str | bytes]],
|
|
278
|
-
) -> tuple[str, bytes]:
|
|
279
|
-
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
|
280
|
-
boundary_bytes = boundary.encode("ascii")
|
|
281
|
-
body = (
|
|
282
|
-
(b"--%s\r\n" % boundary_bytes)
|
|
283
|
-
+ b'Content-Disposition: form-data; name="event"; filename="event.json"\r\n'
|
|
284
|
-
+ b"Content-Type: application/json\r\n\r\n"
|
|
285
|
-
+ _encode_json(event)
|
|
286
|
-
+ b"\r\n"
|
|
287
|
-
+ b"".join(
|
|
288
|
-
(b"--%s\r\n" % boundary_bytes)
|
|
289
|
-
+ (
|
|
290
|
-
b'Content-Disposition: form-data; name="%s"; filename="%s"\r\n'
|
|
291
|
-
% (
|
|
292
|
-
str(item["name"]).encode("utf-8"),
|
|
293
|
-
str(item["filename"]).encode("utf-8"),
|
|
294
|
-
)
|
|
295
|
-
)
|
|
296
|
-
+ (
|
|
297
|
-
b"Content-Type: %s\r\n\r\n"
|
|
298
|
-
% str(item["content_type"]).encode("utf-8")
|
|
299
|
-
)
|
|
300
|
-
+ bytes(item["data"])
|
|
301
|
-
+ b"\r\n"
|
|
302
|
-
for item in files
|
|
303
|
-
)
|
|
304
|
-
+ b"--%s--\r\n" % boundary_bytes
|
|
305
|
-
)
|
|
306
|
-
return f"multipart/form-data; boundary={boundary}", body
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _encode_json(payload: dict[str, str | list[str]]) -> bytes:
|
|
310
|
-
return json.dumps(payload).encode("utf-8")
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import errno
|
|
4
|
-
from unittest.mock import Mock
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from opentelemetry.sdk.extension.profiling.export.pprof_http import (
|
|
9
|
-
PPROFHTTPExporter,
|
|
10
|
-
)
|
|
11
|
-
from opentelemetry.sdk.resources import Resource
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_pprof_http_exporter_posts_multipart_payload(monkeypatch, tmp_path):
|
|
15
|
-
session = Mock()
|
|
16
|
-
session.post.return_value = Mock(status_code=202, text="ok")
|
|
17
|
-
monkeypatch.setenv(
|
|
18
|
-
"OTEL_PROFILING_PPROF_HEADERS",
|
|
19
|
-
"X-API-Key:api-key",
|
|
20
|
-
)
|
|
21
|
-
monkeypatch.setenv(
|
|
22
|
-
"OTEL_PROFILING_PPROF_UPLOAD_URL",
|
|
23
|
-
"http://agent:8126/profiling/v1/input",
|
|
24
|
-
)
|
|
25
|
-
exporter = PPROFHTTPExporter(
|
|
26
|
-
session=session,
|
|
27
|
-
pprof_path=str(tmp_path / "prof"),
|
|
28
|
-
)
|
|
29
|
-
payload = b"foo"
|
|
30
|
-
resource = Resource.create(
|
|
31
|
-
{
|
|
32
|
-
"service.name": "svc",
|
|
33
|
-
"service.version": "1.2.3",
|
|
34
|
-
"deployment.environment.name": "prod",
|
|
35
|
-
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
result = exporter.export(
|
|
39
|
-
payload,
|
|
40
|
-
resource=resource,
|
|
41
|
-
start_time_unix_nano=1_700_000_000_000_000_000,
|
|
42
|
-
end_time_unix_nano=1_700_000_060_000_000_000,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
assert result.name == "SUCCESS"
|
|
46
|
-
session.post.assert_called_once()
|
|
47
|
-
kwargs = session.post.call_args.kwargs
|
|
48
|
-
assert kwargs["url"] == "http://agent:8126/profiling/v1/input"
|
|
49
|
-
assert kwargs["headers"]["X-API-Key"] == "api-key"
|
|
50
|
-
assert kwargs["headers"]["Content-Type"].startswith(
|
|
51
|
-
"multipart/form-data; boundary="
|
|
52
|
-
)
|
|
53
|
-
body = kwargs["data"]
|
|
54
|
-
assert b'filename="event.json"' in body
|
|
55
|
-
assert b'filename="auto.pprof"' in body
|
|
56
|
-
assert b'"family": "python"' in body
|
|
57
|
-
assert b"service:svc" in body
|
|
58
|
-
assert b"env:prod" in body
|
|
59
|
-
assert b"version:1.2.3" in body
|
|
60
|
-
assert payload in body
|
|
61
|
-
files = list(tmp_path.glob("prof*.pprof"))
|
|
62
|
-
assert files
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_pprof_http_exporter_handles_failure(monkeypatch, tmp_path):
|
|
66
|
-
session = Mock()
|
|
67
|
-
session.post.side_effect = Exception("boom")
|
|
68
|
-
exporter = PPROFHTTPExporter(
|
|
69
|
-
session=session,
|
|
70
|
-
pprof_path=str(tmp_path / "prof"),
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
result = exporter.export(b"bar")
|
|
74
|
-
|
|
75
|
-
assert result.name == "FAILURE"
|
|
76
|
-
files = list(tmp_path.glob("prof*.pprof"))
|
|
77
|
-
assert files
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_http_upload_does_not_require_a_writable_directory(
|
|
81
|
-
monkeypatch, tmp_path
|
|
82
|
-
) -> None:
|
|
83
|
-
monkeypatch.chdir(tmp_path)
|
|
84
|
-
monkeypatch.delenv("OTEL_PROFILING_PPROF_PATH", raising=False)
|
|
85
|
-
session = Mock()
|
|
86
|
-
session.post.return_value = Mock(status_code=202)
|
|
87
|
-
exporter = PPROFHTTPExporter(session=session)
|
|
88
|
-
assert exporter.export(b"payload").name == "SUCCESS"
|
|
89
|
-
assert list(tmp_path.iterdir()) == []
|
|
90
|
-
session.post.assert_called_once()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@pytest.mark.parametrize("error_number", [errno.EACCES, errno.ENOSPC])
|
|
94
|
-
def test_local_write_failure_still_uploads_and_recovers(
|
|
95
|
-
monkeypatch, tmp_path, error_number: int, caplog
|
|
96
|
-
) -> None:
|
|
97
|
-
session = Mock()
|
|
98
|
-
session.post.return_value = Mock(status_code=202)
|
|
99
|
-
exporter = PPROFHTTPExporter(
|
|
100
|
-
session=session, pprof_path=str(tmp_path / "profile")
|
|
101
|
-
)
|
|
102
|
-
write = exporter._write_payload
|
|
103
|
-
|
|
104
|
-
def fail(payload: bytes) -> None:
|
|
105
|
-
raise OSError(error_number, "write unavailable")
|
|
106
|
-
|
|
107
|
-
monkeypatch.setattr(exporter, "_write_payload", fail)
|
|
108
|
-
assert exporter.export(b"first").name == "SUCCESS"
|
|
109
|
-
assert "Cannot save local pprof" in caplog.text
|
|
110
|
-
monkeypatch.setattr(exporter, "_write_payload", write)
|
|
111
|
-
assert exporter.export(b"second").name == "SUCCESS"
|
|
112
|
-
assert session.post.call_count == 2
|
|
113
|
-
assert next(tmp_path.glob("*.pprof")).read_bytes() == b"second"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_http_local_copies_have_bounded_instance_retention(
|
|
117
|
-
monkeypatch, tmp_path
|
|
118
|
-
) -> None:
|
|
119
|
-
session = Mock()
|
|
120
|
-
session.post.return_value = Mock(status_code=202)
|
|
121
|
-
monkeypatch.setenv("OTEL_PROFILING_PPROF_MAX_FILES", "2")
|
|
122
|
-
exporter = PPROFHTTPExporter(
|
|
123
|
-
session=session, pprof_path=str(tmp_path / "profile")
|
|
124
|
-
)
|
|
125
|
-
other = tmp_path / "profile.other-process.pprof"
|
|
126
|
-
other.write_bytes(b"keep")
|
|
127
|
-
for index in range(5):
|
|
128
|
-
assert exporter.export(str(index).encode()).name == "SUCCESS"
|
|
129
|
-
copies = [path for path in tmp_path.glob("*.pprof") if path != other]
|
|
130
|
-
assert {path.read_bytes() for path in copies} == {b"3", b"4"}
|
|
131
|
-
assert other.read_bytes() == b"keep"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def test_explicit_empty_path_disables_environment_local_copies(
|
|
135
|
-
monkeypatch, tmp_path
|
|
136
|
-
) -> None:
|
|
137
|
-
monkeypatch.setenv("OTEL_PROFILING_PPROF_PATH", str(tmp_path / "profile"))
|
|
138
|
-
session = Mock()
|
|
139
|
-
session.post.return_value = Mock(status_code=202)
|
|
140
|
-
exporter = PPROFHTTPExporter(session=session, pprof_path="")
|
|
141
|
-
assert exporter.export(b"payload").name == "SUCCESS"
|
|
142
|
-
assert list(tmp_path.iterdir()) == []
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_build_failure_returns_failure_and_next_export_recovers(
|
|
146
|
-
monkeypatch,
|
|
147
|
-
) -> None:
|
|
148
|
-
session = Mock()
|
|
149
|
-
session.post.return_value = Mock(status_code=202)
|
|
150
|
-
exporter = PPROFHTTPExporter(session=session, pprof_path="")
|
|
151
|
-
build = exporter._build_request
|
|
152
|
-
monkeypatch.setattr(
|
|
153
|
-
exporter, "_build_request", Mock(side_effect=ValueError("bad build"))
|
|
154
|
-
)
|
|
155
|
-
assert exporter.export(b"first").name == "FAILURE"
|
|
156
|
-
session.post.assert_not_called()
|
|
157
|
-
monkeypatch.setattr(exporter, "_build_request", build)
|
|
158
|
-
assert exporter.export(b"second").name == "SUCCESS"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beacon_profiling-1.2.3 → beacon_profiling-1.2.6}/tests/test_auto_instrumentation_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|