Skip to content

[EXPORTER] Add custom HTTP client - #4071

Merged
marcalff merged 41 commits into
open-telemetry:mainfrom
ltowarek:fix/issue-24-custom-http-client
Jun 12, 2026
Merged

[EXPORTER] Add custom HTTP client#4071
marcalff merged 41 commits into
open-telemetry:mainfrom
ltowarek:fix/issue-24-custom-http-client

Conversation

@ltowarek

@ltowarek ltowarek commented May 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #2084

Changes

Adds a way to provide a custom HTTP client instead of the default libcurl-based client.

The main motivation are embedded and cross-compiled targets where libcurl is unavailable. A working example for ESP32S3 using ESP-IDF's esp_http_client as the transport backend is at ltowarek/esp-opentelemetry-cpp#30.

@linux-foundation-easycla

linux-foundation-easycla Bot commented May 8, 2026

Copy link
Copy Markdown

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: ltowarek / name: Lukasz Towarek (a323792408764f461e2d16828358376c59f3dacb)
  • ✅ login: ThomsonTan / name: Tom Tan (07d64830d450772c6a829e7ed921a5a804d05c1e, 8b2112a72538927dbcf8d6ae73d450fdc399df15, a5264df01d0ab7e5abfaa4c229a3c378c15a0a91)

@ltowarek
ltowarek marked this pull request as ready for review May 8, 2026 21:40
@ltowarek
ltowarek requested a review from a team as a code owner May 8, 2026 21:40
@owent

owent commented May 11, 2026

Copy link
Copy Markdown
Member

Could you please add a test to use a simple custom http client?

@lalitb

lalitb commented May 11, 2026

Copy link
Copy Markdown
Member

Thanks @ltowarek. Couple of review comments as of now -

  1. As @owent mentioned, could we also add a minimal example for this option, even if the transport is just a no-op custom HttpClient / HttpClientSync? The important part is showing where the user provides HttpClientFactory::Create() and CreateSync(), and how their target is linked with the OTLP HTTP exporter.

  2. Also, I believe there are still a few CMake paths that bring in curl directly. For example, Zipkin, Elasticsearch, HTTP examples, and the W3C tracecontext HTTP test server still link opentelemetry_http_client_curl / CURL::libcurl, while WITH_CUSTOM_HTTP_CLIENT=ON disables the curl client target globally. Can we either make this option explicitly incompatible with those curl-only paths, or update them to follow the same custom-client path?

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.56%. Comparing base (c98d949) to head (aba37bf).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4071      +/-   ##
==========================================
+ Coverage   82.08%   82.56%   +0.49%     
==========================================
  Files         386      389       +3     
  Lines       16211    16617     +406     
==========================================
+ Hits        13305    13718     +413     
+ Misses       2906     2899       -7     
Files with missing lines Coverage Δ
...y/exporters/elasticsearch/es_log_record_exporter.h 100.00% <ø> (+100.00%) ⬆️
...orters/elasticsearch/src/es_log_record_exporter.cc 12.22% <100.00%> (+12.22%) ⬆️
...de/opentelemetry/exporters/otlp/otlp_http_client.h 100.00% <ø> (ø)
exporters/otlp/src/otlp_http_client.cc 67.50% <100.00%> (-0.06%) ⬇️
exporters/otlp/src/otlp_http_exporter.cc 99.56% <100.00%> (+0.51%) ⬆️
exporters/otlp/src/otlp_http_exporter_factory.cc 78.95% <100.00%> (+15.32%) ⬆️
...xporters/otlp/src/otlp_http_log_record_exporter.cc 99.12% <100.00%> (+0.99%) ⬆️
.../otlp/src/otlp_http_log_record_exporter_factory.cc 78.95% <100.00%> (+15.32%) ⬆️
exporters/otlp/src/otlp_http_metric_exporter.cc 99.14% <100.00%> (+0.96%) ⬆️
...ters/otlp/src/otlp_http_metric_exporter_factory.cc 78.95% <100.00%> (+15.32%) ⬆️
... and 5 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dbarker dbarker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR. Please see the question below.

Comment thread exporters/otlp/CMakeLists.txt Outdated
@ltowarek

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I think I've covered everything in the new patch set:

  • Added opentelemetry_http_client interface library to easily inject different implementations.
  • Added tests based on http_client::nosend::HttpClient.
  • Added an example app.
  • Used WITH_HTTP_CLIENT_CURL=OFF instead of WITH_CUSTOM_HTTP_CLIENT.

@marcalff marcalff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution.

