beacon-profiling 1.2.4__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.
Files changed (58) hide show
  1. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/PKG-INFO +31 -41
  2. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/README.rst +26 -36
  3. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/pyproject.toml +4 -4
  4. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/environment_variables.py +2 -0
  5. beacon_profiling-1.2.4/src/opentelemetry/sdk/extension/profiling/export/pprof.py → beacon_profiling-1.2.7/src/opentelemetry/sdk/extension/profiling/export/file.py +12 -10
  6. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/pprof_http.py +5 -2
  7. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/runtime.py +17 -32
  8. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/version.py +1 -1
  9. beacon_profiling-1.2.7/tests/test_compatible_runtime_http.py +75 -0
  10. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_pprof_exporter.py +4 -4
  11. beacon_profiling-1.2.7/tests/test_runtime_exporter_selection.py +91 -0
  12. beacon_profiling-1.2.4/tests/test_runtime_exporter_selection.py +0 -106
  13. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/.gitignore +0 -0
  14. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/LICENSE +0 -0
  15. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/__init__.py +0 -0
  16. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/bootstrap.py +0 -0
  17. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/__init__.py +0 -0
  18. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/base.py +0 -0
  19. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/buffering.py +0 -0
  20. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/exception.py +0 -0
  21. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/lock.py +0 -0
  22. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/memory.py +0 -0
  23. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/collector/stack.py +0 -0
  24. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/context_bridge.py +0 -0
  25. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/__init__.py +0 -0
  26. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/grpc.py +0 -0
  27. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/http.py +0 -0
  28. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/pprof_pb2.py +0 -0
  29. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/export/result.py +0 -0
  30. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/__init__.py +0 -0
  31. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/builder.py +0 -0
  32. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/model/pprof_builder.py +0 -0
  33. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/src/opentelemetry/sdk/extension/profiling/scheduler.py +0 -0
  34. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_asyncio_context_bridge.py +0 -0
  35. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_asyncio_lock_collector.py +0 -0
  36. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_asyncio_lock_counts.py +0 -0
  37. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_auto_instrumentation_integration.py +0 -0
  38. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_bootstrap.py +0 -0
  39. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_buffer_budget.py +0 -0
  40. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_builder.py +0 -0
  41. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_context_bridge.py +0 -0
  42. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_dictionary_contract.py +0 -0
  43. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_exception_collector.py +0 -0
  44. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_exporters.py +0 -0
  45. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_fork_runtime.py +0 -0
  46. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_local_root_context.py +0 -0
  47. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_lock_collector.py +0 -0
  48. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_lock_sampling.py +0 -0
  49. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_memory_collector.py +0 -0
  50. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_package_metadata.py +0 -0
  51. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_pprof_builder.py +0 -0
  52. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_pprof_http_exporter.py +0 -0
  53. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_profiler_integration.py +0 -0
  54. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_protobuf_environment.py +0 -0
  55. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_runtime_scheduling.py +0 -0
  56. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_scheduler_resilience.py +0 -0
  57. {beacon_profiling-1.2.4 → beacon_profiling-1.2.7}/tests/test_stack_collector.py +0 -0
  58. {beacon_profiling-1.2.4 → 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.4
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
@@ -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.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
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 a compatible pprof endpoint and supports multiple collector types:
35
+ or local pprof files and supports multiple collector types:
36
36
 
37
37
  * stack sampling
38
38
  * handled exceptions
@@ -77,7 +77,12 @@ Configuration
77
77
  The following environment variables are supported:
78
78
 
79
79
  * ``OTEL_PROFILING_ENABLED``
80
- * ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``pprof``)
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)
@@ -106,11 +111,7 @@ The following environment variables are supported:
106
111
  * ``OTEL_EXPORTER_OTLP_PROFILES_CERTIFICATE``
107
112
  * ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
108
113
  * ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
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)
112
- * ``OTEL_PROFILING_PPROF_UPLOAD_URL``
113
- * ``OTEL_PROFILING_PPROF_HEADERS``
114
+ * ``OTEL_PROFILING_FILE_PATH`` (directory; defaults to ``otel-profiles``)
114
115
 
