Skip to content

Commit

Permalink
feat(opentelemetry-js): add content size attributes to HTTP spans (#…
Browse files Browse the repository at this point in the history
…1625)

* feat(opentelemetry-js): extract content-length header for span attrs

Signed-off-by: Carlo Pearson <cpearson@newrelic.com>

* feat(opentelemetry-js): add content-length attributes to HTTP spans

Signed-off-by: Carlo Pearson <cpearson@newrelic.com>

* feat(opentelemetry-js): linting

* feat(opentelemetry-js): verify content length attributes are a number

* feat(opentelemetry-js): refactor setting content-length attributes

* feat(opentelemetry-js): lint fixes

* feat(opentelemetry-js): lint fixes

* fix: incorrect docs

Co-authored-by: Carlo Pearson <cpearson@newrelic.com>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
Co-authored-by: Bartlomiej Obecny <bobecny@gmail.com>
  • Loading branch information
4 people committed Dec 10, 2020
1 parent b260f89 commit 52c6096
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
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'];
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

0 comments on commit 52c6096

Please sign in to comment.