Skip to content

feat(otlp-exporter-base): accept fetch parameter in createFetchTransport, and export createFetchTransport and createRetryingTransport#6377

Open
zakcutner wants to merge 1 commit intoopen-telemetry:mainfrom
zakcutner:feat/custom-fetch-option
Open

feat(otlp-exporter-base): accept fetch parameter in createFetchTransport, and export createFetchTransport and createRetryingTransport#6377
zakcutner wants to merge 1 commit intoopen-telemetry:mainfrom
zakcutner:feat/custom-fetch-option

Conversation

@zakcutner
Copy link

@zakcutner zakcutner commented Feb 5, 2026

Which problem is this PR solving?

I'm using OpenTelemetry JS in Cloudflare Workers and I need to send telemetry to a collector running in a private network via Workers VPC. Workers VPC provides a custom fetch function that can route requests through private networks, but there was no way to easily pass this to the OTLP exporters.

Before this change, the only way to achieve this was to create a custom version FetchTransport, which would require duplicating much of its implementation, or to monkey patch globalThis.fetch.

Specifying a custom fetch function could also be useful for other scenarios like adding request/response interceptors, using fetch polyfills, or testing with mocks.

Short description of the changes

Adds a fetch configuration option to createFetchTransport, allowing users to provide a custom fetch implementation instead of using globalThis.fetch.

Exports createFetchTransport and createRetryingTransport, so these can be used in place of a new OTLPTraceExporter().

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

  • Unit tests for FetchTransport using custom fetch
  • Ran npm test

Checklist:

  • Followed the style guidelines of this project
  • Unit tests have been added
  • Documentation has been updated

@zakcutner zakcutner requested a review from a team as a code owner February 5, 2026 23:37
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Feb 5, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: zakcutner / name: Zak Cutner (653f26f)

@zakcutner zakcutner force-pushed the feat/custom-fetch-option branch 4 times, most recently from 60b2f41 to db5111b Compare February 12, 2026 17:43
@zakcutner
Copy link
Author

zakcutner commented Feb 12, 2026

@blumamir @hectorhdzg @JacksonWeber @martinkuba @maryliag @raphael-theriault-swi @svetlanabrennan Would it be possible for a review of these changes, when you get a moment? I'm also happy to open an issue for discussion or change the approach if you would prefer. Sorry for the pings by the way, the contributing guide included that I should @ the approvers on my PR.

@pichlermarc
Copy link
Member

@zakcutner - my original idea was to provide an interface for people to pass their own transports to the exporters, because different transport implementations are so widely requested.

My idea was that people would be able to do something like this, but I ran out of time to work on it:

const exporter = createOtlpExporter({
  serializer: ProtobufTraceSerializer,
  transport: createMyCustomFetchTransport() // returns a custom IExporterTransport impl
});

the underlying implementation is already somewhat setup for such a thing, but requires a bit more boilerplate

import { createOtlpNetworkExportDelegate } from '@opentelemetry/otlp-exporter-base';
import { ProtobufTraceSerializer } from `@opentelemetry/otlp-transformer`;

const exporter: SpanExporter = new createOtlpExportDelegate({
  options: { /** provide all required options accoding to type**/ },
  serializer: ProtobufTraceSerializer, // or JsonTraceSerializer
  transport: createMyCustomFetchTransport() /** implement this yourself, a wrapper around your custom fetch, make sure to set the content-type header to what you're sending: `application/x-protobuf` or `application/json` **/
}); /* delegate happens to match the SpanExporter type, this won't work for metrics today though, because some methods are missing */

I suppose this is what you mean by

the only way to achieve this was to create a custom version FetchTransport, which would require duplicating much of its implementation

IMO this is a good enough workaround for now. Once we drop support for older Node.js versions (around July 2026), we can also finally use fetch in Node.js. Then, all HTTP-based exporters (json/protobuf) will use fetch as a transport regardless of runtime. Introducing that feature then will not result in it being a no-op on Node.js (as is the case in the proposal of this PR) and will therefore be a lot easier to justify adding.

@pichlermarc
Copy link
Member

So my recommendation for this feature is:

  • decline for now
  • use workaround for the time being
  • drop node:http based exporter transport in July 2026
  • add this feature as proposed in this PR to the now streamlined interface where it also works for Node.js
  • feature is released alongside SDK 3.0, everybody can use it

@zakcutner zakcutner force-pushed the feat/custom-fetch-option branch from db5111b to bd58fd1 Compare February 25, 2026 18:11
@zakcutner zakcutner requested a review from a team as a code owner February 25, 2026 18:11
@zakcutner zakcutner changed the title feat(otlp-exporter-base): add custom fetch option to OTLP HTTP exporters feat(otlp-exporter-base): accept fetch parameter in createFetchTransport, and export createFetchTransport and createRetryingTransport Feb 25, 2026
@zakcutner
Copy link
Author

@pichlermarc Thank you so much for the review, and apologies for the delay getting back to you!

Your suggestion makes sense to me, but I noticed that a few things are missing to achieve this workaround:

  1. createRetryingTransport is not exported.
  2. createFetchTransport is not exported.
  3. createFetchTransport does not support providing a custom fetch implementation.

I've reduced the scope of the change to fix these three limitations, so I can replace new OTLPTraceExporter() with something like the following, as you described.

createOtlpNetworkExportDelegate(
  getSharedConfigurationDefaults(),
  JsonTraceSerializer,
  createRetryingTransport({
    transport: createFetchTransport({
      fetch: myCustomFetch,
      headers: { "Content-Type": "application/json" },
    }),
  }),
)

When you get a moment, let me know what you think 😄

…nsport`, and export `createFetchTransport` and `createRetryingTransport`
@zakcutner zakcutner force-pushed the feat/custom-fetch-option branch from bd58fd1 to 653f26f Compare February 25, 2026 18:24

export type { IExporterTransport } from './exporter-transport';
export { createRetryingTransport } from './retrying-transport';
export { createFetchTransport } from './transport/fetch-transport';
Copy link
Contributor

Choose a reason for hiding this comment

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

please add FetchTransportParameters as well

Comment on lines 78 to 81
if (typeof fetchApi.__original === 'function') {
// @ts-expect-error -- fetch could be wrapped
fetchApi = fetchApi.__original;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

if the user-provided fetch already has __original, it gets wiped out since this runs unconditionally. it would be good to have a test for that case also

const fetchStub = sinon
.stub(globalThis, 'fetch')
.resolves(new Response('test response', { status: 200 }));
const customStub = sinon.stub().callsFake(fetchStub);
Copy link
Contributor

Choose a reason for hiding this comment

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

it looks like fetchStub will execute, increasing its call count. you might want to call it directly: sinon.stub().resolves(new Response('test response', { status: 200 }));

}

function isFetchNetworkErrorRetryable(error: unknown): boolean {
return error instanceof TypeError && !error.cause;
Copy link
Contributor

Choose a reason for hiding this comment

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

a custom fetch function might not have the same structure as the default browser fetch, potentially creating an issue with code that does very specific checks like this. we probably want to call out this one in particular in the docs since retry is an important feature

private _parameters: FetchTransportParameters;

constructor(parameters: FetchTransportParameters) {
this._parameters = parameters;
Copy link
Contributor

Choose a reason for hiding this comment

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

i recommend checking that fetch is a function so this fails early if it's not callable. if the failure occurs inside send(), it may look like the wrong kind of error

@overbalance
Copy link
Contributor

Of course, my comments only apply if js core team approves.

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.

3 participants