Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(opentelemetry-js): add content size attributes to HTTP spans #1625

Merged
merged 15 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
65 changes: 65 additions & 0 deletions packages/opentelemetry-plugin-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'];
dyladan marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -318,12 +378,15 @@ export const getOutgoingRequestAttributesOnResponse = (
): Attributes => {
const { statusCode, statusMessage, httpVersion, socket } = response;
const { remoteAddress, remotePort } = socket;

const attributes: Attributes = {
[GeneralAttribute.NET_PEER_IP]: remoteAddress,
[GeneralAttribute.NET_PEER_PORT]: remotePort,
[HttpAttribute.HTTP_HOST]: `${options.hostname}:${remotePort}`,
};

setResponseContentLengthAttribute(response, attributes);

if (statusCode) {
attributes[HttpAttribute.HTTP_STATUS_CODE] = statusCode;
attributes[HttpAttribute.HTTP_STATUS_TEXT] = (
Expand Down Expand Up @@ -384,6 +447,8 @@ export const getIncomingRequestAttributes = (
attributes[HttpAttribute.HTTP_USER_AGENT] = userAgent;
}

setRequestContentLengthAttribute(request, attributes);

const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
return Object.assign(attributes, httpKindAttributes);
};
Expand Down
143 changes: 143 additions & 0 deletions packages/opentelemetry-plugin-http/test/functionals/utils.test.ts
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,146 @@ 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);
});
});
});
43 changes: 43 additions & 0 deletions packages/opentelemetry-plugin-http/test/utils/assertSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,29 @@ 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 @@ -105,6 +127,27 @@ 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
5 changes: 5 additions & 0 deletions packages/opentelemetry-semantic-conventions/src/trace/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const HttpAttribute = {
HTTP_CLIENT_IP: 'http.client_ip',
HTTP_SCHEME: 'http.scheme',
HTTP_RESPONSE_CONTENT_LENGTH: 'http.response_content_length',
HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED:
'http.response_content_length_uncompressed',
HTTP_REQUEST_CONTENT_LENGTH: 'http.request_content_length',
HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED:
'http.request_content_length_uncompressed',

// NOT ON OFFICIAL SPEC
HTTP_ERROR_NAME: 'http.error_name',
Expand Down