Skip to content

Commit 80b1424

Browse files
committed
fix: sdk should add /v1/traces and /v1/metrics endpoints implicitly
This PR fixes #4412 which should add metric and trace endpoints implicitly when endpoint argument is passed. OTel sdk general config https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint
1 parent 382fa46 commit 80b1424

File tree

7 files changed

+204
-19
lines changed

7 files changed

+204
-19
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-exporter-otlp-proto-http`: Fix HTTP OTLP exporters to automatically append signal path
16+
(`/v1/traces`, `/v1/metrics`, `/v1/logs`) to user-provided endpoints per the OpenTelemetry specification.
17+
Previously, users had to manually include these paths in their endpoint configuration.
18+
([#3200](https://github.com/open-telemetry/opentelemetry-python/issues/3200))
19+
([#4412](https://github.com/open-telemetry/opentelemetry-python/issues/4412))
1520
- `opentelemetry-api`: Convert objects of any type other than AnyValue in attributes to string to be exportable
1621
([#4808](https://github.com/open-telemetry/opentelemetry-python/pull/4808))
1722
- docs: Added sqlcommenter example

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,16 @@ def __init__(
8585
):
8686
self._shutdown_is_occuring = threading.Event()
8787
self._endpoint = endpoint or environ.get(
88-
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
89-
_append_logs_path(
90-
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
91-
),
88+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
9289
)
90+
if self._endpoint:
91+
# User-provided endpoint or signal-specific env var - append path if not present
92+
self._endpoint = _append_logs_path(self._endpoint)
93+
else:
94+
# Use general endpoint with path appended per spec
95+
self._endpoint = _append_logs_path(
96+
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
97+
)
9398
# Keeping these as instance variables because they are used in tests
9499
self._certificate_file = certificate_file or environ.get(
95100
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
@@ -240,6 +245,9 @@ def _compression_from_env() -> Compression:
240245

241246

242247
def _append_logs_path(endpoint: str) -> str:
248+
# Don't append path if it's already present
249+
if endpoint.endswith(f"/{DEFAULT_LOGS_EXPORT_PATH}"):
250+
return endpoint
243251
if endpoint.endswith("/"):
244252
return endpoint + DEFAULT_LOGS_EXPORT_PATH
245253
return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}"

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,16 @@ def __init__(
125125
):
126126
self._shutdown_in_progress = threading.Event()
127127
self._endpoint = endpoint or environ.get(
128-
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
129-
_append_metrics_path(
130-
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
131-
),
128+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
132129
)
130+
if self._endpoint:
131+
# User-provided endpoint or signal-specific env var - append path if not present
132+
self._endpoint = _append_metrics_path(self._endpoint)
133+
else:
134+
# Use general endpoint with path appended per spec
135+
self._endpoint = _append_metrics_path(
136+
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
137+
)
133138
self._certificate_file = certificate_file or environ.get(
134139
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
135140
environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True),
@@ -300,6 +305,9 @@ def _compression_from_env() -> Compression:
300305

301306

302307
def _append_metrics_path(endpoint: str) -> str:
308+
# Don't append path if it's already present
309+
if endpoint.endswith(f"/{DEFAULT_METRICS_EXPORT_PATH}"):
310+
return endpoint
303311
if endpoint.endswith("/"):
304312
return endpoint + DEFAULT_METRICS_EXPORT_PATH
305313
return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}"

exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,16 @@ def __init__(
8181
):
8282
self._shutdown_in_progress = threading.Event()
8383
self._endpoint = endpoint or environ.get(
84-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
85-
_append_trace_path(
86-
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
87-
),
84+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
8885
)
86+
if self._endpoint:
87+
# User-provided endpoint or signal-specific env var - append path if not present
88+
self._endpoint = _append_trace_path(self._endpoint)
89+
else:
90+
# Use general endpoint with path appended per spec
91+
self._endpoint = _append_trace_path(
92+
environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
93+
)
8994
self._certificate_file = certificate_file or environ.get(
9095
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
9196
environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True),
@@ -233,6 +238,9 @@ def _compression_from_env() -> Compression:
233238

234239

235240
def _append_trace_path(endpoint: str) -> str:
241+
# Don't append path if it's already present
242+
if endpoint.endswith(f"/{DEFAULT_TRACES_EXPORT_PATH}"):
243+
return endpoint
236244
if endpoint.endswith("/"):
237245
return endpoint + DEFAULT_TRACES_EXPORT_PATH
238246
return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}"

exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
OS_ENV_TIMEOUT = "30"
8888

8989

90-
# pylint: disable=protected-access
90+
# pylint: disable=protected-access,too-many-public-methods
9191
class TestOTLPMetricExporter(TestCase):
9292
def setUp(self):
9393
self.metrics = {
@@ -170,7 +170,10 @@ def f():
170170
)
171171
exporter = OTLPMetricExporter()
172172

173-
self.assertEqual(exporter._endpoint, "https://metrics.endpoint.env")
173+
self.assertEqual(
174+
exporter._endpoint,
175+
f"https://metrics.endpoint.env/{DEFAULT_METRICS_EXPORT_PATH}",
176+
)
174177
self.assertEqual(exporter._certificate_file, "metrics/certificate.env")
175178
self.assertEqual(
176179
exporter._client_certificate_file, "metrics/client-cert.pem"
@@ -222,7 +225,10 @@ def test_exporter_constructor_take_priority(self):
222225
session=Session(),
223226
)
224227

225-
self.assertEqual(exporter._endpoint, "example.com/1234")
228+
self.assertEqual(
229+
exporter._endpoint,
230+
f"example.com/1234/{DEFAULT_METRICS_EXPORT_PATH}",
231+
)
226232
self.assertEqual(exporter._certificate_file, "path/to/service.crt")
227233
self.assertEqual(
228234
exporter._client_certificate_file, "path/to/client-cert.pem"
@@ -290,6 +296,52 @@ def test_exporter_env_endpoint_with_slash(self):
290296
OS_ENV_ENDPOINT + f"/{DEFAULT_METRICS_EXPORT_PATH}",
291297
)
292298

299+
def test_exporter_constructor_endpoint_with_path_appended(self):
300+
"""Test that path is appended to user-provided endpoint."""
301+
exporter = OTLPMetricExporter(
302+
endpoint="http://collector.example.com:4318"
303+
)
304+
self.assertEqual(
305+
exporter._endpoint,
306+
f"http://collector.example.com:4318/{DEFAULT_METRICS_EXPORT_PATH}",
307+
)
308+
309+
def test_exporter_constructor_endpoint_no_duplicate_path(self):
310+
"""Test that path is not duplicated if already present."""
311+
exporter = OTLPMetricExporter(
312+
endpoint=f"http://collector.example.com:4318/{DEFAULT_METRICS_EXPORT_PATH}"
313+
)
314+
self.assertEqual(
315+
exporter._endpoint,
316+
f"http://collector.example.com:4318/{DEFAULT_METRICS_EXPORT_PATH}",
317+
)
318+
319+
@patch.dict(
320+
"os.environ",
321+
{OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "http://metrics.collector:4318"},
322+
)
323+
def test_exporter_signal_specific_env_endpoint_with_path_appended(self):
324+
"""Test that path is appended to signal-specific endpoint."""
325+
exporter = OTLPMetricExporter()
326+
self.assertEqual(
327+
exporter._endpoint,
328+
f"http://metrics.collector:4318/{DEFAULT_METRICS_EXPORT_PATH}",
329+
)
330+
331+
@patch.dict(
332+
"os.environ",
333+
{
334+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: f"http://metrics.collector:4318/{DEFAULT_METRICS_EXPORT_PATH}"
335+
},
336+
)
337+
def test_exporter_signal_specific_env_endpoint_no_duplicate_path(self):
338+
"""Test that path is not duplicated when signal-specific endpoint already has path."""
339+
exporter = OTLPMetricExporter()
340+
self.assertEqual(
341+
exporter._endpoint,
342+
f"http://metrics.collector:4318/{DEFAULT_METRICS_EXPORT_PATH}",
343+
)
344+
293345
@patch.dict(
294346
"os.environ",
295347
{

exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ def f():
132132
)
133133
exporter = OTLPLogExporter()
134134

135-
self.assertEqual(exporter._endpoint, "https://logs.endpoint.env")
135+
self.assertEqual(
136+
exporter._endpoint,
137+
f"https://logs.endpoint.env/{DEFAULT_LOGS_EXPORT_PATH}",
138+
)
136139
self.assertEqual(exporter._certificate_file, "logs/certificate.env")
137140
self.assertEqual(
138141
exporter._client_certificate_file, "logs/client-cert.pem"
@@ -214,7 +217,10 @@ def test_exporter_constructor_take_priority(self):
214217
session=sess(),
215218
)
216219

217-
self.assertEqual(exporter._endpoint, "endpoint.local:69/logs")
220+
self.assertEqual(
221+
exporter._endpoint,
222+
f"endpoint.local:69/logs/{DEFAULT_LOGS_EXPORT_PATH}",
223+
)
218224
self.assertEqual(exporter._certificate_file, "/hello.crt")
219225
self.assertEqual(exporter._client_certificate_file, "/client-cert.pem")
220226
self.assertEqual(exporter._client_key_file, "/client-key.pem")
@@ -261,6 +267,52 @@ def test_exporter_env(self):
261267
)
262268
self.assertIsInstance(exporter._session, requests.Session)
263269

270+
def test_exporter_constructor_endpoint_with_path_appended(self):
271+
"""Test that path is appended to user-provided endpoint."""
272+
exporter = OTLPLogExporter(
273+
endpoint="http://collector.example.com:4318"
274+
)
275+
self.assertEqual(
276+
exporter._endpoint,
277+
f"http://collector.example.com:4318/{DEFAULT_LOGS_EXPORT_PATH}",
278+
)
279+
280+
def test_exporter_constructor_endpoint_no_duplicate_path(self):
281+
"""Test that path is not duplicated if already present."""
282+
exporter = OTLPLogExporter(
283+
endpoint=f"http://collector.example.com:4318/{DEFAULT_LOGS_EXPORT_PATH}"
284+
)
285+
self.assertEqual(
286+
exporter._endpoint,
287+
f"http://collector.example.com:4318/{DEFAULT_LOGS_EXPORT_PATH}",
288+
)
289+
290+
@patch.dict(
291+
"os.environ",
292+
{OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://logs.collector:4318"},
293+
)
294+
def test_exporter_signal_specific_env_endpoint_with_path_appended(self):
295+
"""Test that path is appended to signal-specific endpoint."""
296+
exporter = OTLPLogExporter()
297+
self.assertEqual(
298+
exporter._endpoint,
299+
f"http://logs.collector:4318/{DEFAULT_LOGS_EXPORT_PATH}",
300+
)
301+
302+
@patch.dict(
303+
"os.environ",
304+
{
305+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: f"http://logs.collector:4318/{DEFAULT_LOGS_EXPORT_PATH}"
306+
},
307+
)
308+
def test_exporter_signal_specific_env_endpoint_no_duplicate_path(self):
309+
"""Test that path is not duplicated when signal-specific endpoint already has path."""
310+
exporter = OTLPLogExporter()
311+
self.assertEqual(
312+
exporter._endpoint,
313+
f"http://logs.collector:4318/{DEFAULT_LOGS_EXPORT_PATH}",
314+
)
315+
264316
@staticmethod
265317
def export_log_and_deserialize(log):
266318
with patch("requests.Session.post") as mock_post:

exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ def f():
127127
)
128128
exporter = OTLPSpanExporter()
129129

130-
self.assertEqual(exporter._endpoint, "https://traces.endpoint.env")
130+
self.assertEqual(
131+
exporter._endpoint,
132+
f"https://traces.endpoint.env/{DEFAULT_TRACES_EXPORT_PATH}",
133+
)
131134
self.assertEqual(exporter._certificate_file, "traces/certificate.env")
132135
self.assertEqual(
133136
exporter._client_certificate_file, "traces/client-cert.pem"
@@ -180,7 +183,10 @@ def test_exporter_constructor_take_priority(self):
180183
session=requests.Session(),
181184
)
182185

183-
self.assertEqual(exporter._endpoint, "example.com/1234")
186+
self.assertEqual(
187+
exporter._endpoint,
188+
f"example.com/1234/{DEFAULT_TRACES_EXPORT_PATH}",
189+
)
184190
self.assertEqual(exporter._certificate_file, "path/to/service.crt")
185191
self.assertEqual(
186192
exporter._client_certificate_file, "path/to/client-cert.pem"
@@ -248,6 +254,52 @@ def test_exporter_env_endpoint_with_slash(self):
248254
OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}",
249255
)
250256

257+
def test_exporter_constructor_endpoint_with_path_appended(self):
258+
"""Test that path is appended to user-provided endpoint."""
259+
exporter = OTLPSpanExporter(
260+
endpoint="http://collector.example.com:4318"
261+
)
262+
self.assertEqual(
263+
exporter._endpoint,
264+
f"http://collector.example.com:4318/{DEFAULT_TRACES_EXPORT_PATH}",
265+
)
266+
267+
def test_exporter_constructor_endpoint_no_duplicate_path(self):
268+
"""Test that path is not duplicated if already present."""
269+
exporter = OTLPSpanExporter(
270+
endpoint=f"http://collector.example.com:4318/{DEFAULT_TRACES_EXPORT_PATH}"
271+
)
272+
self.assertEqual(
273+
exporter._endpoint,
274+
f"http://collector.example.com:4318/{DEFAULT_TRACES_EXPORT_PATH}",
275+
)
276+
277+
@patch.dict(
278+
"os.environ",
279+
{OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://traces.collector:4318"},
280+
)
281+
def test_exporter_signal_specific_env_endpoint_with_path_appended(self):
282+
"""Test that path is appended to signal-specific endpoint."""
283+
exporter = OTLPSpanExporter()
284+
self.assertEqual(
285+
exporter._endpoint,
286+
f"http://traces.collector:4318/{DEFAULT_TRACES_EXPORT_PATH}",
287+
)
288+
289+
@patch.dict(
290+
"os.environ",
291+
{
292+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: f"http://traces.collector:4318/{DEFAULT_TRACES_EXPORT_PATH}"
293+
},
294+
)
295+
def test_exporter_signal_specific_env_endpoint_no_duplicate_path(self):
296+
"""Test that path is not duplicated when signal-specific endpoint already has path."""
297+
exporter = OTLPSpanExporter()
298+
self.assertEqual(
299+
exporter._endpoint,
300+
f"http://traces.collector:4318/{DEFAULT_TRACES_EXPORT_PATH}",
301+
)
302+
251303
@patch.dict(
252304
"os.environ",
253305
{

0 commit comments

Comments
 (0)