Skip to content

Commit a387eff

Browse files
committed
opentelemetry-exporter-otlp-pyproto-http: drop requests, use urllib
Replace requests.Session-based transport with stdlib urllib.request/ urllib.error across the trace, log and metric HTTP exporters, sharing a small _post/_build_ssl_context helper in _common.py. The credential- provider entry point's requests.Session type check is commented out (rather than removed) since there is no longer a requests.Session type to validate against; the session= constructor param and its wiring are dropped since there is no longer a Session object for it to populate.
1 parent 4ab7172 commit a387eff

10 files changed

Lines changed: 194 additions & 243 deletions

File tree

exporter/opentelemetry-exporter-otlp-pyproto-http/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ dependencies = [
1313
"opentelemetry-api ~= 1.12",
1414
"opentelemetry-sdk ~= 1.12",
1515
"opentelemetry-exporter-otlp-pyproto-common == 1.43.0.dev",
16-
"requests ~= 2.7",
1716
]
1817

1918
[project.entry-points.opentelemetry_traces_exporter]
Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,66 @@
11
# Copyright The OpenTelemetry Authors
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import ssl
45
from os import environ
56
from typing import Literal
6-
7-
from requests import Response, Session
7+
from urllib.error import HTTPError
8+
from urllib.request import Request, urlopen
89

910
from opentelemetry.sdk.environment_variables import (
1011
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER,
1112
)
1213
from opentelemetry.util._importlib_metadata import entry_points
1314

1415

15-
def _is_retryable(resp: Response) -> bool:
16-
if resp.status_code == 408:
16+
def _is_retryable(status_code: int) -> bool:
17+
if status_code == 408:
1718
return True
18-
if 500 <= resp.status_code <= 599:
19+
if 500 <= status_code <= 599:
1920
return True
2021
return False
2122

2223

24+
def _build_ssl_context(
25+
certificate_file: str | bool,
26+
client_cert: str | tuple[str, str | None] | None,
27+
) -> ssl.SSLContext:
28+
context = ssl.create_default_context(
29+
cafile=certificate_file if isinstance(certificate_file, str) else None
30+
)
31+
if certificate_file is False:
32+
context.check_hostname = False
33+
context.verify_mode = ssl.CERT_NONE
34+
if client_cert:
35+
certfile, keyfile = (
36+
client_cert if isinstance(client_cert, tuple) else (client_cert, None)
37+
)
38+
context.load_cert_chain(certfile, keyfile)
39+
return context
40+
41+
42+
def _post(
43+
url: str,
44+
data: bytes,
45+
headers: dict[str, str],
46+
timeout_sec: float,
47+
ssl_context: ssl.SSLContext,
48+
) -> tuple[int, str]:
49+
request = Request(url, data=data, headers=headers, method="POST")
50+
try:
51+
with urlopen(request, timeout=timeout_sec, context=ssl_context) as response:
52+
return response.status, response.reason
53+
except HTTPError as error:
54+
return error.code, error.reason
55+
56+
2357
def _load_session_from_envvar(
2458
cred_envvar: Literal[
2559
"OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER",
2660
"OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER",
2761
"OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER",
2862
],
29-
) -> Session | None:
63+
):
3064
_credential_env = environ.get(
3165
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER
3266
) or environ.get(cred_envvar)
@@ -45,11 +79,15 @@ def _load_session_from_envvar(
4579
f"Requested component '{_credential_env}' not found in "
4680
f"entry point 'opentelemetry_otlp_credential_provider'"
4781
)
48-
if isinstance(maybe_session, Session):
49-
return maybe_session
50-
else:
51-
raise RuntimeError(
52-
f"Requested component '{_credential_env}' is of type {type(maybe_session)}"
53-
f" must be of type `Session`."
54-
)
82+
# `requests` is no longer a dependency of this package, so the
83+
# provider's return value can no longer be verified here.
84+
# from requests import Session
85+
# if isinstance(maybe_session, Session):
86+
# return maybe_session
87+
# else:
88+
# raise RuntimeError(
89+
# f"Requested component '{_credential_env}' is of type {type(maybe_session)}"
90+
# f" must be of type `Session`."
91+
# )
92+
return maybe_session
5593
return None

exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/__init__.py

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
from random import uniform
1111
from threading import Event
1212
from time import time
13+
from urllib.error import URLError
1314
from urllib.parse import urlparse
1415
from zlib import compress
1516

