opentelemetry-js icon indicating copy to clipboard operation
opentelemetry-js copied to clipboard

nodejs 18: Support native fetch

Open ehtb opened this issue 3 years ago • 24 comments
trafficstars

Node.js 18+ support the native fetch API. The current fetch instrumentation (instrumentation-fetch) only works in a web / browser environment. This is because it is importing sdk-trace-web and depending on location being globally defined.

Before native support this was not a problem, because solutions like node-fetch were using http under the hood, and were therefore wrapped correctly. Now with native fetch support this is not happening anymore.

I can think of these two solutions:

  1. Change instrumentation-fetch to be platform agnostic. This has the benefit of using one module consistently in multiple environments
  2. Create a new instrumentation, for instance instrumentation-fetch-node which is tailored to the node.js / server environment. This would allow for easier support of some node-only utilities. Support would be for node >= 18.

Could you please let me know whether support for this is in the pipeline, and perhaps from which nodejs version onward?

ehtb avatar Nov 15 '22 07:11 ehtb

Hmm the instrumentation shouldn't depend on the SDK package anyway so that needs to be updated. I'm not sure how easy it would be to make the browser fetch instrumentation work in both places, but that would be my preferred solution.

dyladan avatar Nov 16 '22 16:11 dyladan

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Jan 16 '23 06:01 github-actions[bot]

This is still relevant. I'll try to allocate some time to contribute to this if no one else steps in first.

Fryuni avatar Jan 17 '23 18:01 Fryuni

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Mar 20 '23 06:03 github-actions[bot]

Hi @Fryuni Did you have any time to work on this issue? Maybe I could help?

ktrz avatar Mar 24 '23 16:03 ktrz

I started working on a patch that would work for us, but it breaks other scenarios. I still need to work on a proper solution, but time has been eluding me.

If you want to tackle this, here is what I got:

The instrumentation-fetch package uses sdk-trace-web, which relies on browser-only APIs, like the global scope being window and the presence of the global objects location, navigation, and navigator.

It uses only 5 functions from sdk-trace-web. As far as I can tell, the semantics of this function for the caller is entirely agnostic of the browser-only elements, so providing the same functions for Node should suffice.

I used patch-package to comment out everything that used the browser APIs from sdk-trace-web or to replace with a return null or return ''. Just to get it working.

Those 5 functions do all the heavy work for the browser, like creating an empty anchor and moving a value through it to normalize things, but it doesn't seem to me like there would be a lot of work to do when in Node, so it should be quite straightforward to implement those. It could be provided as a NodeFetchInstrumentation or it could even be the same FetchInstrumentation that validates upon load which environment it is in.

I've been analyzing this slowly since my previous comment when I have some spare time, mostly while commuting, so I don't even have a branch for you to work from, sorry about that. 😄

Fryuni avatar Mar 24 '23 18:03 Fryuni

Thanks for the update! I'll see if I can follow up on your research

ktrz avatar Mar 24 '23 18:03 ktrz

Following this too! Would be great for this instrumentation to also work for other serverside WinterCG environments (Vercel, CloudFlare Workers)

RichiCoder1 avatar Mar 27 '23 16:03 RichiCoder1

For reference for Deno I just had to make location.href not crash and the fetch autoinstrumentation seemed to work for a modified version of the basic-tracer-node example

// Monkeypatching to get past FetchInstrumentation's dependence on sdk-trace-web, which depends on some browser-only constructs
globalThis.location = {}; //For this line in parseUrl - https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-web/src/utils.ts#L310

And https://github.com/Grunet/otel-js-deno-experimentation/commit/8cbcc12478b5f77bea81b0a1efc9f95695997f1c for the change to the modified example I've been working with

Grunet avatar Apr 05 '23 02:04 Grunet

Also wanted to cross-document here that the fetch autoinstrumentation package is currently using Date.now() instead of performance.now(), which may explain small offsets in traces (I was seeing them for Deno)

More references for this at https://github.com/open-telemetry/opentelemetry-js/issues/3719#issuecomment-1503114856

Grunet avatar Apr 13 '23 01:04 Grunet

Also wanted to cross-document here that the fetch autoinstrumentation package is currently using Date.now() instead of performance.now(), which may explain small offsets in traces

Date.now() returns milliseconds since unix epoch, performance.now() returns milliseconds since performance.timeOrigin with microseconds precision. They have very different meanings.

If you see offsets between distributed traces, it is probably due to clock skew between the services. performance.now() would provide better precision for the duration of each trace but not for the relation between them.

And if Date.now() gets replaced, I'd suggest using performance.now() only for the web. Node has process.hrtime.bigint() for nanoseconds precision, which would be even better.

