feat(opentelemetry-js): add content size attributes to HTTP spans (#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>
This commit is contained in:
parent
b260f891ee
commit
52c60966f2
|
|
@ -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
|
||||
|
|
@ -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] = (
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Attributes,
|
||||
StatusCode,
|
||||
ROOT_CONTEXT,
|
||||
SpanKind,
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue