Skip to content

Commit

Permalink
Fetch: map request/response headers to attributes (#67)
Browse files Browse the repository at this point in the history
* Fetch: map request/response headers to attributes

* reorder
  • Loading branch information
dvoytenko authored Mar 28, 2024
1 parent 037e807 commit 75d26f5
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-olives-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vercel/otel": minor
---

Fetch: map request/response headers to attributes
1 change: 1 addition & 0 deletions apps/sample/app/api/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function runService(request: Request): Promise<string> {
const response = await fetch(dataUrl, {
method: "POST",
body: JSON.stringify({ cmd: "echo", data: { foo: "bar" } }),
headers: { "X-Cmd": "echo" },
cache: "no-store",
});
if (dataUrl.includes("example")) {
Expand Down
1 change: 1 addition & 0 deletions apps/sample/app/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function Component({ searchParams }: Props): Promise<JSX.Element> {
data: { status: searchParams.status, foo: "bar" },
}),
cache: "no-store",
headers: { "X-Cmd": "echo" },
opentelemetry: {
attributes: {
custom1: "value1",
Expand Down
6 changes: 6 additions & 0 deletions apps/sample/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export function register() {
ignoreUrls: [/^https:\/\/telemetry.nextjs.org/],
propagateContextUrls: [/^http:\/\/localhost:\d+/],
dontPropagateContextUrls: [/no-propagation\=1/],
attributesFromRequestHeaders: {
"request.cmd": "X-Cmd",
},
attributesFromResponseHeaders: {
"response.server": "X-Server",
},
},
},
};
Expand Down
10 changes: 5 additions & 5 deletions packages/bridge-emulator/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ class BridgeEmulatorServer implements Bridge {
if (waiting) {
waiting.finally(() => {
this.waitingAck.delete(json.testId);
res.writeHead(200);
res.writeHead(200, "OK", { "X-Server": "bridge" });
res.write("{}");
res.end();
});
} else {
res.writeHead(404);
res.writeHead(404, "Not Found", { "X-Server": "bridge" });
res.end();
}
return;
Expand All @@ -107,19 +107,19 @@ class BridgeEmulatorServer implements Bridge {
{ headers: fetchHeaders }
);
this.fetches.push(fetchReq);
res.writeHead(200);
res.writeHead(200, "OK", { "X-Server": "bridge" });
res.write(JSON.stringify(json.data));
res.end();
return;
}
if (json.cmd === "status") {
res.writeHead(parseInt(json.data.status));
res.writeHead(parseInt(json.data.status), "", { "X-Server": "bridge" });
res.write(JSON.stringify(json.data));
res.end();
return;
}

res.writeHead(400);
res.writeHead(400, "Bad request", { "X-Server": "bridge" });
res.end();
};

Expand Down
40 changes: 40 additions & 0 deletions packages/otel/src/instrumentations/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig {
* Example: `fetch: { resourceNameTemplate: "{http.host}" }`.
*/
resourceNameTemplate?: string;

/**
* A map of attributes that should be created from the request headers. The keys of the map are
* attribute names and the values are request header names. If a resonse header doesn't exist, no
* attribute will be created for it.
*
* Example: `fetch: { attributesFromRequestHeaders: { "attr1": "X-Attr" } }`
*/
attributesFromRequestHeaders?: Record<string, string>;

/**
* A map of attributes that should be created from the response headers. The keys of the map are
* attribute names and the values are response header names. If a resonse header doesn't exist, no
* attribute will be created for it.
*
* Example: `fetch: { attributesFromResponseHeaders: { "attr1": "X-Attr" } }`
*/
attributesFromResponseHeaders?: Record<string, string>;
}

declare global {
Expand Down Expand Up @@ -163,6 +181,8 @@ export class FetchInstrumentation implements Instrumentation {
const propagateContextUrls = this.config.propagateContextUrls ?? [];
const dontPropagateContextUrls = this.config.dontPropagateContextUrls ?? [];
const resourceNameTemplate = this.config.resourceNameTemplate;
const { attributesFromRequestHeaders, attributesFromResponseHeaders } =
this.config;

const shouldPropagate = (
url: URL,
Expand Down Expand Up @@ -274,6 +294,10 @@ export class FetchInstrumentation implements Instrumentation {
propagation.inject(context.active(), req.headers, HEADERS_SETTER);
}

if (attributesFromRequestHeaders) {
headersToAttributes(span, attributesFromRequestHeaders, req.headers);
}

try {
const startTime = Date.now();
const res = await originalFetch(input, {
Expand All @@ -283,6 +307,9 @@ export class FetchInstrumentation implements Instrumentation {
const duration = Date.now() - startTime;
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, res.status);
span.setAttribute("http.response_time", duration);
if (attributesFromResponseHeaders) {
headersToAttributes(span, attributesFromResponseHeaders, res.headers);
}
if (res.status >= 500) {
onError(span, `Status: ${res.status} (${res.statusText})`);
}
Expand Down Expand Up @@ -372,3 +399,16 @@ function onError(span: Span, err: unknown): void {
});
}
}

function headersToAttributes(
span: Span,
attrsToHeadersMap: Record<string, string>,
headers: Headers
): void {
for (const [attrName, headerName] of Object.entries(attrsToHeadersMap)) {
const headerValue = headers.get(headerName);
if (headerValue !== null) {
span.setAttribute(attrName, headerValue);
}
}
}
11 changes: 11 additions & 0 deletions tests/e2e/test/vercel-deployment/render-outbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe("vercel deployment: outbound", {}, (props) => {
expect.any(Number),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
"http.response_time": expect.any(Number),
"request.cmd": "echo",
"response.server": "bridge",
"operation.name": "fetch.POST",
"resource.name": `http://localhost:${bridge.port}/`,
custom1: "value1",
Expand Down Expand Up @@ -106,6 +108,8 @@ describe("vercel deployment: outbound", {}, (props) => {
expect.any(Number),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
"http.response_time": expect.any(Number),
"request.cmd": "echo",
"response.server": "bridge",
"operation.name": "fetch.POST",
"resource.name": `http://localhost:${bridge.port}/`,
custom1: "value1",
Expand Down Expand Up @@ -170,6 +174,8 @@ describe("vercel deployment: outbound", {}, (props) => {
expect.any(Number),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
"http.response_time": expect.any(Number),
"request.cmd": "echo",
"response.server": "bridge",
},
},
{ name: "process-response" },
Expand Down Expand Up @@ -223,6 +229,7 @@ describe("vercel deployment: outbound", {}, (props) => {
"http.scheme": "http",
"net.peer.name": "localhost",
"net.peer.port": `${bridge.port + 1}`,
"request.cmd": "echo",
},
events: [
{
Expand Down Expand Up @@ -282,6 +289,8 @@ describe("vercel deployment: outbound", {}, (props) => {
expect.any(Number),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
"http.response_time": expect.any(Number),
"request.cmd": "echo",
"response.server": "bridge",
},
events: [],
},
Expand Down Expand Up @@ -343,6 +352,8 @@ describe(
expect.any(Number),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
"http.response_time": expect.any(Number),
"request.cmd": "echo",
"response.server": "bridge",
"operation.name": "fetch.POST",
"resource.name": `custom http localhost:${bridge.port}`,
},
Expand Down

0 comments on commit 75d26f5

Please sign in to comment.