Skip to content

opentelemetry-exporter-otlp-proto-http: auto-append signal path when base URL is passed as endpoint#5273

Open
aleksei140888 wants to merge 3 commits into
open-telemetry:mainfrom
aleksei140888:fix/otlp-http-base-url-auto-path
Open

opentelemetry-exporter-otlp-proto-http: auto-append signal path when base URL is passed as endpoint#5273
aleksei140888 wants to merge 3 commits into
open-telemetry:mainfrom
aleksei140888:fix/otlp-http-base-url-auto-path

Conversation

@aleksei140888

Copy link
Copy Markdown

Description

When switching from the gRPC OTLP exporter to the HTTP OTLP exporter, users expect to provide a single base URL (e.g. http://collector:4318) and have each exporter automatically route to the correct signal endpoint. With gRPC this works because routing is determined by the service stub — the URL path is irrelevant. With HTTP, the path matters.

Currently, passing endpoint directly to OTLPSpanExporter, OTLPMetricExporter, or OTLPLogExporter uses the value as-is, with no signal path appended:

# Sends to http://collector:4318 — incorrect (no /v1/traces path)
OTLPSpanExporter(endpoint="http://collector:4318")

But the OTEL_EXPORTER_OTLP_ENDPOINT environment variable already auto-appends signal paths. This PR makes the programmatic endpoint= parameter behave the same way.

Change

If endpoint is provided without a meaningful path (empty path or just /), it is treated as a base URL and the signal-specific path is appended automatically:

  • OTLPSpanExporter(endpoint="http://host:4318")http://host:4318/v1/traces
  • OTLPSpanExporter(endpoint="http://host:4318/")http://host:4318/v1/traces
  • OTLPSpanExporter(endpoint="http://host:4318/v1/traces") → unchanged (backward compatible)
  • OTLPSpanExporter(endpoint="http://host:4318/custom") → unchanged (explicit custom path)

The same logic applies to OTLPMetricExporter (/v1/metrics) and OTLPLogExporter (/v1/logs).

A shared helper _is_base_endpoint() is added to the _common module to avoid duplicating the detection logic.

Testing

  • Added 4 test cases per exporter (12 total): base URL without slash, base URL with trailing slash, full signal URL (unchanged), custom path (unchanged)
  • All existing tests pass
uv run tox -e py312-test-opentelemetry-exporter-otlp-proto-http
# 70 passed

…ndpoint

When `endpoint` is passed to OTLPSpanExporter, OTLPMetricExporter, or
OTLPLogExporter without a signal-specific path (empty path or just `/`),
treat it as a base URL and append /v1/traces, /v1/metrics, or /v1/logs
respectively — consistent with how OTEL_EXPORTER_OTLP_ENDPOINT behaves.

This makes switching from gRPC to HTTP exporters easier: users can pass
the same base URL style (e.g. http://host:4318) without having to
manually construct signal-specific URLs.

Assisted-by: Claude Sonnet 4.6
@aleksei140888 aleksei140888 requested a review from a team as a code owner June 4, 2026 13:48
@linux-foundation-easycla

linux-foundation-easycla Bot commented Jun 4, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cdec295aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Replace _is_base_endpoint + string-concatenation with
_resolve_endpoint_to_signal that uses urlparse/urlunparse, so that
an endpoint like http://host:4318?tenant=acme becomes
http://host:4318/v1/traces?tenant=acme rather than the malformed
http://host:4318?tenant=acme/v1/traces.

Assisted-by: Claude Sonnet 4.6
@herin049

herin049 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

AFAIK when an endpoint is passed directly via the constructor of the exporters, it should be treated as is. I'm not sure if this is something we want to support given that it makes the behavior of this parameter somewhat convoluted.

@aleksei140888

Copy link
Copy Markdown
Author

AFAIK when an endpoint is passed directly via the constructor of the exporters, it should be treated as is. I'm not sure if this is something we want to support given that it makes the behavior of this parameter somewhat convoluted.

Worth noting that the gRPC exporter also doesn't use the constructor endpoint as-is. It silently strips the scheme and path, keeping only host:port. So from the user's perspective, neither transport treats the parameter verbatim today.

This PR applies the same normalization idea to HTTP: if no signal path is present, treat it as a base URL and append the correct one. If a path is already there, it's left untouched.

The main use case is switching from gRPC to HTTP without having to rethink the endpoint config. With gRPC you just pass http://host:4317 and it works, with HTTP you'd have to manually split it into three signal-specific URLs.

@herin049

herin049 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

AFAIK when an endpoint is passed directly via the constructor of the exporters, it should be treated as is. I'm not sure if this is something we want to support given that it makes the behavior of this parameter somewhat convoluted.

Worth noting that the gRPC exporter also doesn't use the constructor endpoint as-is. It silently strips the scheme and path, keeping only host:port. So from the user's perspective, neither transport treats the parameter verbatim today.

This PR applies the same normalization idea to HTTP: if no signal path is present, treat it as a base URL and append the correct one. If a path is already there, it's left untouched.

The main use case is switching from gRPC to HTTP without having to rethink the endpoint config. With gRPC you just pass http://host:4317 and it works, with HTTP you'd have to manually split it into three signal-specific URLs.

Taking a look at other SDK implementations, it looks like Java and JavaScript require the full endpoint being passed in. While not an explicit requirement, the preference is to keep the APIs across languages similar unless there is a strong reason to deviate.

My other concern here is that (while likely very rare) there may be users which have a signal specific listener configured with no HTTP path. With these changes there would be no way to configure the endpoint anymore to export without a path being appended.

I'd potentially be open to adding an optional endpoint_base parameter that would be treated as a base endpoint like you want. I'd like to hear what other maintainers think as well.

Uses proper URL manipulation so query strings and fragments are preserved.
If the endpoint already has a path other than '/', it is returned unchanged.
"""
parsed = urlparse(endpoint)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably embed this in a try-catch to avoid crashes.

If the endpoint already has a path other than '/', it is returned unchanged.
"""
parsed = urlparse(endpoint)
if not parsed.path or parsed.path == "/":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use hasattr to check if path exists or not.

)
self.assertIsInstance(exporter._session, requests.Session)

def test_endpoint_base_url_no_path(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add tests for some malformed, invalid urls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants