beacon-profiling 1.2.6__tar.gz → 1.2.7__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.6 → beacon_profiling-1.2.7}/PKG-INFO +21 -18
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/README.rst +20 -17
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/environment_variables.py +5 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/__init__.py +4 -1
- beacon_profiling-1.2.7/src/opentelemetry/sdk/extension/profiling/export/pprof_http.py +313 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/runtime.py +19 -4
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/version.py +1 -1
- beacon_profiling-1.2.7/tests/test_compatible_runtime_http.py +75 -0
- beacon_profiling-1.2.7/tests/test_pprof_http_exporter.py +158 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_runtime_exporter_selection.py +20 -10
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/.gitignore +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/LICENSE +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/pyproject.toml +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/__init__.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/bootstrap.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/__init__.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/base.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/buffering.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/exception.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/lock.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/memory.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/stack.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/context_bridge.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/file.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/grpc.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/http.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/pprof_pb2.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/result.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/__init__.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/builder.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/pprof_builder.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/scheduler.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_asyncio_context_bridge.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_asyncio_lock_collector.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_asyncio_lock_counts.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_auto_instrumentation_integration.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_bootstrap.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_buffer_budget.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_builder.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_context_bridge.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_dictionary_contract.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_exception_collector.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_exporters.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_fork_runtime.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_local_root_context.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_lock_collector.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_lock_sampling.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_memory_collector.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_package_metadata.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_pprof_builder.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_pprof_exporter.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_profiler_integration.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_protobuf_environment.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_runtime_scheduling.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_scheduler_resilience.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/tests/test_stack_collector.py +0 -0
- {beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/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.7
|
|
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
|
|
@@ -77,7 +77,12 @@ Configuration
|
|
|
77
77
|
The following environment variables are supported:
|
|
78
78
|
|
|
79
79
|
* ``OTEL_PROFILING_ENABLED``
|
|
80
|
-
* ``OTEL_PROFILING_EXPORTER`` (``
|
|
80
|
+
* ``OTEL_PROFILING_EXPORTER`` (``http`` or ``file``; defaults to ``http``;
|
|
81
|
+
legacy ``datakit`` and ``pprof_http`` names remain aliases)
|
|
82
|
+
* ``OTEL_PROFILING_ENDPOINT`` (defaults to
|
|
83
|
+
``http://127.0.0.1:9529/profiling/v1/input``)
|
|
84
|
+
* ``OTEL_PROFILING_PPROF_UPLOAD_URL`` (legacy explicit endpoint; takes precedence)
|
|
85
|
+
* ``OTEL_PROFILING_PPROF_HEADERS`` (legacy upload headers)
|
|
81
86
|
* ``OTEL_PROFILING_SAMPLE_INTERVAL``
|
|
82
87
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
83
88
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
@@ -137,8 +142,8 @@ periodic memory scheduling uses the positive export cadence instead of spinning.
|
|
|
137
142
|
Examples
|
|
138
143
|
--------
|
|
139
144
|
|
|
140
|
-
Collect stack, exception, lock, and heap profiles and
|
|
141
|
-
|
|
145
|
+
Collect stack, exception, lock, and heap profiles and upload them over HTTP
|
|
146
|
+
using the existing DataKit-compatible multipart/pprof format:
|
|
142
147
|
|
|
143
148
|
::
|
|
144
149
|
|
|
@@ -146,11 +151,11 @@ OTLP/HTTP:
|
|
|
146
151
|
export OTEL_PROFILING_EXCEPTION_ENABLED=true
|
|
147
152
|
export OTEL_PROFILING_LOCK_ENABLED=true
|
|
148
153
|
export OTEL_PROFILING_MEMORY_ENABLED=true
|
|
149
|
-
export
|
|
150
|
-
export
|
|
154
|
+
export OTEL_PROFILING_EXPORTER=http
|
|
155
|
+
export OTEL_PROFILING_ENDPOINT=http://127.0.0.1:9529/profiling/v1/input
|
|
151
156
|
beacon python app.py
|
|
152
157
|
|
|
153
|
-
To dump pprof files instead of
|
|
158
|
+
To dump pprof files instead of uploading them, set:
|
|
154
159
|
|
|
155
160
|
::
|
|
156
161
|
|
|
@@ -160,15 +165,13 @@ To dump pprof files instead of OTLP, set:
|
|
|
160
165
|
The ``file`` exporter ignores all OTLP endpoint and transport settings.
|
|
161
166
|
It writes gzip pprof files to the configured directory, without network uploads.
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
The ``http`` configuration name selects the existing multipart/pprof uploader.
|
|
169
|
+
The ``otlp`` exporter name is reserved for future OTLP Profiles support and is
|
|
170
|
+
currently rejected by environment-based selection. Sampling and profile encoding
|
|
171
|
+
are unchanged. This uploader does not inherit the trace exporter's protocol or shared
|
|
172
|
+
OTLP endpoint. A gRPC traces endpoint is not a profiling upload endpoint.
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
174
|
+
The ``OTEL_EXPORTER_OTLP_PROFILES_*`` settings above apply only when explicitly
|
|
175
|
+
constructing the low-level OTLP exporters; the environment-selected compatible
|
|
176
|
+
uploader uses ``OTEL_PROFILING_ENDPOINT``. These source changes require a new
|
|
177
|
+
Beacon release.
|
|
@@ -50,7 +50,12 @@ Configuration
|
|
|
50
50
|
The following environment variables are supported:
|
|
51
51
|
|
|
52
52
|
* ``OTEL_PROFILING_ENABLED``
|
|
53
|
-
* ``OTEL_PROFILING_EXPORTER`` (``
|
|
53
|
+
* ``OTEL_PROFILING_EXPORTER`` (``http`` or ``file``; defaults to ``http``;
|
|
54
|
+
legacy ``datakit`` and ``pprof_http`` names remain aliases)
|
|
55
|
+
* ``OTEL_PROFILING_ENDPOINT`` (defaults to
|
|
56
|
+
``http://127.0.0.1:9529/profiling/v1/input``)
|
|
57
|
+
* ``OTEL_PROFILING_PPROF_UPLOAD_URL`` (legacy explicit endpoint; takes precedence)
|
|
58
|
+
* ``OTEL_PROFILING_PPROF_HEADERS`` (legacy upload headers)
|
|
54
59
|
* ``OTEL_PROFILING_SAMPLE_INTERVAL``
|
|
55
60
|
* ``OTEL_PROFILING_EXPORT_INTERVAL`` (defaults to 60 seconds and can be
|
|
56
61
|
overridden by the ``Profiler(export_interval=...)`` argument)
|
|
@@ -110,8 +115,8 @@ periodic memory scheduling uses the positive export cadence instead of spinning.
|
|
|
110
115
|
Examples
|
|
111
116
|
--------
|
|
112
117
|
|
|
113
|
-
Collect stack, exception, lock, and heap profiles and
|
|
114
|
-
|
|
118
|
+
Collect stack, exception, lock, and heap profiles and upload them over HTTP
|
|
119
|
+
using the existing DataKit-compatible multipart/pprof format:
|
|
115
120
|
|
|
116
121
|
::
|
|
117
122
|
|
|
@@ -119,11 +124,11 @@ OTLP/HTTP:
|
|
|
119
124
|
export OTEL_PROFILING_EXCEPTION_ENABLED=true
|
|
120
125
|
export OTEL_PROFILING_LOCK_ENABLED=true
|
|
121
126
|
export OTEL_PROFILING_MEMORY_ENABLED=true
|
|
122
|
-
export
|
|
123
|
-
export
|
|
127
|
+
export OTEL_PROFILING_EXPORTER=http
|
|
128
|
+
export OTEL_PROFILING_ENDPOINT=http://127.0.0.1:9529/profiling/v1/input
|
|
124
129
|
beacon python app.py
|
|
125
130
|
|
|
126
|
-
To dump pprof files instead of
|
|
131
|
+
To dump pprof files instead of uploading them, set:
|
|
127
132
|
|
|
128
133
|
::
|
|
129
134
|
|
|
@@ -133,15 +138,13 @@ To dump pprof files instead of OTLP, set:
|
|
|
133
138
|
The ``file`` exporter ignores all OTLP endpoint and transport settings.
|
|
134
139
|
It writes gzip pprof files to the configured directory, without network uploads.
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
The ``http`` configuration name selects the existing multipart/pprof uploader.
|
|
142
|
+
The ``otlp`` exporter name is reserved for future OTLP Profiles support and is
|
|
143
|
+
currently rejected by environment-based selection. Sampling and profile encoding
|
|
144
|
+
are unchanged. This uploader does not inherit the trace exporter's protocol or shared
|
|
145
|
+
OTLP endpoint. A gRPC traces endpoint is not a profiling upload endpoint.
|
|
137
146
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
147
|
+
The ``OTEL_EXPORTER_OTLP_PROFILES_*`` settings above apply only when explicitly
|
|
148
|
+
constructing the low-level OTLP exporters; the environment-selected compatible
|
|
149
|
+
uploader uses ``OTEL_PROFILING_ENDPOINT``. These source changes require a new
|
|
150
|
+
Beacon release.
|
|
@@ -35,6 +35,11 @@ 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
37
|
OTEL_PROFILING_FILE_PATH = "OTEL_PROFILING_FILE_PATH"
|
|
38
|
+
OTEL_PROFILING_ENDPOINT = "OTEL_PROFILING_ENDPOINT"
|
|
39
|
+
OTEL_PROFILING_PPROF_PATH = "OTEL_PROFILING_PPROF_PATH"
|
|
40
|
+
OTEL_PROFILING_PPROF_MAX_FILES = "OTEL_PROFILING_PPROF_MAX_FILES"
|
|
41
|
+
OTEL_PROFILING_PPROF_UPLOAD_URL = "OTEL_PROFILING_PPROF_UPLOAD_URL"
|
|
42
|
+
OTEL_PROFILING_PPROF_HEADERS = "OTEL_PROFILING_PPROF_HEADERS"
|
|
38
43
|
|
|
39
44
|
OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL = "OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL"
|
|
40
45
|
OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT = "OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT"
|
|
@@ -12,8 +12,11 @@ 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
|
+
)
|
|
15
18
|
|
|
16
|
-
__all__ = ("create_otlp_profile_exporter"
|
|
19
|
+
__all__ = ("CompatiblePPROFExporter", "create_otlp_profile_exporter")
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def create_otlp_profile_exporter():
|
|
@@ -0,0 +1,313 @@
|
|
|
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_ENDPOINT,
|
|
19
|
+
OTEL_PROFILING_PPROF_HEADERS,
|
|
20
|
+
OTEL_PROFILING_PPROF_MAX_FILES,
|
|
21
|
+
OTEL_PROFILING_PPROF_PATH,
|
|
22
|
+
OTEL_PROFILING_PPROF_UPLOAD_URL,
|
|
23
|
+
)
|
|
24
|
+
from opentelemetry.sdk.extension.profiling.export.result import (
|
|
25
|
+
ProfileExportResult,
|
|
26
|
+
)
|
|
27
|
+
from opentelemetry.sdk.extension.profiling.version import __version__
|
|
28
|
+
from opentelemetry.sdk.resources import Resource
|
|
29
|
+
|
|
30
|
+
_logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_DEFAULT_AGENT_BASE_URL = "http://127.0.0.1:9529"
|
|
33
|
+
_DEFAULT_AGENT_PATH = "profiling/v1/input"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PPROFHTTPExporter:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
upload_url: str | None = None,
|
|
41
|
+
headers: Optional[Dict[str, str]] = None,
|
|
42
|
+
timeout: float = 10.0,
|
|
43
|
+
session: Optional[requests.Session] = None,
|
|
44
|
+
pprof_path: str | None = None,
|
|
45
|
+
max_files: int | None = None,
|
|
46
|
+
service: str | None = None,
|
|
47
|
+
env_name: str | None = None,
|
|
48
|
+
version: str | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._url = upload_url or _resolve_upload_url()
|
|
51
|
+
self._headers = _resolve_headers(headers)
|
|
52
|
+
self._timeout = timeout
|
|
53
|
+
self._session = session or requests.Session()
|
|
54
|
+
self._pprof_path = (
|
|
55
|
+
pprof_path
|
|
56
|
+
if pprof_path is not None
|
|
57
|
+
else environ.get(OTEL_PROFILING_PPROF_PATH)
|
|
58
|
+
)
|
|
59
|
+
self._max_files = (
|
|
60
|
+
max_files
|
|
61
|
+
if max_files is not None
|
|
62
|
+
else int(environ.get(OTEL_PROFILING_PPROF_MAX_FILES, "3"))
|
|
63
|
+
)
|
|
64
|
+
if self._max_files < 1:
|
|
65
|
+
raise ValueError("max_files must be >= 1")
|
|
66
|
+
self._written_paths: deque[str] = deque()
|
|
67
|
+
self._service = service
|
|
68
|
+
self._env = env_name
|
|
69
|
+
self._version = version
|
|
70
|
+
self._runtime_id = uuid4().hex
|
|
71
|
+
self._host = platform.node()
|
|
72
|
+
self._runtime = platform.python_implementation()
|
|
73
|
+
self._runtime_version = platform.python_version()
|
|
74
|
+
self._pid = os.getpid()
|
|
75
|
+
self._counter = count()
|
|
76
|
+
|
|
77
|
+
def export(
|
|
78
|
+
self,
|
|
79
|
+
payload: bytes,
|
|
80
|
+
*,
|
|
81
|
+
resource: Resource | None = None,
|
|
82
|
+
start_time_unix_nano: int | None = None,
|
|
83
|
+
end_time_unix_nano: int | None = None,
|
|
84
|
+
) -> ProfileExportResult:
|
|
85
|
+
try:
|
|
86
|
+
filename = self._write_payload(payload)
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
# Local copies are diagnostic output, not a prerequisite for
|
|
89
|
+
# upload. Read-only/full disks must not discard a valid profile.
|
|
90
|
+
_logger.warning("Cannot save local pprof payload: %s", exc)
|
|
91
|
+
filename = None
|
|
92
|
+
try:
|
|
93
|
+
request_headers = dict(self._headers)
|
|
94
|
+
content_type, body = self._build_request(
|
|
95
|
+
payload=payload,
|
|
96
|
+
resource=resource,
|
|
97
|
+
start_time_unix_nano=start_time_unix_nano,
|
|
98
|
+
end_time_unix_nano=end_time_unix_nano,
|
|
99
|
+
)
|
|
100
|
+
request_headers["Content-Type"] = content_type
|
|
101
|
+
response = self._session.post(
|
|
102
|
+
url=self._url,
|
|
103
|
+
data=body,
|
|
104
|
+
headers=request_headers,
|
|
105
|
+
timeout=self._timeout,
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
_logger.error("pprof HTTP export failed: %s", exc)
|
|
109
|
+
return ProfileExportResult.FAILURE
|
|
110
|
+
|
|
111
|
+
if response.status_code not in (200, 202):
|
|
112
|
+
_logger.error(
|
|
113
|
+
"pprof HTTP export failed status %s: %s",
|
|
114
|
+
response.status_code,
|
|
115
|
+
response.text,
|
|
116
|
+
)
|
|
117
|
+
return ProfileExportResult.FAILURE
|
|
118
|
+
|
|
119
|
+
_logger.debug(
|
|
120
|
+
"pprof payload saved to %s and posted to %s",
|
|
121
|
+
filename,
|
|
122
|
+
self._url,
|
|
123
|
+
)
|
|
124
|
+
return ProfileExportResult.SUCCESS
|
|
125
|
+
|
|
126
|
+
def _build_request(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
payload: bytes,
|
|
130
|
+
resource: Resource | None,
|
|
131
|
+
start_time_unix_nano: int | None,
|
|
132
|
+
end_time_unix_nano: int | None,
|
|
133
|
+
) -> tuple[str, bytes]:
|
|
134
|
+
service = self._resolve_service(resource)
|
|
135
|
+
event = {
|
|
136
|
+
"version": "4",
|
|
137
|
+
"family": "python",
|
|
138
|
+
"attachments": ["auto.pprof"],
|
|
139
|
+
"tags_profiler": self._format_tags(service, resource),
|
|
140
|
+
"start": _format_timestamp(
|
|
141
|
+
start_time_unix_nano
|
|
142
|
+
or datetime.datetime.now(tz=datetime.timezone.utc).timestamp()
|
|
143
|
+
* 1_000_000_000
|
|
144
|
+
),
|
|
145
|
+
"end": _format_timestamp(
|
|
146
|
+
end_time_unix_nano
|
|
147
|
+
or start_time_unix_nano
|
|
148
|
+
or datetime.datetime.now(tz=datetime.timezone.utc).timestamp()
|
|
149
|
+
* 1_000_000_000
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
files = [
|
|
153
|
+
{
|
|
154
|
+
"name": "auto",
|
|
155
|
+
"filename": "auto.pprof",
|
|
156
|
+
"content_type": "application/octet-stream",
|
|
157
|
+
"data": payload,
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
return _encode_multipart_formdata(event=event, files=files)
|
|
161
|
+
|
|
162
|
+
def _format_tags(self, service: str, resource: Resource | None) -> str:
|
|
163
|
+
tags = {
|
|
164
|
+
"service": service,
|
|
165
|
+
"runtime-id": self._runtime_id,
|
|
166
|
+
"host": self._host,
|
|
167
|
+
"language": "python",
|
|
168
|
+
"runtime": self._runtime,
|
|
169
|
+
"runtime_version": self._runtime_version,
|
|
170
|
+
"profiler_version": __version__,
|
|
171
|
+
}
|
|
172
|
+
env_name = self._resolve_env(resource)
|
|
173
|
+
if env_name:
|
|
174
|
+
tags["env"] = env_name
|
|
175
|
+
version = self._resolve_version(resource)
|
|
176
|
+
if version:
|
|
177
|
+
tags["version"] = version
|
|
178
|
+
return ",".join(f"{key}:{value}" for key, value in tags.items())
|
|
179
|
+
|
|
180
|
+
def _resolve_service(self, resource: Resource | None) -> str:
|
|
181
|
+
return (
|
|
182
|
+
_resource_attribute(resource, "service.name")
|
|
183
|
+
or self._service
|
|
184
|
+
or "unknown_service"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _resolve_env(self, resource: Resource | None) -> str | None:
|
|
188
|
+
return (
|
|
189
|
+
_resource_attribute(
|
|
190
|
+
resource,
|
|
191
|
+
"deployment.environment.name",
|
|
192
|
+
"deployment.environment",
|
|
193
|
+
)
|
|
194
|
+
or self._env
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _resolve_version(self, resource: Resource | None) -> str | None:
|
|
198
|
+
return (
|
|
199
|
+
_resource_attribute(resource, "service.version") or self._version
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _write_payload(self, payload: bytes) -> str | None:
|
|
203
|
+
if not self._pprof_path:
|
|
204
|
+
return None
|
|
205
|
+
filename = (
|
|
206
|
+
f"{self._pprof_path}.{self._pid}.{next(self._counter)}.pprof"
|
|
207
|
+
)
|
|
208
|
+
os.makedirs(os.path.dirname(filename) or ".", exist_ok=True)
|
|
209
|
+
with open(filename, "wb") as handle:
|
|
210
|
+
handle.write(payload)
|
|
211
|
+
self._written_paths.append(filename)
|
|
212
|
+
while len(self._written_paths) > self._max_files:
|
|
213
|
+
previous = self._written_paths.popleft()
|
|
214
|
+
try:
|
|
215
|
+
os.remove(previous)
|
|
216
|
+
except OSError as exc:
|
|
217
|
+
_logger.warning("Cannot remove local pprof payload: %s", exc)
|
|
218
|
+
return filename
|
|
219
|
+
|
|
220
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
221
|
+
del timeout_millis
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
def shutdown(self) -> None:
|
|
225
|
+
self._session.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _resolve_headers(
|
|
229
|
+
headers: Optional[Dict[str, str]],
|
|
230
|
+
) -> Dict[str, str]:
|
|
231
|
+
resolved = _parse_headers(environ.get(OTEL_PROFILING_PPROF_HEADERS, ""))
|
|
232
|
+
if headers is not None:
|
|
233
|
+
resolved.update(headers)
|
|
234
|
+
return resolved
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _resolve_upload_url() -> str:
|
|
238
|
+
explicit = environ.get(OTEL_PROFILING_PPROF_UPLOAD_URL) or environ.get(
|
|
239
|
+
OTEL_PROFILING_ENDPOINT
|
|
240
|
+
)
|
|
241
|
+
if explicit:
|
|
242
|
+
return explicit
|
|
243
|
+
return f"{_DEFAULT_AGENT_BASE_URL}/{_DEFAULT_AGENT_PATH}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _parse_headers(value: str) -> Dict[str, str]:
|
|
247
|
+
headers = {}
|
|
248
|
+
for header in value.split(","):
|
|
249
|
+
if ":" not in header:
|
|
250
|
+
continue
|
|
251
|
+
key, header_value = header.split(":", 1)
|
|
252
|
+
key = key.strip()
|
|
253
|
+
header_value = header_value.strip()
|
|
254
|
+
if key:
|
|
255
|
+
headers[key] = header_value
|
|
256
|
+
return headers
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _resource_attribute(resource: Resource | None, *keys: str) -> str | None:
|
|
260
|
+
if resource is None:
|
|
261
|
+
return None
|
|
262
|
+
for key in keys:
|
|
263
|
+
value = resource.attributes.get(key)
|
|
264
|
+
if isinstance(value, str) and value:
|
|
265
|
+
return value
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _format_timestamp(timestamp_unix_nano: float | int) -> str:
|
|
270
|
+
timestamp = datetime.datetime.fromtimestamp(
|
|
271
|
+
float(timestamp_unix_nano) / 1e9,
|
|
272
|
+
tz=datetime.timezone.utc,
|
|
273
|
+
)
|
|
274
|
+
return timestamp.replace(microsecond=0).isoformat()[0:-6] + "Z"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _encode_multipart_formdata(
|
|
278
|
+
*,
|
|
279
|
+
event: dict[str, str | list[str]],
|
|
280
|
+
files: list[dict[str, str | bytes]],
|
|
281
|
+
) -> tuple[str, bytes]:
|
|
282
|
+
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
|
283
|
+
boundary_bytes = boundary.encode("ascii")
|
|
284
|
+
body = (
|
|
285
|
+
(b"--%s\r\n" % boundary_bytes)
|
|
286
|
+
+ b'Content-Disposition: form-data; name="event"; filename="event.json"\r\n'
|
|
287
|
+
+ b"Content-Type: application/json\r\n\r\n"
|
|
288
|
+
+ _encode_json(event)
|
|
289
|
+
+ b"\r\n"
|
|
290
|
+
+ b"".join(
|
|
291
|
+
(b"--%s\r\n" % boundary_bytes)
|
|
292
|
+
+ (
|
|
293
|
+
b'Content-Disposition: form-data; name="%s"; filename="%s"\r\n'
|
|
294
|
+
% (
|
|
295
|
+
str(item["name"]).encode("utf-8"),
|
|
296
|
+
str(item["filename"]).encode("utf-8"),
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
+ (
|
|
300
|
+
b"Content-Type: %s\r\n\r\n"
|
|
301
|
+
% str(item["content_type"]).encode("utf-8")
|
|
302
|
+
)
|
|
303
|
+
+ bytes(item["data"])
|
|
304
|
+
+ b"\r\n"
|
|
305
|
+
for item in files
|
|
306
|
+
)
|
|
307
|
+
+ b"--%s--\r\n" % boundary_bytes
|
|
308
|
+
)
|
|
309
|
+
return f"multipart/form-data; boundary={boundary}", body
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _encode_json(payload: dict[str, str | list[str]]) -> bytes:
|
|
313
|
+
return json.dumps(payload).encode("utf-8")
|
|
@@ -65,7 +65,7 @@ from opentelemetry.sdk.extension.profiling.environment_variables import (
|
|
|
65
65
|
OTEL_PROFILING_SAMPLE_INTERVAL,
|
|
66
66
|
)
|
|
67
67
|
from opentelemetry.sdk.extension.profiling.export import (
|
|
68
|
-
|
|
68
|
+
CompatiblePPROFExporter,
|
|
69
69
|
)
|
|
70
70
|
from opentelemetry.sdk.extension.profiling.export.file import (
|
|
71
71
|
FileProfileExporter,
|
|
@@ -74,6 +74,7 @@ from opentelemetry.sdk.extension.profiling.model.builder import (
|
|
|
74
74
|
ProfilesRequestBuilder,
|
|
75
75
|
)
|
|
76
76
|
from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
|
|
77
|
+
CompatiblePprofProfileBuilder,
|
|
77
78
|
PprofProfileBuilder,
|
|
78
79
|
)
|
|
79
80
|
from opentelemetry.sdk.extension.profiling.scheduler import ProfileScheduler
|
|
@@ -432,6 +433,18 @@ class Profiler:
|
|
|
432
433
|
resource=resource,
|
|
433
434
|
sample_period_ns=int(self._sample_interval * 1_000_000_000),
|
|
434
435
|
)
|
|
436
|
+
if isinstance(self._exporter, CompatiblePPROFExporter):
|
|
437
|
+
self._exporter.export(
|
|
438
|
+
payload,
|
|
439
|
+
resource=resource,
|
|
440
|
+
start_time_unix_nano=min(
|
|
441
|
+
sample.timestamp_unix_nano for sample in batch
|
|
442
|
+
),
|
|
443
|
+
end_time_unix_nano=max(
|
|
444
|
+
sample.timestamp_unix_nano for sample in batch
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
return
|
|
435
448
|
self._exporter.export(payload)
|
|
436
449
|
|
|
437
450
|
def force_flush(self) -> None:
|
|
@@ -447,20 +460,22 @@ class Profiler:
|
|
|
447
460
|
return Resource.create()
|
|
448
461
|
|
|
449
462
|
def _create_builder(self):
|
|
463
|
+
if isinstance(self._exporter, CompatiblePPROFExporter):
|
|
464
|
+
return CompatiblePprofProfileBuilder()
|
|
450
465
|
if isinstance(self._exporter, FileProfileExporter):
|
|
451
466
|
return PprofProfileBuilder()
|
|
452
467
|
return ProfilesRequestBuilder()
|
|
453
468
|
|
|
454
469
|
@staticmethod
|
|
455
470
|
def _create_exporter_from_env():
|
|
456
|
-
exporter_name = environ.get(OTEL_PROFILING_EXPORTER, "
|
|
471
|
+
exporter_name = environ.get(OTEL_PROFILING_EXPORTER, "http").strip()
|
|
457
472
|
if exporter_name == "file":
|
|
458
473
|
return FileProfileExporter()
|
|
459
|
-
if exporter_name
|
|
474
|
+
if exporter_name not in ("http", "datakit", "pprof_http"):
|
|
460
475
|
raise ValueError(
|
|
461
476
|
f"Unsupported profiling exporter '{exporter_name}'"
|
|
462
477
|
)
|
|
463
|
-
return
|
|
478
|
+
return CompatiblePPROFExporter()
|
|
464
479
|
|
|
465
480
|
def _start_collectors(self) -> list[Collector]:
|
|
466
481
|
active_collectors = []
|
|
@@ -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.7"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import json
|
|
5
|
+
from email import policy
|
|
6
|
+
from email.parser import BytesParser
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from threading import Event, Thread
|
|
9
|
+
from time import sleep
|
|
10
|
+
|
|
11
|
+
from opentelemetry.sdk.extension.profiling.export import pprof_pb2
|
|
12
|
+
from opentelemetry.sdk.extension.profiling.runtime import Profiler
|
|
13
|
+
from opentelemetry.sdk.resources import Resource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_http_environment_name_keeps_real_multipart_pprof_upload(monkeypatch):
|
|
17
|
+
received = {}
|
|
18
|
+
ready = Event()
|
|
19
|
+
|
|
20
|
+
class Handler(BaseHTTPRequestHandler):
|
|
21
|
+
def do_POST(self) -> None:
|
|
22
|
+
body = self.rfile.read(int(self.headers["Content-Length"]))
|
|
23
|
+
message = BytesParser(policy=policy.default).parsebytes(
|
|
24
|
+
f"Content-Type: {self.headers['Content-Type']}\r\n\r\n".encode()
|
|
25
|
+
+ body
|
|
26
|
+
)
|
|
27
|
+
received["path"] = self.path
|
|
28
|
+
received["parts"] = {
|
|
29
|
+
part.get_filename(): part.get_payload(decode=True)
|
|
30
|
+
for part in message.iter_parts()
|
|
31
|
+
}
|
|
32
|
+
self.send_response(200)
|
|
33
|
+
self.end_headers()
|
|
34
|
+
ready.set()
|
|
35
|
+
|
|
36
|
+
def log_message(self, format: str, *args) -> None: # noqa: A002
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
40
|
+
worker = Thread(target=server.serve_forever, daemon=True)
|
|
41
|
+
worker.start()
|
|
42
|
+
monkeypatch.setenv("OTEL_PROFILING_EXPORTER", "http")
|
|
43
|
+
monkeypatch.delenv("OTEL_PROFILING_PPROF_UPLOAD_URL", raising=False)
|
|
44
|
+
monkeypatch.setenv(
|
|
45
|
+
"OTEL_PROFILING_ENDPOINT",
|
|
46
|
+
f"http://127.0.0.1:{server.server_port}/profiling/v1/input",
|
|
47
|
+
)
|
|
48
|
+
profiler = Profiler(
|
|
49
|
+
sample_interval=0.005,
|
|
50
|
+
export_interval=3600,
|
|
51
|
+
exception_enabled=False,
|
|
52
|
+
lock_enabled=False,
|
|
53
|
+
memory_enabled=False,
|
|
54
|
+
resource=Resource.create({"service.name": "multipart-regression"}),
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
profiler.start()
|
|
58
|
+
sleep(0.1)
|
|
59
|
+
profiler.stop()
|
|
60
|
+
assert ready.wait(3)
|
|
61
|
+
assert received["path"] == "/profiling/v1/input"
|
|
62
|
+
event = json.loads(received["parts"]["event.json"])
|
|
63
|
+
assert event["family"] == "python"
|
|
64
|
+
assert event["attachments"] == ["auto.pprof"]
|
|
65
|
+
assert "service:multipart-regression" in event["tags_profiler"]
|
|
66
|
+
profile = pprof_pb2.Profile.FromString(
|
|
67
|
+
gzip.decompress(received["parts"]["auto.pprof"])
|
|
68
|
+
)
|
|
69
|
+
assert profile.sample
|
|
70
|
+
assert profile.function
|
|
71
|
+
finally:
|
|
72
|
+
profiler.stop()
|
|
73
|
+
server.shutdown()
|
|
74
|
+
server.server_close()
|
|
75
|
+
worker.join(3)
|
|
@@ -0,0 +1,158 @@
|
|
|
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"
|
|
@@ -5,23 +5,33 @@ import pytest
|
|
|
5
5
|
from opentelemetry.sdk.extension.profiling.export.file import (
|
|
6
6
|
FileProfileExporter,
|
|
7
7
|
)
|
|
8
|
+
from opentelemetry.sdk.extension.profiling.export.pprof_http import (
|
|
9
|
+
PPROFHTTPExporter,
|
|
10
|
+
)
|
|
8
11
|
from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
|
|
12
|
+
CompatiblePprofProfileBuilder,
|
|
9
13
|
PprofProfileBuilder,
|
|
10
14
|
)
|
|
11
15
|
from opentelemetry.sdk.extension.profiling.runtime import Profiler
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
monkeypatch.delenv("
|
|
18
|
+
@pytest.mark.parametrize("name", [None, "http", "datakit", "pprof_http"])
|
|
19
|
+
def test_runtime_keeps_compatible_upload_for_http_name(monkeypatch, name):
|
|
20
|
+
monkeypatch.delenv("OTEL_PROFILING_PPROF_UPLOAD_URL", raising=False)
|
|
21
|
+
if name is None:
|
|
22
|
+
monkeypatch.delenv("OTEL_PROFILING_EXPORTER", raising=False)
|
|
23
|
+
else:
|
|
24
|
+
monkeypatch.setenv("OTEL_PROFILING_EXPORTER", name)
|
|
17
25
|
monkeypatch.setenv(
|
|
18
|
-
"
|
|
26
|
+
"OTEL_PROFILING_ENDPOINT", "http://127.0.0.1:9529/profiling/v1/input"
|
|
19
27
|
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
exporter = Profiler._create_exporter_from_env()
|
|
29
|
+
assert isinstance(exporter, PPROFHTTPExporter)
|
|
30
|
+
assert exporter._url == "http://127.0.0.1:9529/profiling/v1/input"
|
|
31
|
+
assert isinstance(
|
|
32
|
+
Profiler(exporter=exporter)._builder, CompatiblePprofProfileBuilder
|
|
23
33
|
)
|
|
24
|
-
|
|
34
|
+
exporter.shutdown()
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
def test_file_ignores_all_endpoint_settings(monkeypatch, tmp_path):
|
|
@@ -38,8 +48,8 @@ def test_file_ignores_all_endpoint_settings(monkeypatch, tmp_path):
|
|
|
38
48
|
assert next(tmp_path.glob("*.pprof")).read_bytes() == b"profile"
|
|
39
49
|
|
|
40
50
|
|
|
41
|
-
@pytest.mark.parametrize("name", ["
|
|
42
|
-
def
|
|
51
|
+
@pytest.mark.parametrize("name", ["unknown", "pprof", "none", "otlp"])
|
|
52
|
+
def test_unsupported_exporter_names_rejected(monkeypatch, name):
|
|
43
53
|
monkeypatch.setenv("OTEL_PROFILING_EXPORTER", name)
|
|
44
54
|
with pytest.raises(ValueError, match="Unsupported profiling exporter"):
|
|
45
55
|
Profiler._create_exporter_from_env()
|
|
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
|
|
File without changes
|
|
File without changes
|
{beacon_profiling-1.2.6 → beacon_profiling-1.2.7}/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
|
|
File without changes
|