Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(node): Use @opentelemetry/instrumentation-undici for fetch tra…
…cing
  • Loading branch information
timfish committed Aug 27, 2024
commit dbd4ed83195ccb33c1ce6fe84210c2469cb2a87d
1 change: 0 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ updates:
- dependency-name: "@sentry/vite-plugin"
- dependency-name: "@opentelemetry/*"
- dependency-name: "@prisma/instrumentation"
- dependency-name: "opentelemetry-instrumentation-fetch-node"
versioning-strategy: increase
commit-message:
prefix: feat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { conditionalTest } from '../../../../utils';
import { createRunner } from '../../../../utils/runner';
import { createTestServer } from '../../../../utils/server';

conditionalTest({ min: 18 })('outgoing fetch', () => {
conditionalTest({ min: 16 })('outgoing fetch', () => {
test('outgoing fetch requests are correctly instrumented with tracing disabled', done => {
expect.assertions(11);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ Sentry.init({
});

async function run(): Promise<void> {
// Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented
await new Promise(resolve => setTimeout(resolve, 100));
await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { conditionalTest } from '../../../../utils';
import { createRunner } from '../../../../utils/runner';
import { createTestServer } from '../../../../utils/server';

conditionalTest({ min: 18 })('outgoing fetch', () => {
conditionalTest({ min: 16 })('outgoing fetch', () => {
test('outgoing sampled fetch requests without active span are correctly instrumented', done => {
expect.assertions(11);

createTestServer(done)
.get('/api/v0', headers => {
expect(headers['baggage']).toEqual(expect.any(String));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
})
.get('/api/v1', headers => {
expect(headers['baggage']).toEqual(expect.any(String));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
})
.get('/api/v2', headers => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ Sentry.init({
async function run(): Promise<void> {
// Wrap in span that is not sampled
await Sentry.startSpan({ name: 'outer' }, async () => {
// Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented
await new Promise(resolve => setTimeout(resolve, 100));
await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { conditionalTest } from '../../../../utils';
import { createRunner } from '../../../../utils/runner';
import { createTestServer } from '../../../../utils/server';

conditionalTest({ min: 18 })('outgoing fetch', () => {
conditionalTest({ min: 16 })('outgoing fetch', () => {
test('outgoing fetch requests are correctly instrumented when not sampled', done => {
expect.assertions(11);

Expand Down
6 changes: 2 additions & 4 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@
"@opentelemetry/instrumentation-ioredis": "0.42.0",
"@opentelemetry/instrumentation-koa": "0.42.0",
"@opentelemetry/instrumentation-mongodb": "0.46.0",
"@opentelemetry/instrumentation-mongoose": "0.40.0",
"@opentelemetry/instrumentation-mongoose": "0.41.0",
"@opentelemetry/instrumentation-mysql": "0.40.0",
"@opentelemetry/instrumentation-mysql2": "0.40.0",
"@opentelemetry/instrumentation-nestjs-core": "0.39.0",
"@opentelemetry/instrumentation-pg": "0.43.0",
"@opentelemetry/instrumentation-redis-4": "0.41.0",
"@opentelemetry/instrumentation-undici": "0.5.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-trace-base": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.25.1",
Expand All @@ -98,9 +99,6 @@
"devDependencies": {
"@types/node": "^14.18.0"
},
"optionalDependencies": {
"opentelemetry-instrumentation-fetch-node": "1.2.3"
},
"scripts": {
"build": "run-p build:transpile build:types",
"build:dev": "yarn build",
Expand Down
90 changes: 21 additions & 69 deletions packages/node/src/integrations/node-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import type { Span } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import { context, propagation } from '@opentelemetry/api';
import type { UndiciRequest, UndiciResponse } from '@opentelemetry/instrumentation-undici';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
import { addBreadcrumb, defineIntegration, getCurrentScope, hasTracingEnabled } from '@sentry/core';
import {
addOpenTelemetryInstrumentation,
generateSpanContextForPropagationContext,
getPropagationContextFromSpan,
} from '@sentry/opentelemetry';
import type { IntegrationFn, SanitizedRequestData } from '@sentry/types';
import { getSanitizedUrlString, logger, parseUrl } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
import { NODE_MAJOR } from '../nodeVersion';

import type { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node';

import { addOriginToSpan } from '../utils/addOriginToSpan';

interface FetchRequest {
method: string;
origin: string;
path: string;
headers: string | string[];
}

interface FetchResponse {
headers: Buffer[];
statusCode: number;
}
import { getSanitizedUrlString, parseUrl } from '@sentry/utils';

interface NodeFetchOptions {
/**
Expand All @@ -46,30 +29,11 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {
const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs;
const _ignoreOutgoingRequests = options.ignoreOutgoingRequests;

async function getInstrumentation(): Promise<FetchInstrumentation | void> {
// Only add NodeFetch if Node >= 18, as previous versions do not support it
if (NODE_MAJOR < 18) {
DEBUG_BUILD && logger.log('NodeFetch is not supported on Node < 18, skipping instrumentation...');
return;
}

try {
const pkg = await import('opentelemetry-instrumentation-fetch-node');
const { FetchInstrumentation } = pkg;

class SentryNodeFetchInstrumentation extends FetchInstrumentation {
// We extend this method so we have access to request _and_ response for the breadcrumb
public onHeaders({ request, response }: { request: FetchRequest; response: FetchResponse }): void {
if (_breadcrumbs) {
_addRequestBreadcrumb(request, response);
}

return super.onHeaders({ request, response });
}
}

return new SentryNodeFetchInstrumentation({
ignoreRequestHook: (request: FetchRequest) => {
return {
name: 'NodeFetch',
setupOnce() {
const instrumentation = new UndiciInstrumentation({
ignoreRequestHook: request => {
const url = getAbsoluteUrl(request.origin, request.path);
const tracingDisabled = !hasTracingEnabled();
const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url);
Expand Down Expand Up @@ -113,39 +77,27 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

return false;
},
onRequest: ({ span }: { span: Span }) => {
_updateSpan(span);
startSpanHook: () => {
return {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'auto.http.otel.node_fetch',
};
},
responseHook: (_, { request, response }) => {
if (_breadcrumbs) {
addRequestBreadcrumb(request, response);
}
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
} catch (error) {
// Could not load instrumentation
DEBUG_BUILD && logger.log('Error while loading NodeFetch instrumentation: \n', error);
}
}

return {
name: 'NodeFetch',
setupOnce() {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
getInstrumentation().then(instrumentation => {
if (instrumentation) {
addOpenTelemetryInstrumentation(instrumentation);
}
});

addOpenTelemetryInstrumentation(instrumentation);
},
};
}) satisfies IntegrationFn;

export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration);

/** Update the span with data we need. */
function _updateSpan(span: Span): void {
addOriginToSpan(span, 'auto.http.otel.node_fetch');
}

/** Add a breadcrumb for outgoing requests. */
function _addRequestBreadcrumb(request: FetchRequest, response: FetchResponse): void {
function addRequestBreadcrumb(request: UndiciRequest, response: UndiciResponse): void {
const data = getBreadcrumbData(request);

addBreadcrumb(
Expand All @@ -165,7 +117,7 @@ function _addRequestBreadcrumb(request: FetchRequest, response: FetchResponse):
);
}

function getBreadcrumbData(request: FetchRequest): Partial<SanitizedRequestData> {
function getBreadcrumbData(request: UndiciRequest): Partial<SanitizedRequestData> {
try {
const url = new URL(request.path, request.origin);
const parsedUrl = parseUrl(url.toString());
Expand Down
45 changes: 12 additions & 33 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7166,10 +7166,10 @@
"@opentelemetry/sdk-metrics" "^1.9.1"
"@opentelemetry/semantic-conventions" "^1.22.0"

"@opentelemetry/instrumentation-mongoose@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.40.0.tgz#9c888312e524c381bfdf56a094c799150332dd51"
integrity sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==
"@opentelemetry/instrumentation-mongoose@0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz#9557245f7fb2a7f4673722a9c597bddb3e3be652"
integrity sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
Expand Down Expand Up @@ -7221,6 +7221,14 @@
"@opentelemetry/redis-common" "^0.36.2"
"@opentelemetry/semantic-conventions" "^1.22.0"

"@opentelemetry/[email protected]":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz#50782ff300027d0d0664fb60a3c12227586d5ebd"
integrity sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"

"@opentelemetry/[email protected]", "@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.0", "@opentelemetry/instrumentation@^0.52.1":
version "0.52.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48"
Expand All @@ -7244,17 +7252,6 @@
semver "^7.5.2"
shimmer "^1.2.1"

"@opentelemetry/instrumentation@^0.46.0":
version "0.46.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e"
integrity sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw==
dependencies:
"@types/shimmer" "^1.0.2"
import-in-the-middle "1.7.1"
require-in-the-middle "^7.1.1"
semver "^7.5.2"
shimmer "^1.2.1"

"@opentelemetry/otlp-transformer@^0.50.0":
version "0.50.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.50.0.tgz#211fe512fcce9d76042680f955336dbde3be03ef"
Expand Down Expand Up @@ -20766,16 +20763,6 @@ [email protected]:
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"

[email protected]:
version "1.7.1"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz#3e111ff79c639d0bde459bd7ba29dd9fdf357364"
integrity sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==
dependencies:
acorn "^8.8.2"
acorn-import-assertions "^1.9.0"
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"

import-in-the-middle@^1.11.0, import-in-the-middle@^1.8.1:
version "1.11.0"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708"
Expand Down Expand Up @@ -26304,14 +26291,6 @@ opener@^1.5.2:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==

[email protected]:
version "1.2.3"
resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.3.tgz#beb24048bdccb1943ba2a5bbadca68020e448ea7"
integrity sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A==
dependencies:
"@opentelemetry/instrumentation" "^0.46.0"
"@opentelemetry/semantic-conventions" "^1.17.0"

[email protected]:
version "0.7.1"
resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.7.1.tgz#ef90ede718612786f7015e5496bd25cac8c49ce3"
Expand Down