Skip to content

Commit d77a8b9

Browse files
vmarchaudFlarnadyladan
authored
feat(http-instrumentation): add content size attributes to spans (#1771)
Co-authored-by: Gerhard Stöbich <[email protected]> Co-authored-by: Daniel Dyla <[email protected]>
1 parent 80ea2e0 commit d77a8b9

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

packages/opentelemetry-instrumentation-http/src/utils.ts

+62
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,66 @@ export const setSpanWithError = (
177177
span.setStatus(status);
178178
};
179179

180+
/**
181+
* Adds attributes for request content-length and content-encoding HTTP headers
182+
* @param { IncomingMessage } Request object whose headers will be analyzed
183+
* @param { Attributes } Attributes object to be modified
184+
*/
185+
export const setRequestContentLengthAttribute = (
186+
request: IncomingMessage,
187+
attributes: Attributes
188+
) => {
189+
const length = getContentLength(request.headers);
190+
if (length === null) return;
191+
192+
if (isCompressed(request.headers)) {
193+
attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH] = length;
194+
} else {
195+
attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED] = length;
196+
}
197+
};
198+
199+
/**
200+
* Adds attributes for response content-length and content-encoding HTTP headers
201+
* @param { IncomingMessage } Response object whose headers will be analyzed
202+
* @param { Attributes } Attributes object to be modified
203+
*/
204+
export const setResponseContentLengthAttribute = (
205+
response: IncomingMessage,
206+
attributes: Attributes
207+
) => {
208+
const length = getContentLength(response.headers);
209+
if (length === null) return;
210+
211+
if (isCompressed(response.headers)) {
212+
attributes[HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH] = length;
213+
} else {
214+
attributes[
215+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED
216+
] = length;
217+
}
218+
};
219+
220+
function getContentLength(
221+
headers: OutgoingHttpHeaders | IncomingHttpHeaders
222+
): number | null {
223+
const contentLengthHeader = headers['content-length'];
224+
if (contentLengthHeader === undefined) return null;
225+
226+
const contentLength = parseInt(contentLengthHeader as string, 10);
227+
if (isNaN(contentLength)) return null;
228+
229+
return contentLength;
230+
}
231+
232+
export const isCompressed = (
233+
headers: OutgoingHttpHeaders | IncomingHttpHeaders
234+
): boolean => {
235+
const encoding = headers['content-encoding'];
236+
237+
return !!encoding && encoding !== 'identity';
238+
};
239+
180240
/**
181241
* Makes sure options is an url object
182242
* return an object with default value and parsed options
@@ -326,6 +386,7 @@ export const getOutgoingRequestAttributesOnResponse = (
326386
[GeneralAttribute.NET_PEER_PORT]: remotePort,
327387
[HttpAttribute.HTTP_HOST]: `${options.hostname}:${remotePort}`,
328388
};
389+
setResponseContentLengthAttribute(response, attributes);
329390

330391
if (statusCode) {
331392
attributes[HttpAttribute.HTTP_STATUS_CODE] = statusCode;
@@ -386,6 +447,7 @@ export const getIncomingRequestAttributes = (
386447
if (userAgent !== undefined) {
387448
attributes[HttpAttribute.HTTP_USER_AGENT] = userAgent;
388449
}
450+
setRequestContentLengthAttribute(request, attributes);
389451

390452
const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
391453
return Object.assign(attributes, httpKindAttributes);

packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts

+142
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import {
17+
Attributes,
1718
StatusCode,
1819
ROOT_CONTEXT,
1920
SpanKind,
@@ -308,4 +309,145 @@ describe('Utility', () => {
308309
assert.deepEqual(attributes[HttpAttribute.HTTP_ROUTE], undefined);
309310
});
310311
});
312+
// Verify the key in the given attributes is set to the given value,
313+
// and that no other HTTP Content Length attributes are set.
314+
function verifyValueInAttributes(
315+
attributes: Attributes,
316+
key: string | undefined,
317+
value: number
318+
) {
319+
const httpAttributes = [
320+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
321+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH,
322+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
323+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH,
324+
];
325+
326+
for (const attr of httpAttributes) {
327+
if (attr === key) {
328+
assert.strictEqual(attributes[attr], value);
329+
} else {
330+
assert.strictEqual(attributes[attr], undefined);
331+
}
332+
}
333+
}
334+
335+
describe('setRequestContentLengthAttributes()', () => {
336+
it('should set request content-length uncompressed attribute with no content-encoding header', () => {
337+
const attributes: Attributes = {};
338+
const request = {} as IncomingMessage;
339+
340+
request.headers = {
341+
'content-length': '1200',
342+
};
343+
utils.setRequestContentLengthAttribute(request, attributes);
344+
345+
verifyValueInAttributes(
346+
attributes,
347+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
348+
1200
349+
);
350+
});
351+
352+
it('should set request content-length uncompressed attribute with "identity" content-encoding header', () => {
353+
const attributes: Attributes = {};
354+
const request = {} as IncomingMessage;
355+
request.headers = {
356+
'content-length': '1200',
357+
'content-encoding': 'identity',
358+
};
359+
utils.setRequestContentLengthAttribute(request, attributes);
360+
361+
verifyValueInAttributes(
362+
attributes,
363+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
364+
1200
365+
);
366+
});
367+
368+
it('should set request content-length compressed attribute with "gzip" content-encoding header', () => {
369+
const attributes: Attributes = {};
370+
const request = {} as IncomingMessage;
371+
request.headers = {
372+
'content-length': '1200',
373+
'content-encoding': 'gzip',
374+
};
375+
utils.setRequestContentLengthAttribute(request, attributes);
376+
377+
verifyValueInAttributes(
378+
attributes,
379+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH,
380+
1200
381+
);
382+
});
383+
});
384+
385+
describe('setResponseContentLengthAttributes()', () => {
386+
it('should set response content-length uncompressed attribute with no content-encoding header', () => {
387+
const attributes: Attributes = {};
388+
389+
const response = {} as IncomingMessage;
390+
391+
response.headers = {
392+
'content-length': '1200',
393+
};
394+
utils.setResponseContentLengthAttribute(response, attributes);
395+
396+
verifyValueInAttributes(
397+
attributes,
398+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
399+
1200
400+
);
401+
});
402+
403+
it('should set response content-length uncompressed attribute with "identity" content-encoding header', () => {
404+
const attributes: Attributes = {};
405+
406+
const response = {} as IncomingMessage;
407+
408+
response.headers = {
409+
'content-length': '1200',
410+
'content-encoding': 'identity',
411+
};
412+
413+
utils.setResponseContentLengthAttribute(response, attributes);
414+
415+
verifyValueInAttributes(
416+
attributes,
417+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
418+
1200
419+
);
420+
});
421+
422+
it('should set response content-length compressed attribute with "gzip" content-encoding header', () => {
423+
const attributes: Attributes = {};
424+
425+
const response = {} as IncomingMessage;
426+
427+
response.headers = {
428+
'content-length': '1200',
429+
'content-encoding': 'gzip',
430+
};
431+
432+
utils.setResponseContentLengthAttribute(response, attributes);
433+
434+
verifyValueInAttributes(
435+
attributes,
436+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH,
437+
1200
438+
);
439+
});
440+
441+
it('should set no attributes with no content-length header', () => {
442+
const attributes: Attributes = {};
443+
const message = {} as IncomingMessage;
444+
445+
message.headers = {
446+
'content-encoding': 'gzip',
447+
};
448+
utils.setResponseContentLengthAttribute(message, attributes);
449+
450+
verifyValueInAttributes(attributes, undefined, 1200);
451+
});
452+
});
311453
});

packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts

+40
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,26 @@ export const assertSpan = (
8787
}
8888
}
8989
if (span.kind === SpanKind.CLIENT) {
90+
if (validations.resHeaders['content-length']) {
91+
const contentLength = Number(validations.resHeaders['content-length']);
92+
93+
if (
94+
validations.resHeaders['content-encoding'] &&
95+
validations.resHeaders['content-encoding'] !== 'identity'
96+
) {
97+
assert.strictEqual(
98+
span.attributes[HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH],
99+
contentLength
100+
);
101+
} else {
102+
assert.strictEqual(
103+
span.attributes[
104+
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED
105+
],
106+
contentLength
107+
);
108+
}
109+
}
90110
assert.strictEqual(
91111
span.attributes[GeneralAttribute.NET_PEER_NAME],
92112
validations.hostname,
@@ -108,6 +128,26 @@ export const assertSpan = (
108128
);
109129
}
110130
if (span.kind === SpanKind.SERVER) {
131+
if (validations.reqHeaders && validations.reqHeaders['content-length']) {
132+
const contentLength = validations.reqHeaders['content-length'];
133+
134+
if (
135+
validations.reqHeaders['content-encoding'] &&
136+
validations.reqHeaders['content-encoding'] !== 'identity'
137+
) {
138+
assert.strictEqual(
139+
span.attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH],
140+
contentLength
141+
);
142+
} else {
143+
assert.strictEqual(
144+
span.attributes[
145+
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED
146+
],
147+
contentLength
148+
);
149+
}
150+
}
111151
if (validations.serverName) {
112152
assert.strictEqual(
113153
span.attributes[HttpAttribute.HTTP_SERVER_NAME],

0 commit comments

Comments
 (0)