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 5 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
47 changes: 46 additions & 1 deletion packages/opentelemetry-plugin-http/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,46 @@ export const setSpanWithError = (
span.setStatus(status);
};

/**
* Adds attributes for content-length and content-encoding HTTP headers
* @param { OutgoingHttpHeaders | IncomingHttpHeaders } headers http headers
* @param { Attributes } Attributes span attributes
* @param { boolean } isRequest set true for setting request content-header
*/
export const setContentLengthAttributes = (
headers: OutgoingHttpHeaders | IncomingHttpHeaders,
attributes: Attributes,
isRequest: boolean
) => {
let isCompressed = false;

if (headers['content-length'] === undefined) return;

if (
headers['content-encoding'] &&
nijotz marked this conversation as resolved.
Show resolved Hide resolved
headers['content-encoding'] !== 'identity'
nijotz marked this conversation as resolved.
Show resolved Hide resolved
) {
isCompressed = true;
}

let key = null;
nijotz marked this conversation as resolved.
Show resolved Hide resolved
if (isCompressed) {
if (isRequest) {
key = HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH;
} else {
key = HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH;
}
} else {
if (isRequest) {
key = HttpAttribute.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED;
} else {
key = HttpAttribute.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED;
}
}

attributes[key] = Number(headers['content-length']);
};

/**
* Makes sure options is an url object
* return an object with default value and parsed options
Expand Down Expand Up @@ -356,14 +396,17 @@ export const getOutgoingRequestAttributesOnResponse = (
response: IncomingMessage,
options: { hostname: string }
): Attributes => {
const { statusCode, statusMessage, httpVersion, socket } = response;
const { statusCode, statusMessage, httpVersion, headers, 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}`,
};

setContentLengthAttributes(headers, attributes, false);

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

setContentLengthAttributes(headers, attributes, true);

const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
return Object.assign(attributes, httpKindAttributes);
};
Expand Down
123 changes: 122 additions & 1 deletion packages/opentelemetry-plugin-http/test/functionals/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CanonicalCode, SpanKind, TraceFlags } from '@opentelemetry/api';
import {
Attributes,
CanonicalCode,
SpanKind,
TraceFlags,
} from '@opentelemetry/api';
import { NoopLogger } from '@opentelemetry/core';
import { BasicTracerProvider, Span } from '@opentelemetry/tracing';
import { HttpAttribute } from '@opentelemetry/semantic-conventions';
Expand Down Expand Up @@ -309,4 +314,120 @@ describe('Utility', () => {
assert.deepEqual(attributes[HttpAttribute.HTTP_ROUTE], undefined);
});
});

describe('setContentLengthAttributes', () => {
// 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);
}
}
}

it('should set response content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};
const headers: http.IncomingHttpHeaders = {};
headers['content-length'] = '1200';
utils.setContentLengthAttributes(headers, attributes, false);

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 headers: http.IncomingHttpHeaders = {};
headers['content-length'] = '1200';
headers['content-encoding'] = 'identity';
utils.setContentLengthAttributes(headers, attributes, false);

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 headers: http.IncomingHttpHeaders = {};
headers['content-length'] = '1200';
headers['content-encoding'] = 'gzip';
utils.setContentLengthAttributes(headers, attributes, false);

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

it('should set request content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};
const headers: http.OutgoingHttpHeaders = {};
headers['content-length'] = '1200';
utils.setContentLengthAttributes(headers, attributes, true);

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 headers: http.OutgoingHttpHeaders = {};
headers['content-length'] = '1200';
headers['content-encoding'] = 'identity';
utils.setContentLengthAttributes(headers, attributes, true);

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 headers: http.OutgoingHttpHeaders = {};
headers['content-length'] = '1200';
headers['content-encoding'] = 'gzip';
utils.setContentLengthAttributes(headers, attributes, true);

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

it('should set no attributes with no content-length header', () => {
const attributes: Attributes = {};
const headers: http.OutgoingHttpHeaders = {};
headers['content-encoding'] = 'gzip';
utils.setContentLengthAttributes(headers, attributes, true);

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'
nijotz marked this conversation as resolved.
Show resolved Hide resolved
) {
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'
nijotz marked this conversation as resolved.
Show resolved Hide resolved
) {
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