16-
from requests import Session
17-
from requests.exceptions import ConnectionError, RequestException
18-
1917
from opentelemetry.exporter.otlp.pyproto.common._exporter_metrics import (
2018
create_exporter_metrics,
2119
)
@@ -27,8 +25,9 @@
2725
Compression,
2826
)
2927
from opentelemetry.exporter.otlp.pyproto.http._common import (
28+
_build_ssl_context,
3029
_is_retryable,
31-
_load_session_from_envvar,
30+
_post,
3231
)
3332
from opentelemetry.metrics import MeterProvider
3433
from opentelemetry.sdk._logs import ReadableLogRecord
@@ -38,7 +37,6 @@
3837
)
3938
from opentelemetry.sdk._shared_internal import DuplicateFilter
4039
from opentelemetry.sdk.environment_variables import (
41-
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER,
4240
OTEL_EXPORTER_OTLP_CERTIFICATE,
4341
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
4442
OTEL_EXPORTER_OTLP_CLIENT_KEY,
@@ -82,7 +80,6 @@ def __init__(
8280
headers: dict[str, str] | None = None,
8381
timeout: float | None = None,
8482
compression: Compression | None = None,
85-
session: Session | None = None,
8683
*,
8784
meter_provider: MeterProvider | None = None,
8885
):
@@ -122,20 +119,12 @@ def __init__(
122119
)
123120
)
124121
self._compression = compression or _compression_from_env()
125-
self._session = (
126-
session
127-
or _load_session_from_envvar(
128-
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER
129-
)
130-
or Session()
131-
)
132-
self._session.headers.update(self._headers)
133-
self._session.headers.update(_OTLP_HTTP_HEADERS)
134-
self._session.headers.update(self._headers)
122+
self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers}
135123
if self._compression is not Compression.NoCompression:
136-
self._session.headers.update(
137-
{"Content-Encoding": self._compression.value}
138-
)
124+
self._request_headers["Content-Encoding"] = self._compression.value
125+
self._ssl_context = _build_ssl_context(
126+
self._certificate_file, self._client_cert
127+
)
139128
self._shutdown = False
140129