Please consider to use:

  • an abstract class HttpClientFactory, with virtual methods for Create and CreateSync
  • A child class HttpCurlClientFactory, which implement this for CURL
  • a HttpCurlClientFactory::singleton

Everywhere the static method HttpClientFactory::Create() is used today, pass a HttpClientFactory instance instead, taken as input parameter.

The user of an HTTP client can then decide which implementation to use:

  • pass HttpCurlClientFactory::singleton to use CURL
  • pass an instance of MyOwnHttpClientFactory for alternate implementations.

It should be possible to mix CURL and non CURL HTTP clients in the same binary, for example:

  • send traces using HTTP + CURL to endpoint A
  • send metrics using HTTP + alternate to endpoint B

@ltowarek
ltowarek force-pushed the fix/issue-24-custom-http-client branch from 5147826 to 516c51b Compare May 13, 2026 21:38
@ltowarek

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution.

Please consider to use:

* an abstract class `HttpClientFactory`, with virtual methods for `Create` and `CreateSync`

* A child class `HttpCurlClientFactory`, which implement this for CURL

* a HttpCurlClientFactory::singleton

Everywhere the static method HttpClientFactory::Create() is used today, pass a HttpClientFactory instance instead, taken as input parameter.

The user of an HTTP client can then decide which implementation to use:

* pass HttpCurlClientFactory::singleton to use CURL

* pass an instance of MyOwnHttpClientFactory for alternate implementations.

It should be possible to mix CURL and non CURL HTTP clients in the same binary, for example:

* send traces using HTTP + CURL to endpoint A

* send metrics using HTTP + alternate to endpoint B

@marcalff, it looks like a good idea.

My main goal with the current PR is build-time configuration - to enable opentelemetry-cpp for embedded systems as they simply can't build
libcurl. The binary for such systems will always contain only one HTTP client.

I think runtime changes should be a part of a separate PR which I'm open to working on.

Comment thread exporters/zipkin/CMakeLists.txt Outdated
@ltowarek ltowarek changed the title [EXPORTER] Add WITH_CUSTOM_HTTP_CLIENT option [EXPORTER] Add custom HTTP client May 14, 2026
@ltowarek
ltowarek requested review from marcalff and owent May 14, 2026 18:17
@ltowarek

Copy link
Copy Markdown
Contributor Author

@marcalff I've added runtime changes to this PR. Please let me know if b8eb45d fits the needs.

@lalitb

lalitb commented May 15, 2026

Copy link
Copy Markdown
Member

Thanks for adding the runtime factory path. I think this is moving in the right direction, but can we make the injection story consistent across all HTTP exporters?

Right now OtlpHttpExporterFactory has overloads that accept a custom HttpClientFactory / HttpClient, but the OTLP HTTP log and metric exporter factories still only expose the default-client path. That means a user can inject a custom client for traces, but not for logs/metrics through the same public factory API.

Also, after making HttpClientFactory::Create*() virtual instance methods, there still seem to be a few static call sites left, for example the HTTP example and test_ext_http_curl.cc. Those should probably go through GetDefaultHttpClientFactory()->Create*() or a concrete curl factory.

@ltowarek
ltowarek requested a review from lalitb May 26, 2026 20:54

@marcalff marcalff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution.

This is good work.

I suspect GetDefaultHttpClientFactory() may need minor adjustments later, but this will be for a different PR.

@marcalff marcalff self-assigned this Jun 9, 2026

@dbarker dbarker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the feature! Please see feedback below to build/test the new example in CI. Once that is in and CI is passing this should be good to merge.

Comment thread examples/custom_http_client/CMakeLists.txt
Comment thread examples/CMakeLists.txt Outdated
Comment thread examples/custom_http_client/CMakeLists.txt Outdated
@marcalff
marcalff merged commit 9e93606 into open-telemetry:main Jun 12, 2026
71 checks passed
ltowarek added a commit to ltowarek/esp-opentelemetry-cpp that referenced this pull request Jun 13, 2026
Replaces the libcurl cross-compile workaround (stub FindCURL.cmake,
~30 CURL_*/SIZEOF_*/HAVE_* cache variables, and the
opentelemetry_http_client_curl target hijack) with WITH_HTTP_CLIENT_CURL=OFF
and the new OtlpHttpExporter(options, HttpClient) constructor, passing
MakeEspHttpClient() directly.

Bumps third_party/opentelemetry-cpp to 9e93606d, which includes the merged
open-telemetry/opentelemetry-cpp#4071.
ltowarek added a commit to ltowarek/esp-opentelemetry-cpp that referenced this pull request Jun 13, 2026
* Remove CURL workarounds