115
116
  Service metadata should be configured with standard OpenTelemetry
116
117
  resource settings such as ``OTEL_SERVICE_NAME`` and
@@ -141,8 +142,8 @@ periodic memory scheduling uses the positive export cadence instead of spinning.
141
142
  Examples
142
143
  --------
143
144
 
144
- Collect stack, exception, lock, and heap profiles and export them with
145
- OTLP/HTTP:
145
+ Collect stack, exception, lock, and heap profiles and upload them over HTTP
146
+ using the existing DataKit-compatible multipart/pprof format:
146
147
 
147
148
  ::
148
149
 
@@ -150,38 +151,27 @@ OTLP/HTTP:
150
151
  export OTEL_PROFILING_EXCEPTION_ENABLED=true
151
152
  export OTEL_PROFILING_LOCK_ENABLED=true
152
153
  export OTEL_PROFILING_MEMORY_ENABLED=true
153
- export OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL=http/protobuf
154
- export OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT=http://localhost:4318/v1development/profiles
154
+ export OTEL_PROFILING_EXPORTER=http
155
+ export OTEL_PROFILING_ENDPOINT=http://127.0.0.1:9529/profiling/v1/input
155
156
  beacon python app.py
156
157
 
157
- To dump pprof files instead of OTLP, set:
158
+ To dump pprof files instead of uploading them, set:
158
159
 
159
160
  ::
160
161
 
161
- export OTEL_PROFILING_EXPORTER=pprof
162
- export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
162
+ export OTEL_PROFILING_EXPORTER=file
163
+ export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
163
164
 
164
- To POST pprof directly to a compatible agent endpoint and keep a local
165
- copy:
165
+ The ``file`` exporter ignores all OTLP endpoint and transport settings.
166
+ It writes gzip pprof files to the configured directory, without network uploads.
166
167
 
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.
180
-
181
- Capture and export exceptions are logged; subsequent scheduled captures
182
- and export attempts continue. Failed batches are not replayed automatically.
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.
183
173
 
184
- When ``OTEL_PROFILING_EXPORTER`` is unset and
185
- ``OTEL_PROFILING_PPROF_UPLOAD_URL`` is configured, the runtime
186
- defaults to a legacy-compatible ``pprof`` upload layout. Set
187
- ``OTEL_PROFILING_EXPORTER=otlp`` to force OTLP profile export.
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.
@@ -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 a compatible pprof endpoint and supports multiple collector types:
8
+ or local pprof files and supports multiple collector types:
9
9
 
10
10
  * stack sampling
11
11
  * handled exceptions
@@ -50,7 +50,12 @@ Configuration
50
50
  The following environment variables are supported:
51
51
 
52
52
  * ``OTEL_PROFILING_ENABLED``
53
- * ``OTEL_PROFILING_EXPORTER`` (``otlp`` or ``pprof``)
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)
@@ -79,11 +84,7 @@ The following environment variables are supported:
79
84
  * ``OTEL_EXPORTER_OTLP_PROFILES_CERTIFICATE``
80
85
  * ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_KEY``
81
86
  * ``OTEL_EXPORTER_OTLP_PROFILES_CLIENT_CERTIFICATE``
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)
85
- * ``OTEL_PROFILING_PPROF_UPLOAD_URL``
86
- * ``OTEL_PROFILING_PPROF_HEADERS``
87
+ * ``OTEL_PROFILING_FILE_PATH`` (directory; defaults to ``otel-profiles``)
87
88
 
88
89
  Service metadata should be configured with standard OpenTelemetry
89
90
  resource settings such as ``OTEL_SERVICE_NAME`` and
@@ -114,8 +115,8 @@ periodic memory scheduling uses the positive export cadence instead of spinning.
114
115
  Examples
115
116
  --------
116
117
 
117
- Collect stack, exception, lock, and heap profiles and export them with
118
- OTLP/HTTP:
118
+ Collect stack, exception, lock, and heap profiles and upload them over HTTP
119
+ using the existing DataKit-compatible multipart/pprof format:
119
120
 