141130
self._metrics = create_exporter_metrics(
@@ -161,22 +150,21 @@ def _export(self, serialized_data: bytes, timeout_sec: float | None = None):
161150
if timeout_sec is None:
162151
timeout_sec = self._timeout
163152
try:
164-
resp = self._session.post(
165-
url=self._endpoint,
166-
data=data,
167-
verify=self._certificate_file,
168-
timeout=timeout_sec,
169-
cert=self._client_cert,
153+
return _post(
154+
self._endpoint,
155+
data,
156+
self._request_headers,
157+
timeout_sec,
158+
self._ssl_context,
170159
)
171-
except ConnectionError:
172-
resp = self._session.post(
173-
url=self._endpoint,
174-
data=data,
175-
verify=self._certificate_file,
176-
timeout=timeout_sec,
177-
cert=self._client_cert,
160+
except URLError:
161+
return _post(
162+
self._endpoint,
163+
data,
164+
self._request_headers,
165+
timeout_sec,
166+
self._ssl_context,
178167
)
179-
return resp
180168

181169
def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult:
182170
if self._shutdown:
@@ -190,18 +178,17 @@ def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult:
190178
backoff_seconds = 2**retry_num * uniform(0.8, 1.2)
191179
export_error: Exception | None = None
192180
try:
193-
resp = self._export(serialized_data, deadline_sec - time())
194-
if resp.ok:
181+
status_code, reason = self._export(
182+
serialized_data, deadline_sec - time()
183+
)
184+
if status_code < 400:
195185
return LogRecordExportResult.SUCCESS
196-
except RequestException as error:
197-
reason = error
186+
retryable = _is_retryable(status_code)
187+
except URLError as error:
188+
reason = error.reason
198189
export_error = error
199-
retryable = isinstance(error, ConnectionError)
190+
retryable = True
200191
status_code = None
201-
else:
202-
reason = resp.reason
203-
retryable = _is_retryable(resp)
204-
status_code = resp.status_code
205192

206193
if not retryable:
207194
_logger.error(
@@ -254,7 +241,6 @@ def shutdown(self):
254241
return
255242
self._shutdown = True
256243
self._shutdown_is_occuring.set()
257-
self._session.close()
258244

259245

260246
def _compression_from_env() -> Compression:

exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/__init__.py

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111
from random import uniform
1212
from threading import Event
1313
from time import time
14+
from urllib.error import URLError
1415
from urllib.parse import urlparse
1516
from zlib import compress
1617

17-
from requests import Session
18-
from requests.exceptions import ConnectionError, RequestException
19-
2018
from opentelemetry.exporter.otlp.pyproto.common._exporter_metrics import (
2119
create_exporter_metrics,
2220
)
@@ -29,8 +27,9 @@
2927
Compression,
3028
)
3129
from opentelemetry.exporter.otlp.pyproto.http._common import (
30+
_build_ssl_context,
3231
_is_retryable,
33-
_load_session_from_envvar,
32+
_post,
3433
)
3534
from opentelemetry.metrics import MeterProvider
3635
from opentelemetry.pyproto.collector.metrics.v1.metrics_service_pypb2 import (
@@ -47,7 +46,6 @@
4746
Summary,
4847
)
4948
from opentelemetry.sdk.environment_variables import (
50-
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER,
5149
OTEL_EXPORTER_OTLP_CERTIFICATE,
5250
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
5351
OTEL_EXPORTER_OTLP_CLIENT_KEY,
@@ -97,7 +95,6 @@ def __init__(
9795
headers: dict[str, str] | None = None,
9896
timeout: float | None = None,
9997
compression: Compression | None = None,
100-
session: Session | None = None,
10198
preferred_temporality: dict[type, AggregationTemporality] | None = None,
10299
preferred_aggregation: dict[type, Aggregation] | None = None,
103100
max_export_batch_size: int | None = None,
@@ -140,20 +137,12 @@ def __init__(
140137
)
141138
)
142139
self._compression = compression or _compression_from_env()
143-
self._session = (
144-
session
145-
or _load_session_from_envvar(
146-
_OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER
147-
)
148-
or Session()
149-
)
150-
self._session.headers.update(self._headers)
151-
self._session.headers.update(_OTLP_HTTP_HEADERS)
152-
self._session.headers.update(self._headers)
140+
self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers}
153141
if self._compression is not Compression.NoCompression:
154-
self._session.headers.update(
155-
{"Content-Encoding": self._compression.value}
156-
)
142+
self._request_headers["Content-Encoding"] = self._compression.value
143+
self._ssl_context = _build_ssl_context(
144+
self._certificate_file, self._client_cert
145+
)
157146
self._common_configuration(preferred_temporality, preferred_aggregation)
158147
self._max_export_batch_size = max_export_batch_size
159148
self._shutdown = False
@@ -181,22 +170,21 @@ def _export(self, serialized_data: bytes, timeout_sec: float | None = None):
181170
if timeout_sec is None:
182171
timeout_sec = self._timeout
183172
try:
184-
resp = self._session.post(
185-
url=self._endpoint,
186-
data=data,
187-
verify=self._certificate_file,
188-
timeout=timeout_sec,
189-
cert=self._client_cert,
173+
return _post(
174+
self._endpoint,
175+
data,
176+
self._request_headers,
177+
timeout_sec,
178+
self._ssl_context,
190179
)
191-
except ConnectionError:
192-
resp = self._session.post(
193-
url=self._endpoint,
194-
data=data,
195-
verify=self._certificate_file,
196-
timeout=timeout_sec,
197-
cert=self._client_cert,
180+
except URLError:
181+
return _post(
182+
self._endpoint,
183+
data,
184+
self._request_headers,
185+
timeout_sec,
186+
self._ssl_context,
198187
)
199-
return resp
200188

201189
def _export_with_retries(
202190
self,
@@ -210,18 +198,17 @@ def _export_with_retries(
210198
backoff_seconds = 2**retry_num * uniform(0.8, 1.2)
211199
export_error: Exception | None = None
212200
try:
213-
resp = self._export(serialized_data, deadline_sec - time())
214-
if resp.ok:
201+
status_code, reason = self._export(
202+
serialized_data, deadline_sec - time()
203+
)
204+
if status_code < 400:
215205
return MetricExportResult.SUCCESS
216-
except RequestException as error:
217-
reason = error
206+
retryable = _is_retryable(status_code)
207+
except URLError as error:
208+
reason = error.reason
218209
export_error = error
219-
retryable = isinstance(error, ConnectionError)
210+
retryable = True
220211
status_code = None
221-
else:
222-
reason = resp.reason
223-
retryable = _is_retryable(resp)
224-
status_code = resp.status_code
225212

226213
if not retryable:
227214
_logger.error(
@@ -299,7 +286,6 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
299286
return
300287
self._shutdown = True
301288
self._shutdown_in_progress.set()
302-
self._session.close()
303289

304290
def force_flush(self, timeout_millis: float = 10_000) -> bool:
305291
return True

0 commit comments

Comments
 (0)