Skip to content

Commit

Permalink
feat(http-instrumentation): add content size attributes to spans (#1771)
Browse files Browse the repository at this point in the history
Co-authored-by: Gerhard Stöbich <[email protected]>
Co-authored-by: Daniel Dyla <[email protected]>
  • Loading branch information
3 people authored Dec 22, 2020
1 parent 80ea2e0 commit d77a8b9
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 0 deletions.
62 changes: 62 additions & 0 deletions packages/opentelemetry-instrumentation-http/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,66 @@ export const setSpanWithError = (
span.setStatus(status);
};

/**
* Adds attributes for request content-length and content-encoding HTTP headers
* @param { IncomingMessage } Request object whose headers will be analyzed
* @param { Attributes } Attributes object to be modified
*/
export const setRequestContentLengthAttribute = (
request: IncomingMessage,
attributes: Attributes
) => {
const length = getContentLength(request.headers);
if (length === null) return;

if (isCompressed(request.headers)) {
attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH] = length;
} else {
attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED] = length;
}
};

/**
* Adds attributes for response content-length and content-encoding HTTP headers
* @param { IncomingMessage } Response object whose headers will be analyzed
* @param { Attributes } Attributes object to be modified
*/
export const setResponseContentLengthAttribute = (
response: IncomingMessage,
attributes: Attributes
) => {
const length = getContentLength(response.headers);
if (length === null) return;

if (isCompressed(response.headers)) {
attributes[HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH] = length;
} else {
attributes[
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED
] = length;
}
};

function getContentLength(
headers: OutgoingHttpHeaders | IncomingHttpHeaders
): number | null {
const contentLengthHeader = headers['content-length'];
if (contentLengthHeader === undefined) return null;

const contentLength = parseInt(contentLengthHeader as string, 10);
if (isNaN(contentLength)) return null;

return contentLength;
}

export const isCompressed = (
headers: OutgoingHttpHeaders | IncomingHttpHeaders
): boolean => {
const encoding = headers['content-encoding'];

return !!encoding && encoding !== 'identity';
};

/**
* Makes sure options is an url object
* return an object with default value and parsed options
Expand Down Expand Up @@ -326,6 +386,7 @@ export const getOutgoingRequestAttributesOnResponse = (
[GeneralAttribute.NET_PEER_PORT]: remotePort,
[HttpAttribute.HTTP_HOST]: `${options.hostname}:${remotePort}`,
};
setResponseContentLengthAttribute(response, attributes);

if (statusCode) {
attributes[HttpAttribute.HTTP_STATUS_CODE] = statusCode;
Expand Down Expand Up @@ -386,6 +447,7 @@ export const getIncomingRequestAttributes = (
if (userAgent !== undefined) {
attributes[HttpAttribute.HTTP_USER_AGENT] = userAgent;
}
setRequestContentLengthAttribute(request, attributes);

const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
return Object.assign(attributes, httpKindAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import {
Attributes,
StatusCode,
ROOT_CONTEXT,
SpanKind,
Expand Down Expand Up @@ -308,4 +309,145 @@ describe('Utility', () => {
assert.deepEqual(attributes[HttpAttribute.HTTP_ROUTE], undefined);
});
});
// Verify the key in the given attributes is set to the given value,
// and that no other HTTP Content Length attributes are set.
function verifyValueInAttributes(
attributes: Attributes,
key: string | undefined,
value: number
) {
const httpAttributes = [
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH,
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH,
];

for (const attr of httpAttributes) {
if (attr === key) {
assert.strictEqual(attributes[attr], value);
} else {
assert.strictEqual(attributes[attr], undefined);
}
}
}

describe('setRequestContentLengthAttributes()', () => {
it('should set request content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;

request.headers = {
'content-length': '1200',
};
utils.setRequestContentLengthAttribute(request, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});

it('should set request content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};
utils.setRequestContentLengthAttribute(request, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});

it('should set request content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};
utils.setRequestContentLengthAttribute(request, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH,
1200
);
});
});

describe('setResponseContentLengthAttributes()', () => {
it('should set response content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};

const response = {} as IncomingMessage;

response.headers = {
'content-length': '1200',
};
utils.setResponseContentLengthAttribute(response, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});

it('should set response content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: Attributes = {};

const response = {} as IncomingMessage;

response.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};

utils.setResponseContentLengthAttribute(response, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});

it('should set response content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: Attributes = {};

const response = {} as IncomingMessage;

response.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};

utils.setResponseContentLengthAttribute(response, attributes);

verifyValueInAttributes(
attributes,
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH,
1200
);
});

it('should set no attributes with no content-length header', () => {
const attributes: Attributes = {};
const message = {} as IncomingMessage;

message.headers = {
'content-encoding': 'gzip',
};
utils.setResponseContentLengthAttribute(message, attributes);

verifyValueInAttributes(attributes, undefined, 1200);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ export const assertSpan = (
}
}
if (span.kind === SpanKind.CLIENT) {
if (validations.resHeaders['content-length']) {
const contentLength = Number(validations.resHeaders['content-length']);

if (
validations.resHeaders['content-encoding'] &&
validations.resHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[
HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED
],
contentLength
);
}
}
assert.strictEqual(
span.attributes[GeneralAttribute.NET_PEER_NAME],
validations.hostname,
Expand All @@ -108,6 +128,26 @@ export const assertSpan = (
);
}
if (span.kind === SpanKind.SERVER) {
if (validations.reqHeaders && validations.reqHeaders['content-length']) {
const contentLength = validations.reqHeaders['content-length'];

if (
validations.reqHeaders['content-encoding'] &&
validations.reqHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[
HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED
],
contentLength
);
}
}
if (validations.serverName) {
assert.strictEqual(
span.attributes[HttpAttribute.HTTP_SERVER_NAME],
Expand Down

0 comments on commit d77a8b9

Please sign in to comment.