Fryuni avatar Apr 17 '23 18:04 Fryuni

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Jun 19 '23 06:06 github-actions[bot]

Is anyone currently working on this? We switched back to using node-fetch package to force it back to using http/https packages for tracing.

I might have a few days available to work on this during the summer, if noone else is actively on it.

Multiply avatar Jun 25 '23 06:06 Multiply

@Fryuni @ktrz is there any progress made on this? Could I help out to push this along?

drewcorlin1 avatar Aug 02 '23 03:08 drewcorlin1

I haven't progressed on this since my comment on March, kinda left my hacky workaround there since It Just Works™️.

If you wanna replicate it, replace all the functions on sdk-trace-web that touch the navigator, the DOM or the window with either return null or return '' as appropriate for the type. Certainly those functions serve a purpose, but I haven't missed any functionality without them, so I think they are not even necessary for a Node's fetch.

Fryuni avatar Aug 14 '23 01:08 Fryuni

Thanks for the context! I'll try to find a bit of time to work on this 😄

drewcorlin1 avatar Aug 14 '23 10:08 drewcorlin1

While I work on fixing this, I want to share what I ran into: In order to make sure that trace context (via the propogation headers) was shared across services, so that an HTTP request from client -> service A -> service B showed up as 1 trace, I had to do the following with the fetch instrumentation

// @ts-ignore
globalThis.location = {};
// @ts-ignore
globalThis.navigator = {};

registerInstrumentations({
  tracerProvider: provider,
  instrumentations: [
    // other instrumentations...,
    new FetchInstrumentation({
      propagateTraceHeaderCorsUrls: /.*/,
    }),
  ],
});

full snippet of my setup

import { hostname } from 'os';
import { context } from '@opentelemetry/api';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { Resource } from '@opentelemetry/resources';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

context.setGlobalContextManager(new AsyncHooksContextManager().enable());

const provider = new NodeTracerProvider({
  resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: hostname() }),
});

const otlpTraceExporter = new OTLPTraceExporter({ url: process.env.OPEN_TELEMETRY_DESTINATION_URL });
provider.addSpanProcessor(new BatchSpanProcessor(otlpTraceExporter));

// @ts-ignore
globalThis.location = {};
// @ts-ignore
globalThis.navigator = {};

registerInstrumentations({
  tracerProvider: provider,
  instrumentations: [
    // other instrumentations...,
    new FetchInstrumentation({
      propagateTraceHeaderCorsUrls: /.*/,
    }),
  ],
});

provider.register();

drewcorlin1 avatar Aug 18 '23 12:08 drewcorlin1

another issue i want to add to this thread an issue i've encountered in the code, the _patchConstructor https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L295-L405

it seems a conscious choice was made to explicitly ignore the second argument passed to the function if the first is an instanceof Request but that actually breaks functionality should you choose to supply RequestInit options in addition to a Request which a perfectly valid usecase and seems explicit in specification.

You can see this handled here in the undici (node native impl) https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L125-L137 the resulting request is the result of instantiating a new request with the options, which extends the request with the additional options.

it might be necessary for this to be pulled into a bug of it's own since this would also affect the web/browser version i would imagine.

sonewman avatar Oct 28 '23 11:10 sonewman

https://github.com/open-telemetry/opentelemetry-js/pull/4063 Has now been merged, should this issue be closed? @sonewman may be best to open a new issue for what you're seeing?

drewcorlin1 avatar Nov 15 '23 16:11 drewcorlin1

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Feb 19 '24 06:02 github-actions[bot]

seems #4063 is only for the web (or am I misunderstanding)?

mbrevda avatar Feb 19 '24 07:02 mbrevda

It makes the web SDK work when missing the web-only globals. So although it changes only the web SDK it should now work with Node (and others non-browser runtimes)

Fryuni avatar Feb 19 '24 12:02 Fryuni

it should now work

Just tried it, it doesn't seem to be instrumenting thing I'd expect to see the way opentelemetry-instrumentation-fetch-node does

mbrevda avatar Feb 28 '24 10:02 mbrevda

There is a new instrumentation released a couple of weeks ago. This instrumentation works for undici but also for the fetch global Node.js API (since the API uses undici under the hood). https://www.npmjs.com/package/@opentelemetry/instrumentation-undici

@ehtb I think this covers point 2 right? Could you give it a try?

david-luna avatar Apr 16 '24 09:04 david-luna

@david-luna @ehtb Given the existence of the undici/native-fetch instrumentation, this issue can be closed.

JacksonWeber avatar May 17 '24 22:05 JacksonWeber

Closing the issue. @ehtb ping me or open a new issue if needed :)

david-luna avatar Jun 03 '24 13:06 david-luna