Repository: open-telemetry/opentelemetry-python

Issue:
OTLP HTTP Exporter should append `v1/{signal}` to the endpoint URL when non-per-signal env var is used
Per the spec re: [Endpoint URLs for OTLP/HTTP](https://github.com/open-telemetry/opentelemetry-specification/blob/b7473b5de0f55f921f896948442ebb274f58b584/specification/protocol/exporter.md#per-signal-urls), when the non-per-signal endpoint env var (`OTEL_EXPORTER_OTLP_ENDPOINT`) is set, the exporter *must* construct per-signal URLs (either `v1/traces` or `v1/metrics`). 

Currently, the [exporter does not do this](https://github.com/open-telemetry/opentelemetry-python/blob/80f5a20ba8f3a71450fe3020fecf362fedb76bff/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py#L68); `v1/traces` must be manually added to the end point when setting only `OTEL_EXPORTER_OTLP_ENDPOINT`. Not doing so produces a 404 error when attempting to export spans. 



Discussion:
😅  i thought i was going crazy, i can submit a PR to make this bit smarter, it _might_ be considered a breaking change though 😢  so idk what all will be involved in merging this.
@flyinprogrammer I wanted to see if you plan on submitting a PR for this? I'd like to take a stab at it with my colleague otherwise. :) 
By all means submit one, I am swamped at work right now!

On Thu, Feb 24, 2022, 5:53 PM Reese Lee ***@***.***> wrote:

> @flyinprogrammer <https://github.com/flyinprogrammer> I wanted to see if
> you plan on submitting a PR for this? I'd like to take a stab at it with my
> colleague otherwise. :)
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/open-telemetry/opentelemetry-python/issues/2443#issuecomment-1050372627>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAOCM36KQKUASP6X5E7UY4DU43AGHANCNFSM5NTH35EQ>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

Thank you for the update! @TylerHelmuth and I will work on a PR for this. 

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py
@@ -47,7 +47,8 @@
 
 
 DEFAULT_COMPRESSION = Compression.NoCompression
-DEFAULT_ENDPOINT = "http://localhost:4318/v1/traces"
+DEFAULT_ENDPOINT = "http://localhost:4318/"
+DEFAULT_TRACES_EXPORT_PATH = "v1/traces"
 DEFAULT_TIMEOUT = 10  # in seconds
 
 
@@ -65,7 +66,9 @@ def __init__(
     ):
         self._endpoint = endpoint or environ.get(
             OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
-            environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT),
+            _append_trace_path(
+                environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
+            ),
         )
         self._certificate_file = certificate_file or environ.get(
             OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
@@ -172,3 +175,9 @@ def _compression_from_env() -> Compression:
         .strip()
     )
     return Compression(compression)
+
+
+def _append_trace_path(endpoint: str) -> str:
+    if endpoint.endswith("/"):
+        return endpoint + DEFAULT_TRACES_EXPORT_PATH
+    return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}"


Test patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
@@ -20,6 +20,7 @@
     DEFAULT_COMPRESSION,
     DEFAULT_ENDPOINT,
     DEFAULT_TIMEOUT,
+    DEFAULT_TRACES_EXPORT_PATH,
     OTLPSpanExporter,
 )
 from opentelemetry.sdk.environment_variables import (
@@ -47,7 +48,9 @@ def test_constructor_default(self):
 
         exporter = OTLPSpanExporter()
 
-        self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT)
+        self.assertEqual(
+            exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH
+        )
         self.assertEqual(exporter._certificate_file, True)
         self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT)
         self.assertIs(exporter._compression, DEFAULT_COMPRESSION)
@@ -90,6 +93,7 @@ def test_exporter_traces_env_take_priority(self):
             OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE,
             OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value,
             OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT,
+            OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env",
             OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS,
             OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT,
         },
@@ -117,7 +121,6 @@ def test_exporter_constructor_take_priority(self):
         {
             OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE,
             OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value,
-            OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT,
             OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS,
             OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT,
         },
@@ -126,7 +129,6 @@ def test_exporter_env(self):
 
         exporter = OTLPSpanExporter()
 
-        self.assertEqual(exporter._endpoint, OS_ENV_ENDPOINT)
         self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE)
         self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT))
         self.assertIs(exporter._compression, Compression.Gzip)
@@ -134,6 +136,32 @@ def test_exporter_env(self):
             exporter._headers, {"envheader1": "val1", "envheader2": "val2"}
         )
 
+    @patch.dict(
+        "os.environ",
+        {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT},
+    )
+    def test_exporter_env_endpoint_without_slash(self):
+
+        exporter = OTLPSpanExporter()
+
+        self.assertEqual(
+            exporter._endpoint,
+            OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}",
+        )
+
+    @patch.dict(
+        "os.environ",
+        {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"},
+    )
+    def test_exporter_env_endpoint_with_slash(self):
+
+        exporter = OTLPSpanExporter()
+
+        self.assertEqual(
+            exporter._endpoint,
+            OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}",
+        )
+
     @patch.dict(
         "os.environ",
         {