120
121
  ::
121
122
 
@@ -123,38 +124,27 @@ OTLP/HTTP:
123
124
  export OTEL_PROFILING_EXCEPTION_ENABLED=true
124
125
  export OTEL_PROFILING_LOCK_ENABLED=true
125
126
  export OTEL_PROFILING_MEMORY_ENABLED=true
126
- export OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL=http/protobuf
127
- export OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT=http://localhost:4318/v1development/profiles
127
+ export OTEL_PROFILING_EXPORTER=http
128
+ export OTEL_PROFILING_ENDPOINT=http://127.0.0.1:9529/profiling/v1/input
128
129
  beacon python app.py
129
130
 
130
- To dump pprof files instead of OTLP, set:
131
+ To dump pprof files instead of uploading them, set:
131
132
 
132
133
  ::
133
134
 
134
- export OTEL_PROFILING_EXPORTER=pprof
135
- export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
135
+ export OTEL_PROFILING_EXPORTER=file
136
+ export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
136
137
 
137
- To POST pprof directly to a compatible agent endpoint and keep a local
138
- copy:
138
+ The ``file`` exporter ignores all OTLP endpoint and transport settings.
139
+ It writes gzip pprof files to the configured directory, without network uploads.
139
140
 
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.
153
-
154
- Capture and export exceptions are logged; subsequent scheduled captures
155
- and export attempts continue. Failed batches are not replayed automatically.
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.
156
146
 
157
- When ``OTEL_PROFILING_EXPORTER`` is unset and
158
- ``OTEL_PROFILING_PPROF_UPLOAD_URL`` is configured, the runtime
159
- defaults to a legacy-compatible ``pprof`` upload layout. Set
160
- ``OTEL_PROFILING_EXPORTER=otlp`` to force OTLP profile export.
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.
@@ -25,10 +25,10 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.14",
26
26
  ]
