beacon-profiling 1.2.4__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.
Files changed (56) hide show
  1. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/PKG-INFO +22 -35
  2. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/README.rst +17 -30
  3. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/pyproject.toml +4 -4
  4. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/environment_variables.py +1 -4
  5. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/__init__.py +1 -4
  6. beacon_profiling-1.2.4/src/opentelemetry/sdk/extension/profiling/export/pprof.py → beacon_profiling-1.2.6/src/opentelemetry/sdk/extension/profiling/export/file.py +12 -10
  7. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/runtime.py +9 -39
  8. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/version.py +1 -1
  9. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_pprof_exporter.py +4 -4
  10. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_runtime_exporter_selection.py +24 -49
  11. beacon_profiling-1.2.4/src/opentelemetry/sdk/extension/profiling/export/pprof_http.py +0 -310
  12. beacon_profiling-1.2.4/tests/test_pprof_http_exporter.py +0 -158
  13. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/.gitignore +0 -0
  14. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/LICENSE +0 -0
  15. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/__init__.py +0 -0
  16. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/bootstrap.py +0 -0
  17. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/__init__.py +0 -0
  18. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/base.py +0 -0
  19. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/buffering.py +0 -0
  20. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/exception.py +0 -0
  21. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/lock.py +0 -0
  22. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/memory.py +0 -0
  23. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/collector/stack.py +0 -0
  24. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/context_bridge.py +0 -0
  25. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/grpc.py +0 -0
  26. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/http.py +0 -0
  27. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/pprof_pb2.py +0 -0
  28. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/export/result.py +0 -0
  29. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/__init__.py +0 -0
  30. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/builder.py +0 -0
  31. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/model/pprof_builder.py +0 -0
  32. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/src/opentelemetry/sdk/extension/profiling/scheduler.py +0 -0
  33. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_asyncio_context_bridge.py +0 -0
  34. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_asyncio_lock_collector.py +0 -0
  35. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_asyncio_lock_counts.py +0 -0
  36. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_auto_instrumentation_integration.py +0 -0
  37. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_bootstrap.py +0 -0
  38. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_buffer_budget.py +0 -0
  39. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_builder.py +0 -0
  40. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_context_bridge.py +0 -0
  41. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_dictionary_contract.py +0 -0
  42. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_exception_collector.py +0 -0
  43. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_exporters.py +0 -0
  44. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_fork_runtime.py +0 -0
  45. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_local_root_context.py +0 -0
  46. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_lock_collector.py +0 -0
  47. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_lock_sampling.py +0 -0
  48. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_memory_collector.py +0 -0
  49. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_package_metadata.py +0 -0
  50. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_pprof_builder.py +0 -0
  51. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_profiler_integration.py +0 -0
  52. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_protobuf_environment.py +0 -0
  53. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_runtime_scheduling.py +0 -0
  54. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_scheduler_resilience.py +0 -0
  55. {beacon_profiling-1.2.4 → beacon_profiling-1.2.6}/tests/test_stack_collector.py +0 -0
  56. {beacon_profiling-1.2.4 → 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.4
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.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,7 @@ 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`` (``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
- * ``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``
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=pprof
162
- export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
157
+ export OTEL_PROFILING_EXPORTER=file
158
+ export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
163
159
 
164
- To POST pprof directly to a compatible agent endpoint and keep a local
165
- copy:
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
- Capture and export exceptions are logged; subsequent scheduled captures
182
- and export attempts continue. Failed batches are not replayed automatically.
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
- 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.
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 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,7 @@ 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`` (``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
- * ``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``
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=pprof
135
- export OTEL_PROFILING_PPROF_PATH=/tmp/otel-profile
130
+ export OTEL_PROFILING_EXPORTER=file
131
+ export OTEL_PROFILING_FILE_PATH=/tmp/otel-profiles
136
132
 
137
- To POST pprof directly to a compatible agent endpoint and keep a local
138
- copy:
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
- Capture and export exceptions are logged; subsequent scheduled captures
155
- and export attempts continue. Failed batches are not replayed automatically.
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
- 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.
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.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,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
- OTEL_PROFILING_PPROF_PATH = "OTEL_PROFILING_PPROF_PATH"
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__ = ("CompatiblePPROFExporter", "create_otlp_profile_exporter")
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 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
 
@@ -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.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,
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: "Profiler | None" = None
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, CompatiblePPROFExporter):
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 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()
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: # noqa: ANN001
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.4"
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.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"
@@ -1,73 +1,48 @@
1
1
  from __future__ import annotations
2
2
 
3
- from opentelemetry.sdk.extension.profiling.export import (
4
- CompatiblePPROFExporter,
5
- )
6
- from opentelemetry.sdk.extension.profiling.export.pprof import (
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 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
- ):
14
+ def test_runtime_defaults_to_otlp(monkeypatch):
34
15
  sentinel = object()
35
- monkeypatch.setenv("OTEL_PROFILING_EXPORTER", "otlp")
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 test_runtime_uses_compatible_pprof_builder_for_http_exporter(tmp_path):
51
- profiler = Profiler(
52
- exporter=CompatiblePPROFExporter(pprof_path=str(tmp_path / "prof"))
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
- try:
67
- assert isinstance(profiler._builder, PprofProfileBuilder)
68
- assert not isinstance(profiler._builder, CompatiblePprofProfileBuilder)
69
- finally:
70
- profiler._exporter.shutdown()
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"