Skip to content

Commit

Permalink
Fetch opentelemetry API extension (#34)
Browse files Browse the repository at this point in the history
* Fetch opentelemetry API extension

* merge the global type for RequestInit.opentelemetry

* main tests pass

* debug waitUntil and other failures

* fix tests

* edge tests

* fix docs

* remove debug info

* cleanup

* docs
  • Loading branch information
dvoytenko authored Feb 21, 2024
1 parent c311f29 commit cc137f8
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 21 deletions.
5 changes: 5 additions & 0 deletions apps/sample/app/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export async function Component({ searchParams }: Props): Promise<JSX.Element> {
data: { status: searchParams.status, foo: "bar" },
}),
cache: "no-store",
opentelemetry: {
attributes: {
custom1: "value1",
},
},
});
const json = await response.json();
span.end();
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge-emulator/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ class BridgeEmulatorServer implements Bridge {
"x-otel-test-bridge-port": String(this.port),
},
});
return res;
const resClone = res.clone();
await res.arrayBuffer();
return resClone;
} finally {
waitingResolve(undefined);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/bridge-emulator/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ export class BridgeEmulatorContextReader implements TextMapPropagator {
if (!responseAck) {
responseAck = fetch(`http://127.0.0.1:${bridgePort}`, {
method: "POST",
body: JSON.stringify({ cmd: "ack", testId }),
body: JSON.stringify({
cmd: "ack",
testId,
runtime: process.env.NEXT_RUNTIME,
}),
headers: { "content-type": "application/json" },
// @ts-expect-error - internal Next request.
next: { internal: true },
Expand Down
5 changes: 5 additions & 0 deletions packages/otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Registers the OpenTelemetry SDK with the specified configuration. Configuration
- `propagateContextUrls`: A set of URL matchers (string prefix or regex) for which the tracing context should be propagated (see `propagators`). By default the context is propagated _only_ for the [deployment URLs](https://vercel.com/docs/deployments/generated-urls), all other URLs should be enabled explicitly. Example: `fetch: { propagateContextUrls: [ /my.api/ ] }`.
- `dontPropagateContextUrls`: A set of URL matchers (string prefix or regex) for which the tracing context should not be propagated (see `propagators`). This allows you to exclude a subset of URLs allowed by the `propagateContextUrls`.
- `resourceNameTemplate`: A string for the "resource.name" attribute that can include attribute expressions in `{}`. Example: `fetch: { resourceNameTemplate: "{http.host}" }`.
- The `fetch` instrumentation also allows the caller to pass relevant telemetry parameters via `fetch(..., { opentelemetry: {} })` argument (requires Next 14.1.1 or above), which may include the following fields:
- `ignore: boolean`: overrides `ignoreUrls` for this call.
- `propagateContext: boolean`: overrides `propagateContextUrls` for this call.
- `spanName: string`: overrides the computed span name for this call.
- `attributes: Attributes`: overrides the computed attributes for this call.
- `propagators`: A set of propagators that may extend inbound and outbound contexts. By default, `@vercel/otel` configures [W3C Trace Context](https://www.w3.org/TR/trace-context/) propagator.
- `traceSampler`: The sampler to be used to decide which requests should be traced. By default, all requests are traced. This option can be changed to, for instance, only trace 1% of all requests.
- `spanProcessors` and `traceExporter`: The export mechanism for traces. By default, `@vercel/otel` configures the best export mechanism for the environment. For instance, if a [tracing integrations](https://vercel.com/docs/observability/otel-overview/quickstart) is configured on Vercel, this integration will be automatically used for export; otherwise an [OTLP exporter](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#otlp-exporter) can be used if configured in environment variables.
Expand Down
67 changes: 57 additions & 10 deletions packages/otel/src/instrumentations/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
propagation,
context,
} from "@opentelemetry/api";
import type { TextMapSetter, TracerProvider } from "@opentelemetry/api";
import type {
Attributes,
TextMapSetter,
TracerProvider,
} from "@opentelemetry/api";
import type {
Instrumentation,
InstrumentationConfig,
Expand All @@ -16,11 +20,23 @@ import { isSampled } from "../util/sampled";

/**
* Configuration for the "fetch" instrumentation.
*
* Some of this configuration can be overriden on a per-fetch call basis by
* using the `opentelemetry` property in the `RequestInit` object (requires Next 14.1.1 or above).
* This property can include:
* - `ignore`: boolean - whether to ignore the fetch call from tracing. Overrides
* `ignoreUrls`.
* - `propagateContext: boolean`: overrides `propagateContextUrls` for this call.
* - `spanName: string`: overrides the computed span name for this call.
* - `attributes: Attributes`: overrides the computed attributes for this call.
*/
export interface FetchInstrumentationConfig extends InstrumentationConfig {
/**
* A set of URL matchers (string prefix or regex) that should be ignored from tracing.
* By default all URLs are traced. Example: `fetch: { ignoreUrls: [/example.com/] }`.
* By default all URLs are traced.
* Can be overriden by the `opentelemetry.ignore` property in the `RequestInit` object.
*
* Example: `fetch: { ignoreUrls: [/example.com/] }`.
*/
ignoreUrls?: (string | RegExp)[];

Expand All @@ -30,6 +46,8 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig {
* By default the context is propagated _only_ for the
* [deployment URLs](https://vercel.com/docs/deployments/generated-urls), all
* other URLs should be enabled explicitly.
* Can be overriden by the `opentelemetry.propagateContext` property in the `RequestInit` object.
*
* Example: `fetch: { propagateContextUrls: [ /my.api/ ] }`.
*/
propagateContextUrls?: (string | RegExp)[];
Expand All @@ -38,17 +56,31 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig {
* A set of URL matchers (string prefix or regex) for which the tracing context
* should not be propagated (see [`propagators`](Configuration#propagators)). This allows you to exclude a
* subset of URLs allowed by the [`propagateContextUrls`](FetchInstrumentationConfig#propagateContextUrls).
* Can be overriden by the `opentelemetry.propagateContext` property in the `RequestInit` object.
*/
dontPropagateContextUrls?: (string | RegExp)[];

/**
* A string for the "resource.name" attribute that can include attribute expressions in `{}`.
* Can be overriden by the `opentelemetry.attributes` property in the `RequestInit` object.
*
* Example: `fetch: { resourceNameTemplate: "{http.host}" }`.
*/
resourceNameTemplate?: string;
}

type NextRequestInit = RequestInit & {
declare global {
interface RequestInit {
opentelemetry?: {
ignore?: boolean;
propagateContext?: boolean;
spanName?: string;
attributes?: Attributes;
};
}
}

type InternalRequestInit = RequestInit & {
next?: {
internal: boolean;
};
Expand Down Expand Up @@ -99,7 +131,13 @@ export class FetchInstrumentation implements Instrumentation {

const ignoreUrls = this.config.ignoreUrls ?? [];

const shouldIgnore = (url: URL): boolean => {
const shouldIgnore = (
url: URL,
init: InternalRequestInit | undefined
): boolean => {
if (init?.opentelemetry?.ignore !== undefined) {
return init.opentelemetry.ignore;
}
if (ignoreUrls.length === 0) {
return false;
}
Expand All @@ -125,7 +163,13 @@ export class FetchInstrumentation implements Instrumentation {
const dontPropagateContextUrls = this.config.dontPropagateContextUrls ?? [];
const resourceNameTemplate = this.config.resourceNameTemplate;

const shouldPropagate = (url: URL): boolean => {
const shouldPropagate = (
url: URL,
init: InternalRequestInit | undefined
): boolean => {
if (init?.opentelemetry?.propagateContext) {
return init.opentelemetry.propagateContext;
}
const urlString = url.toString();
if (
dontPropagateContextUrls.length > 0 &&
Expand Down Expand Up @@ -172,15 +216,17 @@ export class FetchInstrumentation implements Instrumentation {
const originalFetch = globalThis.fetch;
this.originalFetch = originalFetch;

const doFetch: typeof fetch = (input, init) => {
const doFetch: typeof fetch = (input, initArg) => {
const init = initArg as InternalRequestInit | undefined;

// Passthrough internal requests.
if ((init as NextRequestInit | undefined)?.next?.internal) {
if (init?.next?.internal) {
return originalFetch(input, init);
}

const req = new Request(input, init);
const url = new URL(req.url);
if (shouldIgnore(url)) {
if (shouldIgnore(url, init)) {
return originalFetch(input, init);
}

Expand All @@ -197,20 +243,21 @@ export class FetchInstrumentation implements Instrumentation {
: removeSearch(req.url);

return tracer.startActiveSpan(
`fetch ${req.method} ${req.url}`,
init?.opentelemetry?.spanName ?? `fetch ${req.method} ${req.url}`,
{
kind: SpanKind.CLIENT,
attributes: {
...attrs,
"operation.name": `fetch.${req.method}`,
"resource.name": resourceName,
...init?.opentelemetry?.attributes,
},
},
async (span) => {
if (
span.isRecording() &&
isSampled(span.spanContext().traceFlags) &&
shouldPropagate(url)
shouldPropagate(url, init)
) {
propagation.inject(context.active(), req.headers, HEADERS_SETTER);
}
Expand Down
1 change: 1 addition & 0 deletions packages/otel/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface Configuration {
* Use the "auto" value to include all default propagators.
* By default, `@vercel/otel` configures [W3C Trace Context](https://www.w3.org/TR/trace-context/) propagator.
* This option can be also configured via the [`OTEL_PROPAGATORS`](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) environment variable.
*
* Example: `{ propagators: ["auto", new MyPropagator()] }`
*/
propagators?: PropagatorOrName[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ describe(

const execResp = await bridge.fetch("/slugs/baz");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectStdio(stdio.out, "name: 'GET /slugs/[slug]'");
expect(collector.getAllTraces().length).toBe(0);
Expand All @@ -26,7 +25,6 @@ describe(

const execResp = await bridge.fetch("/slugs/baz/edge");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectStdio(stdio.out, "name: 'GET /slugs/[slug]/edge'");
expect(collector.getAllTraces().length).toBe(0);
Expand Down
2 changes: 0 additions & 2 deletions tests/e2e/test/vercel-deployment/render-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ describe("vercel deployment: middleware", {}, (props) => {

const execResp = await bridge.fetch("/behind-middleware/baz");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "middleware GET /behind-middleware/baz",
Expand Down Expand Up @@ -69,7 +68,6 @@ describe("vercel deployment: middleware", {}, (props) => {

const execResp = await bridge.fetch("/behind-middleware/baz/edge");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "middleware GET /behind-middleware/baz/edge",
Expand Down
58 changes: 57 additions & 1 deletion tests/e2e/test/vercel-deployment/render-outbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe("vercel deployment: outbound", {}, (props) => {
spans: [
{
name: "render route (app) /slugs/[slug]",
attributes: {},
spans: [
{
name: "sample-span",
Expand All @@ -41,6 +40,63 @@ describe("vercel deployment: outbound", {}, (props) => {
"http.status_code": 200,
"operation.name": "fetch.POST",
"resource.name": `http://localhost:${bridge.port}/`,
custom1: "value1",
},
},
],
},
],
},
],
});

const fetches = bridge.fetches;
expect(fetches).toHaveLength(1);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fetch = fetches[0]!;
expect(fetch.headers.get("traceparent")).toMatch(
/00-[0-9a-fA-F]{32}-[0-9a-fA-F]{16}-01/
);
});

it("should create a span for fetch in edge", async () => {
const { collector, bridge } = props();

await bridge.fetch(
`/slugs/baz/edge?dataUrl=${encodeURIComponent(
`http://localhost:${bridge.port}`
)}`
);

await expectTrace(collector, {
name: "GET /slugs/[slug]/edge",
status: { code: SpanStatusCode.UNSET },
kind: SpanKind.SERVER,
resource: { "vercel.runtime": "edge" },
spans: [
{
name: "render route (app) /slugs/[slug]/edge",
spans: [
{
name: "sample-span",
attributes: { scope: "sample", foo: "bar" },
spans: [
{
name: `fetch POST http://localhost:${bridge.port}/`,
kind: SpanKind.CLIENT,
attributes: {
scope: "@vercel/otel/fetch",
"http.method": "POST",
"http.url": `http://localhost:${bridge.port}/`,
"http.host": `localhost:${bridge.port}`,
"http.scheme": "http",
"net.peer.name": "localhost",
"net.peer.port": `${bridge.port}`,
"http.status_code": 200,
"operation.name": "fetch.POST",
"resource.name": `http://localhost:${bridge.port}/`,
custom1: "value1",
},
},
],
Expand Down
2 changes: 0 additions & 2 deletions tests/e2e/test/vercel-deployment/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ describe("vercel deployment: render", {}, (props) => {

const execResp = await bridge.fetch("/slugs/baz");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "GET /slugs/[slug]",
Expand Down Expand Up @@ -66,7 +65,6 @@ describe("vercel deployment: render", {}, (props) => {

const execResp = await bridge.fetch("/slugs/baz/edge");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "GET /slugs/[slug]/edge",
Expand Down
2 changes: 0 additions & 2 deletions tests/e2e/test/vercel-deployment/vercel-collector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ describe(

const execResp = await bridge.fetch("/slugs/baz");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "GET /slugs/[slug]",
Expand Down Expand Up @@ -72,7 +71,6 @@ describe(

const execResp = await bridge.fetch("/slugs/baz/edge");
expect(execResp.status).toBe(200);
void execResp.arrayBuffer();

await expectTrace(collector, {
name: "GET /slugs/[slug]/edge",
Expand Down

0 comments on commit cc137f8

Please sign in to comment.