27
27
  dependencies = [
28
- "opentelemetry-api == 1.45.1",
29
- "opentelemetry-sdk == 1.45.1",
30
- "opentelemetry-instrumentation == 0.66b1",
31
- "opentelemetry-exporter-otlp == 1.45.1",
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,6 +34,8 @@ 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
+ OTEL_PROFILING_FILE_PATH = "OTEL_PROFILING_FILE_PATH"
38
+ OTEL_PROFILING_ENDPOINT = "OTEL_PROFILING_ENDPOINT"
37
39
  OTEL_PROFILING_PPROF_PATH = "OTEL_PROFILING_PPROF_PATH"
38
40
  OTEL_PROFILING_PPROF_MAX_FILES = "OTEL_PROFILING_PPROF_MAX_FILES"
39
41
  OTEL_PROFILING_PPROF_UPLOAD_URL = "OTEL_PROFILING_PPROF_UPLOAD_URL"
@@ -1,28 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from itertools import count
4
+ from pathlib import Path
5
+ from uuid import uuid4
5
6
 
6
7
  from opentelemetry.sdk.extension.profiling.environment_variables import (
7
- OTEL_PROFILING_PPROF_PATH,
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 PPROFProfileExporter:
15
+ class FileProfileExporter:
15
16
  def __init__(self, *, directory: str | None = None) -> None:
16
- self._directory = directory or os.environ.get(
17
- OTEL_PROFILING_PPROF_PATH, "otel-profiles"
17
+ self._directory = Path(
18
+ directory
19
+ or os.environ.get(OTEL_PROFILING_FILE_PATH, "otel-profiles")
18
20
  )
19
- os.makedirs(os.path.dirname(self._directory) or ".", exist_ok=True)
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 = f"{self._directory}.{self._pid}.{next(self._counter)}.pprof"
25
- with open(filename, "wb") as handle:
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
 
@@ -15,6 +15,7 @@ from uuid import uuid4
15
15
  import requests
16
16
 
17
17
  from opentelemetry.sdk.extension.profiling.environment_variables import (
18
+ OTEL_PROFILING_ENDPOINT,
18
19
  OTEL_PROFILING_PPROF_HEADERS,
19
20
  OTEL_PROFILING_PPROF_MAX_FILES,
20
21
  OTEL_PROFILING_PPROF_PATH,
@@ -28,7 +29,7 @@ from opentelemetry.sdk.resources import Resource
28
29
 
29
30
  _logger = logging.getLogger(__name__)
30
31
 
31
- _DEFAULT_AGENT_BASE_URL = "http://localhost:8126"
32
+ _DEFAULT_AGENT_BASE_URL = "http://127.0.0.1:9529"
32
33
  _DEFAULT_AGENT_PATH = "profiling/v1/input"
33
34
 
34
35
 
@@ -234,7 +235,9 @@ def _resolve_headers(
234
235
 
235
236
 
236
237
  def _resolve_upload_url() -> str:
237
- explicit = environ.get(OTEL_PROFILING_PPROF_UPLOAD_URL)
238
+ explicit = environ.get(OTEL_PROFILING_PPROF_UPLOAD_URL) or environ.get(
239
+ OTEL_PROFILING_ENDPOINT
240
+ )
238
241
  if explicit:
239
242
  return explicit
240
243
  return f"{_DEFAULT_AGENT_BASE_URL}/{_DEFAULT_AGENT_PATH}"
@@ -62,15 +62,13 @@ 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
68
  CompatiblePPROFExporter,
70
- create_otlp_profile_exporter,
71
69
  )
72
- from opentelemetry.sdk.extension.profiling.export.pprof import (
73
- PPROFProfileExporter,
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,
@@ -89,7 +87,7 @@ _DEFAULT_EXPORT_INTERVAL_SECONDS = 60.0
89
87
 
90
88
 
91
89
  class Profiler:
92
- _active_instance: "Profiler | None" = None
90
+ _active_instance: Profiler | None = None
93
91
  _active_lock = Lock()
94
92
 
95
93
  def __init__(
@@ -318,8 +316,7 @@ class Profiler:
318
316
  active = self.__class__._active_instance
319
317
  if active is not None and active is not self and active._started:
320
318
  _logger.error(
321
- "A profiler is already running. Only one profiler "
322
- "instance can be active at a time."
319
+ "A profiler is already running. Only one profiler instance can be active at a time."
323
320
  )
324
321
  return
325
322
  if self._started:
@@ -431,12 +428,6 @@ class Profiler:
431
428
  batch = self._samples.drain()
432
429
 
433
430
  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
431
  payload = self._builder.build(
441
432
  samples=batch,
442
433
  resource=resource,
@@ -446,8 +437,12 @@ class Profiler:
446
437
  self._exporter.export(
447
438
  payload,
448
439
  resource=resource,
449
- start_time_unix_nano=start_time_unix_nano,
450
- end_time_unix_nano=end_time_unix_nano,
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
+ ),
451
446
  )
452
447
  return
453
448
  self._exporter.export(payload)
@@ -467,30 +462,20 @@ class Profiler:
467
462
  def _create_builder(self):
468
463
  if isinstance(self._exporter, CompatiblePPROFExporter):
469
464
  return CompatiblePprofProfileBuilder()
470
- if isinstance(self._exporter, PPROFProfileExporter):
465
+ if isinstance(self._exporter, FileProfileExporter):
471
466
  return PprofProfileBuilder()
472
467
  return ProfilesRequestBuilder()
473
468
 
474
469
  @staticmethod
475
470
  def _create_exporter_from_env():
476
- exporter_name = environ.get(OTEL_PROFILING_EXPORTER)
477
- if exporter_name is None:
478
- if environ.get(OTEL_PROFILING_PPROF_UPLOAD_URL):
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()
489
- if exporter_name != "otlp":
471
+ exporter_name = environ.get(OTEL_PROFILING_EXPORTER, "http").strip()
472
+ if exporter_name == "file":
473
+ return FileProfileExporter()
474
+ if exporter_name not in ("http", "datakit", "pprof_http"):
490
475
  raise ValueError(
491
476
  f"Unsupported profiling exporter '{exporter_name}'"
492
477
  )
493
- return create_otlp_profile_exporter()
478
+ return CompatiblePPROFExporter()
494
479
 
495
480
  def _start_collectors(self) -> list[Collector]:
496
481
  active_collectors = []
@@ -593,7 +578,7 @@ class Profiler:
593
578
  after_fork()
594
579
 
595
580
  @staticmethod
596
- def _reset_after_fork(component) -> None: # noqa: ANN001
581
+ def _reset_after_fork(component) -> None:
597
582
  reset = getattr(component, "reset_after_fork", None)
598
583
  if callable(reset):
599
584
  reset()
@@ -1,2 +1,2 @@
1
1
  # Generated from beacon/version.properties by beacon/scripts/check-version.py.
2
- __version__ = "1.2.4"
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)
@@ -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.pprof import (
6
- PPROFProfileExporter,
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 = PPROFProfileExporter(directory=str(tmp_path / "prof"))
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("prof*.pprof"))
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"
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from opentelemetry.sdk.extension.profiling.export.file import (
6
+ FileProfileExporter,
7
+ )
8
+ from opentelemetry.sdk.extension.profiling.export.pprof_http import (
9
+ PPROFHTTPExporter,
10
+ )
11
+ from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
12
+ CompatiblePprofProfileBuilder,
13
+ PprofProfileBuilder,
14
+ )
15
+ from opentelemetry.sdk.extension.profiling.runtime import Profiler
16
+
17
+
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)
25
+ monkeypatch.setenv(
26
+ "OTEL_PROFILING_ENDPOINT", "http://127.0.0.1:9529/profiling/v1/input"
27
+ )
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
33
+ )
34
+ exporter.shutdown()
35
+
36
+
37
+ def test_file_ignores_all_endpoint_settings(monkeypatch, tmp_path):
38
+ monkeypatch.setenv("OTEL_PROFILING_EXPORTER", "file")
39
+ monkeypatch.setenv("OTEL_PROFILING_FILE_PATH", str(tmp_path))
40
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROFILES_ENDPOINT", "not a URL")
41
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROFILES_PROTOCOL", "invalid")
42
+ exporter = Profiler._create_exporter_from_env()
43
+ assert isinstance(exporter, FileProfileExporter)
44
+ assert isinstance(
45
+ Profiler(exporter=exporter)._builder, PprofProfileBuilder
46
+ )
47
+ exporter.export(b"profile")
48
+ assert next(tmp_path.glob("*.pprof")).read_bytes() == b"profile"
49
+
50
+
51
+ @pytest.mark.parametrize("name", ["unknown", "pprof", "none", "otlp"])
52
+ def test_unsupported_exporter_names_rejected(monkeypatch, name):
53
+ monkeypatch.setenv("OTEL_PROFILING_EXPORTER", name)
54
+ with pytest.raises(ValueError, match="Unsupported profiling exporter"):
55
+ Profiler._create_exporter_from_env()
56
+
57
+
58
+ def test_runtime_defaults_to_sixty_second_intervals(monkeypatch):
59
+ monkeypatch.delenv("OTEL_PROFILING_EXPORT_INTERVAL", raising=False)
60
+ monkeypatch.delenv("OTEL_PROFILING_MEMORY_INTERVAL", raising=False)
61
+
62
+ profiler = Profiler(exporter=object())
63
+
64
+ assert profiler._export_interval == 60.0
65
+ assert profiler._memory_interval == 60.0
66
+
67
+
68
+ def test_runtime_interval_environment_variables_override_defaults(
69
+ monkeypatch,
70
+ ):
71
+ monkeypatch.setenv("OTEL_PROFILING_EXPORT_INTERVAL", "17.5")
72
+ monkeypatch.setenv("OTEL_PROFILING_MEMORY_INTERVAL", "23.5")
73
+
74
+ profiler = Profiler(exporter=object())
75
+
76
+ assert profiler._export_interval == 17.5
77
+ assert profiler._memory_interval == 23.5
78
+
79
+
80
+ def test_runtime_interval_arguments_override_environment(monkeypatch):
81
+ monkeypatch.setenv("OTEL_PROFILING_EXPORT_INTERVAL", "17.5")
82
+ monkeypatch.setenv("OTEL_PROFILING_MEMORY_INTERVAL", "23.5")
83
+
84
+ profiler = Profiler(
85
+ exporter=object(),
86
+ export_interval=31.5,
87
+ memory_interval=37.5,
88
+ )
89
+
90
+ assert profiler._export_interval == 31.5
91
+ assert profiler._memory_interval == 37.5
@@ -1,106 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from opentelemetry.sdk.extension.profiling.export import (
4
- CompatiblePPROFExporter,
5
- )
6
- from opentelemetry.sdk.extension.profiling.export.pprof import (
7
- PPROFProfileExporter,
8
- )
9
- from opentelemetry.sdk.extension.profiling.model.pprof_builder import (
10
- CompatiblePprofProfileBuilder,
11
- PprofProfileBuilder,
12
- )
13
- from opentelemetry.sdk.extension.profiling.runtime import Profiler
14
-
15
-
16
- def test_runtime_defaults_to_compatible_exporter_for_env_fallback(
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
- ):
34
- sentinel = object()
35
- monkeypatch.setenv("OTEL_PROFILING_EXPORTER", "otlp")
36
- monkeypatch.setenv(
37
- "OTEL_PROFILING_PPROF_UPLOAD_URL",
38
- "http://collector:9529/profiling/v1/input",
39
- )
40
- monkeypatch.setattr(
41
- "opentelemetry.sdk.extension.profiling.runtime.create_otlp_profile_exporter",
42
- lambda: sentinel,
43
- )
44
-
45
- exporter = Profiler._create_exporter_from_env()
46
-
47
- assert exporter is sentinel
48
-
49
-
50
- def test_runtime_uses_compatible_pprof_builder_for_http_exporter(tmp_path):
51
- profiler = Profiler(
52
- exporter=CompatiblePPROFExporter(pprof_path=str(tmp_path / "prof"))
53
- )
54
-
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
-
66
- try:
67
- assert isinstance(profiler._builder, PprofProfileBuilder)
68
- assert not isinstance(profiler._builder, CompatiblePprofProfileBuilder)
69
- finally:
70
- profiler._exporter.shutdown()
71
-
72
-
73
- def test_runtime_defaults_to_sixty_second_intervals(monkeypatch):
74
- monkeypatch.delenv("OTEL_PROFILING_EXPORT_INTERVAL", raising=False)
75
- monkeypatch.delenv("OTEL_PROFILING_MEMORY_INTERVAL", raising=False)
76
-
77
- profiler = Profiler(exporter=object())
78
-
79
- assert profiler._export_interval == 60.0
80
- assert profiler._memory_interval == 60.0
81
-
82
-
83
- def test_runtime_interval_environment_variables_override_defaults(
84
- monkeypatch,
85
- ):
86
- monkeypatch.setenv("OTEL_PROFILING_EXPORT_INTERVAL", "17.5")
87
- monkeypatch.setenv("OTEL_PROFILING_MEMORY_INTERVAL", "23.5")
88
-
89
- profiler = Profiler(exporter=object())
90
-
91
- assert profiler._export_interval == 17.5
92
- assert profiler._memory_interval == 23.5
93
-
94
-
95
- def test_runtime_interval_arguments_override_environment(monkeypatch):
96
- monkeypatch.setenv("OTEL_PROFILING_EXPORT_INTERVAL", "17.5")
97
- monkeypatch.setenv("OTEL_PROFILING_MEMORY_INTERVAL", "23.5")
98
-
99
- profiler = Profiler(
100
- exporter=object(),
101
- export_interval=31.5,
102
- memory_interval=37.5,
103
- )
104
-
105
- assert profiler._export_interval == 31.5
106
- assert profiler._memory_interval == 37.5