* Use WITH_HTTP_CLIENT_CURL=OFF; bump opentelemetry-cpp

Replace the non-existent WITH_CUSTOM_HTTP_CLIENT flag with the
correct WITH_HTTP_CLIENT_CURL=OFF, which opentelemetry-cpp uses to
signal that the caller supplies its own HttpClientFactory transport.

Bump third_party/opentelemetry-cpp to e568cc0 (tip of the local
fix/issue-24-custom-http-client fork branch), which adds the
opentelemetry_http_client interface library and the custom_http_client
example that accompanies opentelemetry-cpp PR #4071.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Refactor HttpClientFactory to use EspHttpClientFactory for improved clarity and maintainability

* Remove stale libcurl references from comments

Update comments in the transport source, header, and README to reflect
that libcurl is now excluded entirely via WITH_HTTP_CLIENT_CURL=OFF rather
than replaced at the link level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use opentelemetry-cpp#4071 custom HTTP client instead of curl

Replaces the libcurl cross-compile workaround (stub FindCURL.cmake,
~30 CURL_*/SIZEOF_*/HAVE_* cache variables, and the
opentelemetry_http_client_curl target hijack) with WITH_HTTP_CLIENT_CURL=OFF
and the new OtlpHttpExporter(options, HttpClient) constructor, passing
MakeEspHttpClient() directly.

Bumps third_party/opentelemetry-cpp to 9e93606d, which includes the merged
open-telemetry/opentelemetry-cpp#4071.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Sep 8, 2026
…ccepts

Main grew its own FakeResponse, FakeRequest, FakeSession and FakeHttpClient
in open-telemetry#4071 and open-telemetry#4501, in an unnamed namespace, and this branch already had
four of the same names in another unnamed namespace in the same file.
Reopening an unnamed namespace names the same namespace, so the rebase
merged both without a conflict into four redefinitions that do not compile.

Merged rather than renamed. Main's set is used once and answers with one
fixed response; this branch's runs a script the case supplies, which is the
general case. So the session and the client take a script and default to
answering the way main's did, and the existing call site is untouched.

The default body had to change too, which is not cosmetic. It read
`{"errors": false, "failed" : 0}`, and this pull request stops reading
`failed` and starts requiring one `items` result per submitted record, so
that body is now rejected: with it,
ExportingARecordWithInvalidUtf8DoesNotAbort returns kFailure and fails.
The default is now a body a server could send for a one record batch, and
the comment that described the old substring search is rewritten.

Verified in both configurations, since the guards cut different code:
26 tests, 22 passed and 4 skipped without async export, 20 passed and 6
skipped with it, no failures in either.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Sep 8, 2026
Main's open-telemetry#4071 and open-telemetry#4501 put FakeResponse, FakeRequest, FakeSession and
FakeHttpClient in an unnamed namespace at the top of this file, and this
branch already had four of those names in a second unnamed namespace lower
down. Reopening an unnamed namespace names the same namespace, so the
rebase merged both with no conflict at all and left four redefinitions.

One set now. The session and the client take a script and default to
answering the way main's did, so main's own call site needs no edit, and
DeferredSession and DeferredHttpClient are untouched.

The default body stays as main wrote it. This branch does not change how
the exporter decides success, so what main's cases send still passes here.

Verified in both configurations with maintainer mode on: 19 cases, all
passing without async export, 3 passing and 16 skipping with it.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Sep 8, 2026
Main's open-telemetry#4071 and open-telemetry#4501 put FakeResponse, FakeRequest, FakeSession and
FakeHttpClient in an unnamed namespace at the top of this file, and this
branch already had four of those names in a second unnamed namespace lower
down. Reopening an unnamed namespace names the same namespace, so the
rebase merged both with no conflict at all and left four redefinitions.

One set now. The session and the client take a script and default to
answering the way main's did, so main's own call site needs no edit. The
script carries the handler as a shared_ptr rather than a reference, because
the cases here have to keep it and send a second event to it, and the
client keeps its on_create_session and on_cancel_all hooks, both empty by
default.

The default body stays as main wrote it. This branch does not change how
the exporter decides success, so what main's cases send still passes here.

Verified in both configurations with maintainer mode on: 12 cases, 12
passing with async export and 3 passing with 9 skipping without it.
Removing CompleteOnce's compare and exchange turns six of them red, so the
rewritten fixtures still discriminate.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow adding custom HTTP client

5 participants