diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 4ac335a9cc..13454c6eb4 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,7 +10,10 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) * feat(api-logs): Add delegating no-op logger provider [#4861](https://github.com/open-telemetry/opentelemetry-js/pull/4861) @hectorhdzg -* feat(instrumentation-http): Add support for client span semantic conventions 1.27 [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) @dyladan +* feat(instrumentation-http): Add support for [Semantic Conventions 1.27+](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.27.0) [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) [#4978](https://github.com/open-telemetry/opentelemetry-js/pull/4978) @dyladan + * Applies to both client and server spans + * Generate spans compliant with Semantic Conventions 1.27+ when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` or `http/dup` + * Generate spans backwards compatible with previous attributes when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT contain `http` ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-instrumentation-http/README.md b/experimental/packages/opentelemetry-instrumentation-http/README.md index cdd2f2337f..456d5b3473 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/README.md +++ b/experimental/packages/opentelemetry-instrumentation-http/README.md @@ -76,7 +76,7 @@ The following options are deprecated: ## Semantic Conventions -### Client Spans +### Client and Server Spans Prior to version `0.54`, this instrumentation created spans targeting an experimental semantic convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md). @@ -91,7 +91,7 @@ If neither `http` or `http/dup` is included in `OTEL_SEMCONV_STABILITY_OPT_IN`, Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` OR `http/dup`. This is the recommended configuration, and will soon become the default behavior. -Follow all requirements and recommendations of HTTP Client Span Semantic Conventions [Version 1.27.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md), including all required and recommended attributes. +Follow all requirements and recommendations of HTTP Client and Server Span Semantic Conventions [Version 1.27.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md), including all required and recommended attributes. #### Legacy Behavior (default) diff --git a/experimental/packages/opentelemetry-instrumentation-http/package.json b/experimental/packages/opentelemetry-instrumentation-http/package.json index ae7e01b0d5..930e1e744d 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/package.json +++ b/experimental/packages/opentelemetry-instrumentation-http/package.json @@ -80,6 +80,7 @@ "@opentelemetry/core": "1.26.0", "@opentelemetry/instrumentation": "0.53.0", "@opentelemetry/semantic-conventions": "1.27.0", + "forwarded-parse": "2.1.2", "semver": "^7.5.2" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http", diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index 06bc3cac50..81e56183a1 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -528,6 +528,7 @@ export class HttpInstrumentation extends InstrumentationBase { const hostname = options.hostname; const port = options.port; - const requestMethod = requestOptions.method; - const method = requestMethod ? requestMethod.toUpperCase() : 'GET'; + const method = requestOptions.method ?? 'GET'; + const normalizedMethod = normalizeMethod(method); const headers = requestOptions.headers || {}; const userAgent = headers['user-agent']; const urlFull = getAbsoluteUrl( @@ -409,7 +414,7 @@ export const getOutgoingRequestAttributes = ( const newAttributes: Attributes = { // Required attributes - [ATTR_HTTP_REQUEST_METHOD]: method, + [ATTR_HTTP_REQUEST_METHOD]: normalizedMethod, [ATTR_SERVER_ADDRESS]: hostname, [ATTR_SERVER_PORT]: Number(port), [ATTR_URL_FULL]: urlFull, @@ -421,8 +426,8 @@ export const getOutgoingRequestAttributes = ( }; // conditionally required if request method required case normalization - if (requestMethod && method !== requestMethod) { - newAttributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = requestMethod; + if (method !== normalizedMethod) { + newAttributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = method; } if (userAgent !== undefined) { @@ -536,23 +541,174 @@ export const getOutgoingRequestMetricAttributesOnResponse = ( return metricAttributes; }; +function parseHostHeader( + hostHeader: string, + proto?: string +): { host: string; port?: string } { + const parts = hostHeader.split(':'); + + // no semicolon implies ipv4 dotted syntax or host name without port + // x.x.x.x + // example.com + if (parts.length === 1) { + if (proto === 'http') { + return { host: parts[0], port: '80' }; + } + + if (proto === 'https') { + return { host: parts[0], port: '443' }; + } + + return { host: parts[0] }; + } + + // single semicolon implies ipv4 dotted syntax or host name with port + // x.x.x.x:yyyy + // example.com:yyyy + if (parts.length === 2) { + return { + host: parts[0], + port: parts[1], + }; + } + + // more than 2 parts implies ipv6 syntax with multiple colons + // [x:x:x:x:x:x:x:x] + // [x:x:x:x:x:x:x:x]:yyyy + if (parts[0].startsWith('[')) { + if (parts[parts.length - 1].endsWith(']')) { + if (proto === 'http') { + return { host: hostHeader, port: '80' }; + } + + if (proto === 'https') { + return { host: hostHeader, port: '443' }; + } + } else if (parts[parts.length - 2].endsWith(']')) { + return { + host: parts.slice(0, -1).join(':'), + port: parts[parts.length - 1], + }; + } + } + + // if nothing above matches just return the host header + return { host: hostHeader }; +} + +/** + * Get server.address and port according to http semconv 1.27 + * https://github.com/open-telemetry/semantic-conventions/blob/bf0a2c1134f206f034408b201dbec37960ed60ec/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes + */ +function getServerAddress( + request: IncomingMessage, + component: 'http' | 'https' +): { host: string; port?: string } | null { + const forwardedHeader = request.headers['forwarded']; + if (forwardedHeader) { + for (const entry of forwardedParse(forwardedHeader)) { + if (entry.host) { + return parseHostHeader(entry.host, entry.proto); + } + } + } + + const xForwardedHost = request.headers['x-forwarded-host']; + if (typeof xForwardedHost === 'string') { + if (typeof request.headers['x-forwarded-proto'] === 'string') { + return parseHostHeader( + xForwardedHost, + request.headers['x-forwarded-proto'] + ); + } + + if (Array.isArray(request.headers['x-forwarded-proto'])) { + return parseHostHeader( + xForwardedHost, + request.headers['x-forwarded-proto'][0] + ); + } + + return parseHostHeader(xForwardedHost); + } else if ( + Array.isArray(xForwardedHost) && + typeof xForwardedHost[0] === 'string' && + xForwardedHost[0].length > 0 + ) { + if (typeof request.headers['x-forwarded-proto'] === 'string') { + return parseHostHeader( + xForwardedHost[0], + request.headers['x-forwarded-proto'] + ); + } + + if (Array.isArray(request.headers['x-forwarded-proto'])) { + return parseHostHeader( + xForwardedHost[0], + request.headers['x-forwarded-proto'][0] + ); + } + + return parseHostHeader(xForwardedHost[0]); + } + + const host = request.headers['host']; + if (typeof host === 'string' && host.length > 0) { + return parseHostHeader(host, component); + } + + return null; +} + +/** + * Get server.address and port according to http semconv 1.27 + * https://github.com/open-telemetry/semantic-conventions/blob/bf0a2c1134f206f034408b201dbec37960ed60ec/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes + */ +export function getRemoteClientAddress( + request: IncomingMessage +): string | null { + const forwardedHeader = request.headers['forwarded']; + if (forwardedHeader) { + for (const entry of forwardedParse(forwardedHeader)) { + if (entry.for) { + return entry.for; + } + } + } + + const xForwardedFor = request.headers['x-forwarded-for']; + if (typeof xForwardedFor === 'string') { + return xForwardedFor; + } else if (Array.isArray(xForwardedFor)) { + return xForwardedFor[0]; + } + + const remote = request.socket.remoteAddress; + if (remote) { + return remote; + } + + return null; +} + /** * Returns incoming request attributes scoped to the request data * @param {IncomingMessage} request the request object * @param {{ component: string, serverName?: string, hookAttributes?: SpanAttributes }} options used to pass data needed to create attributes + * @param {SemconvStability} semconvStability determines which semconv version to use */ export const getIncomingRequestAttributes = ( request: IncomingMessage, options: { - component: string; + component: 'http' | 'https'; serverName?: string; hookAttributes?: SpanAttributes; + semconvStability: SemconvStability; } ): SpanAttributes => { const headers = request.headers; const userAgent = headers['user-agent']; const ips = headers['x-forwarded-for']; - const method = request.method || 'GET'; const httpVersion = request.httpVersion; const requestUrl = request.url ? url.parse(request.url) : null; const host = requestUrl?.host || headers.host; @@ -560,8 +716,43 @@ export const getIncomingRequestAttributes = ( requestUrl?.hostname || host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || 'localhost'; + + const method = request.method; + const normalizedMethod = normalizeMethod(method); + + const serverAddress = getServerAddress(request, options.component); const serverName = options.serverName; - const attributes: SpanAttributes = { + + const remoteClientAddress = getRemoteClientAddress(request); + + const newAttributes: Attributes = { + [ATTR_HTTP_REQUEST_METHOD]: normalizedMethod, + [ATTR_URL_SCHEME]: options.component, + [ATTR_SERVER_ADDRESS]: serverAddress?.host, + [ATTR_NETWORK_PEER_ADDRESS]: request.socket.remoteAddress, + [ATTR_NETWORK_PEER_PORT]: request.socket.remotePort, + [ATTR_NETWORK_PROTOCOL_VERSION]: request.httpVersion, + [ATTR_USER_AGENT_ORIGINAL]: userAgent, + }; + + if (requestUrl?.pathname != null) { + newAttributes[ATTR_URL_PATH] = requestUrl.pathname; + } + + if (remoteClientAddress != null) { + newAttributes[ATTR_CLIENT_ADDRESS] = remoteClientAddress; + } + + if (serverAddress?.port != null) { + newAttributes[ATTR_SERVER_PORT] = Number(serverAddress.port); + } + + // conditionally required if request method required case normalization + if (method !== normalizedMethod) { + newAttributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = method; + } + + const oldAttributes: Attributes = { [SEMATTRS_HTTP_URL]: getAbsoluteUrl( requestUrl, headers, @@ -574,23 +765,31 @@ export const getIncomingRequestAttributes = ( }; if (typeof ips === 'string') { - attributes[SEMATTRS_HTTP_CLIENT_IP] = ips.split(',')[0]; + oldAttributes[SEMATTRS_HTTP_CLIENT_IP] = ips.split(',')[0]; } if (typeof serverName === 'string') { - attributes[SEMATTRS_HTTP_SERVER_NAME] = serverName; + oldAttributes[SEMATTRS_HTTP_SERVER_NAME] = serverName; } if (requestUrl) { - attributes[SEMATTRS_HTTP_TARGET] = requestUrl.path || '/'; + oldAttributes[SEMATTRS_HTTP_TARGET] = requestUrl.path || '/'; } if (userAgent !== undefined) { - attributes[SEMATTRS_HTTP_USER_AGENT] = userAgent; + oldAttributes[SEMATTRS_HTTP_USER_AGENT] = userAgent; } - setRequestContentLengthAttribute(request, attributes); - setAttributesFromHttpKind(httpVersion, attributes); - return Object.assign(attributes, options.hookAttributes); + setRequestContentLengthAttribute(request, oldAttributes); + setAttributesFromHttpKind(httpVersion, oldAttributes); + + switch (options.semconvStability) { + case SemconvStability.STABLE: + return Object.assign(newAttributes, options.hookAttributes); + case SemconvStability.OLD: + return Object.assign(oldAttributes, options.hookAttributes); + } + + return Object.assign(oldAttributes, newAttributes, options.hookAttributes); }; /** @@ -617,31 +816,44 @@ export const getIncomingRequestMetricAttributes = ( */ export const getIncomingRequestAttributesOnResponse = ( request: IncomingMessage, - response: ServerResponse + response: ServerResponse, + semconvStability: SemconvStability ): SpanAttributes => { // take socket from the request, // since it may be detached from the response object in keep-alive mode const { socket } = request; const { statusCode, statusMessage } = response; + const newAttributes = { + [ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode, + }; + const rpcMetadata = getRPCMetadata(context.active()); - const attributes: SpanAttributes = {}; + const oldAttributes: SpanAttributes = {}; if (socket) { const { localAddress, localPort, remoteAddress, remotePort } = socket; - attributes[SEMATTRS_NET_HOST_IP] = localAddress; - attributes[SEMATTRS_NET_HOST_PORT] = localPort; - attributes[SEMATTRS_NET_PEER_IP] = remoteAddress; - attributes[SEMATTRS_NET_PEER_PORT] = remotePort; + oldAttributes[SEMATTRS_NET_HOST_IP] = localAddress; + oldAttributes[SEMATTRS_NET_HOST_PORT] = localPort; + oldAttributes[SEMATTRS_NET_PEER_IP] = remoteAddress; + oldAttributes[SEMATTRS_NET_PEER_PORT] = remotePort; } - attributes[SEMATTRS_HTTP_STATUS_CODE] = statusCode; - attributes[AttributeNames.HTTP_STATUS_TEXT] = ( + oldAttributes[SEMATTRS_HTTP_STATUS_CODE] = statusCode; + oldAttributes[AttributeNames.HTTP_STATUS_TEXT] = ( statusMessage || '' ).toUpperCase(); if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) { - attributes[SEMATTRS_HTTP_ROUTE] = rpcMetadata.route; + oldAttributes[SEMATTRS_HTTP_ROUTE] = rpcMetadata.route; + } + + switch (semconvStability) { + case SemconvStability.STABLE: + return newAttributes; + case SemconvStability.OLD: + return oldAttributes; } - return attributes; + + return Object.assign(oldAttributes, newAttributes); }; /** @@ -693,3 +905,31 @@ export function headerCapture(type: 'request' | 'response', headers: string[]) { } }; } + +const KNOWN_METHODS = new Set([ + // methods from https://www.rfc-editor.org/rfc/rfc9110.html#name-methods + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + + // PATCH from https://www.rfc-editor.org/rfc/rfc5789.html + 'PATCH', +]); + +function normalizeMethod(method?: string | null) { + if (method == null) { + return 'GET'; + } + + const upper = method.toUpperCase(); + if (KNOWN_METHODS.has(upper)) { + return upper; + } + + return '_OTHER'; +} diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts index 047089c9d8..e7270ebdc9 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts @@ -30,6 +30,7 @@ import { SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; import { + ATTR_CLIENT_ADDRESS, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_NETWORK_PEER_ADDRESS, @@ -38,6 +39,8 @@ import { ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, + ATTR_URL_PATH, + ATTR_URL_SCHEME, HTTP_REQUEST_METHOD_VALUE_GET, NETTRANSPORTVALUES_IP_TCP, SEMATTRS_HTTP_CLIENT_IP, @@ -46,9 +49,12 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, SEMATTRS_HTTP_ROUTE, + SEMATTRS_HTTP_SCHEME, SEMATTRS_HTTP_STATUS_CODE, SEMATTRS_HTTP_TARGET, SEMATTRS_HTTP_URL, + SEMATTRS_NET_HOST_IP, + SEMATTRS_NET_HOST_NAME, SEMATTRS_NET_HOST_PORT, SEMATTRS_NET_PEER_IP, SEMATTRS_NET_PEER_NAME, @@ -80,6 +86,7 @@ instrumentation.disable(); import * as http from 'http'; import { AttributeNames } from '../../src/enums/AttributeNames'; +import { getRemoteClientAddress } from '../../src/utils'; const applyCustomAttributesOnSpanErrorMessage = 'bad applyCustomAttributesOnSpan function'; @@ -1068,7 +1075,10 @@ describe('HttpInstrumentation', () => { assert.strictEqual(rpcData.route, undefined); rpcData.route = 'TheRoute'; } - response.end('Test Server Response'); + response.setHeader('Content-Type', 'application/json'); + response.end( + JSON.stringify({ address: getRemoteClientAddress(request) }) + ); }); await new Promise(resolve => server.listen(serverPort, resolve)); @@ -1079,7 +1089,7 @@ describe('HttpInstrumentation', () => { instrumentation.disable(); }); - it('should generate semconv 1.27 spans', async () => { + it('should generate semconv 1.27 client spans', async () => { const response = await httpRequest.get( `${protocol}://${hostname}:${serverPort}${pathname}` ); @@ -1099,6 +1109,31 @@ describe('HttpInstrumentation', () => { [ATTR_NETWORK_PROTOCOL_VERSION]: '1.1', }); }); + + it('should generate semconv 1.27 server spans', async () => { + const response = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, _] = spans; + assert.strictEqual(spans.length, 2); + + const body = JSON.parse(response.data); + + // should have only required and recommended attributes for semconv 1.27 + assert.deepStrictEqual(incomingSpan.attributes, { + [ATTR_CLIENT_ADDRESS]: body.address, + [ATTR_HTTP_REQUEST_METHOD]: HTTP_REQUEST_METHOD_VALUE_GET, + [ATTR_SERVER_ADDRESS]: hostname, + [ATTR_SERVER_PORT]: serverPort, + [ATTR_HTTP_RESPONSE_STATUS_CODE]: 200, + [ATTR_NETWORK_PEER_ADDRESS]: body.address, + [ATTR_NETWORK_PEER_PORT]: response.clientRemotePort, + [ATTR_NETWORK_PROTOCOL_VERSION]: '1.1', + [ATTR_URL_PATH]: pathname, + [ATTR_URL_SCHEME]: protocol, + }); + }); }); describe('with semconv stability set to http/dup', () => { @@ -1110,8 +1145,11 @@ describe('HttpInstrumentation', () => { before(async () => { instrumentation['_semconvStability'] = SemconvStability.DUPLICATE; instrumentation.enable(); - server = http.createServer((_, response) => { - response.end('Test Server Response'); + server = http.createServer((request, response) => { + response.setHeader('Content-Type', 'application/json'); + response.end( + JSON.stringify({ address: getRemoteClientAddress(request) }) + ); }); await new Promise(resolve => server.listen(serverPort, resolve)); @@ -1146,7 +1184,8 @@ describe('HttpInstrumentation', () => { [SEMATTRS_HTTP_FLAVOR]: '1.1', [SEMATTRS_HTTP_HOST]: `${hostname}:${serverPort}`, [SEMATTRS_HTTP_METHOD]: 'GET', - [SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED]: 20, + [SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED]: + response.data.length, [SEMATTRS_HTTP_STATUS_CODE]: 200, [SEMATTRS_HTTP_TARGET]: '/test', [SEMATTRS_HTTP_URL]: `http://${hostname}:${serverPort}${pathname}`, @@ -1159,6 +1198,49 @@ describe('HttpInstrumentation', () => { [AttributeNames.HTTP_STATUS_TEXT]: 'OK', }); }); + + it('should create server spans with semconv 1.27 and old 1.7', async () => { + const response = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const incomingSpan = spans[0]; + const body = JSON.parse(response.data); + + // should have only required and recommended attributes for semconv 1.27 + assert.deepStrictEqual(incomingSpan.attributes, { + // 1.27 attributes + [ATTR_CLIENT_ADDRESS]: body.address, + [ATTR_HTTP_REQUEST_METHOD]: HTTP_REQUEST_METHOD_VALUE_GET, + [ATTR_SERVER_ADDRESS]: hostname, + [ATTR_SERVER_PORT]: serverPort, + [ATTR_HTTP_RESPONSE_STATUS_CODE]: 200, + [ATTR_NETWORK_PEER_ADDRESS]: body.address, + [ATTR_NETWORK_PEER_PORT]: response.clientRemotePort, + [ATTR_NETWORK_PROTOCOL_VERSION]: '1.1', + [ATTR_URL_PATH]: pathname, + [ATTR_URL_SCHEME]: protocol, + + // 1.7 attributes + [SEMATTRS_HTTP_FLAVOR]: '1.1', + [SEMATTRS_HTTP_HOST]: `${hostname}:${serverPort}`, + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_SCHEME]: protocol, + [SEMATTRS_HTTP_STATUS_CODE]: 200, + [SEMATTRS_HTTP_TARGET]: '/test', + [SEMATTRS_HTTP_URL]: `http://${hostname}:${serverPort}${pathname}`, + [SEMATTRS_NET_TRANSPORT]: 'ip_tcp', + [SEMATTRS_NET_HOST_IP]: body.address, + [SEMATTRS_NET_HOST_NAME]: hostname, + [SEMATTRS_NET_HOST_PORT]: serverPort, + [SEMATTRS_NET_PEER_IP]: body.address, + [SEMATTRS_NET_PEER_PORT]: response.clientRemotePort, + + // unspecified old names + [AttributeNames.HTTP_STATUS_TEXT]: 'OK', + }); + }); }); describe('with require parent span', () => { diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts index 4b64e93681..d64f795383 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts @@ -313,7 +313,8 @@ describe('Utility', () => { () => { const attributes = utils.getIncomingRequestAttributesOnResponse( request, - {} as ServerResponse + {} as ServerResponse, + SemconvStability.OLD ); assert.deepStrictEqual(attributes[SEMATTRS_HTTP_ROUTE], '/user/:id'); context.disable(); @@ -326,9 +327,13 @@ describe('Utility', () => { const request = { socket: {}, } as IncomingMessage; - const attributes = utils.getIncomingRequestAttributesOnResponse(request, { - socket: {}, - } as ServerResponse & { socket: Socket }); + const attributes = utils.getIncomingRequestAttributesOnResponse( + request, + { + socket: {}, + } as ServerResponse & { socket: Socket }, + SemconvStability.OLD + ); assert.deepEqual(attributes[SEMATTRS_HTTP_ROUTE], undefined); }); }); @@ -501,6 +506,7 @@ describe('Utility', () => { const request = { url: 'http://hostname/user/:id', method: 'GET', + socket: {}, } as IncomingMessage; request.headers = { 'user-agent': 'chrome', @@ -508,6 +514,7 @@ describe('Utility', () => { }; const attributes = utils.getIncomingRequestAttributes(request, { component: 'http', + semconvStability: SemconvStability.OLD, }); assert.strictEqual(attributes[SEMATTRS_HTTP_ROUTE], undefined); }); @@ -516,12 +523,14 @@ describe('Utility', () => { const request = { url: 'http://hostname/user/?q=val', method: 'GET', + socket: {}, } as IncomingMessage; request.headers = { 'user-agent': 'chrome', }; const attributes = utils.getIncomingRequestAttributes(request, { component: 'http', + semconvStability: SemconvStability.OLD, }); assert.strictEqual(attributes[SEMATTRS_HTTP_TARGET], '/user/?q=val'); }); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts b/experimental/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts index 14be1f8b11..76d1b600c1 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts @@ -23,6 +23,10 @@ type GetResult = Promise<{ reqHeaders: http.OutgoingHttpHeaders; method: string | undefined; address?: string; + clientRemotePort?: number; + clientRemoteAddress?: string; + req: http.OutgoingMessage; + res: http.IncomingMessage; }>; function get(input: string | URL, options?: http.RequestOptions): GetResult; @@ -48,6 +52,10 @@ function get(input: any, options?: any): GetResult { resHeaders: res.headers, method: res.req.method, address: req.socket?.remoteAddress, + clientRemotePort: res.req.socket?.localPort, + clientRemoteAddress: res.req.socket?.localAddress, + req, + res, }); }); resp.on('error', err => { diff --git a/package-lock.json b/package-lock.json index 9e86857d99..a641a47810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -974,6 +974,7 @@ "@opentelemetry/core": "1.26.0", "@opentelemetry/instrumentation": "0.53.0", "@opentelemetry/semantic-conventions": "1.27.0", + "forwarded-parse": "2.1.2", "semver": "^7.5.2" }, "devDependencies": { @@ -14184,6 +14185,12 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -33252,6 +33259,7 @@ "axios": "1.7.4", "codecov": "3.8.3", "cross-var": "1.1.0", + "forwarded-parse": "2.1.2", "lerna": "6.6.2", "mocha": "10.7.3", "nock": "13.3.8", @@ -40800,6 +40808,11 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, + "forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",