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

Http server semconv span stable #4978

Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c5f4b41
feat(instr-http): emit stable semconv for client spans
dyladan Aug 21, 2024
3d1b95d
Document OTEL_SEMCONV_STABILITY_OPT_IN for http
dyladan Aug 21, 2024
d978eb1
Lint
dyladan Aug 21, 2024
963e19a
Merge remote-tracking branch 'origin/main' into http-client-semconv-s…
dyladan Aug 22, 2024
dc75e57
Fix build
dyladan Aug 22, 2024
c71cdb0
Changelog
dyladan Aug 22, 2024
eae1d5c
lint
dyladan Aug 22, 2024
f7efbe8
Add tests for 1.27 semconv
dyladan Aug 23, 2024
2d60245
Merge remote-tracking branch 'origin/main' into http-client-semconv-s…
dyladan Aug 23, 2024
5031276
Use actual remote address for tests
dyladan Aug 23, 2024
10e49f1
Apply fix to stable spans
dyladan Aug 23, 2024
e4a36b1
Merge branch 'main' into http-client-semconv-span-stable
dyladan Aug 28, 2024
9b5ae5b
Update experimental/packages/opentelemetry-instrumentation-http/src/u…
dyladan Sep 4, 2024
83b91f0
chore(deps): update dependency chromedriver to v128 (#4954)
renovate-bot Aug 28, 2024
61f344d
chore(deps): update dependency webpack to v5.94.0 (#4961)
pichlermarc Aug 28, 2024
362a1bc
chore(deps): update dependency glob to v11 (#4955)
renovate-bot Aug 28, 2024
9e2485d
chore(deps): update all patch versions (#4942)
renovate-bot Aug 28, 2024
08c5a9a
chore(deps): update dependency chromedriver to v128.0.1 (#4967)
renovate-bot Sep 2, 2024
4920848
chore(deps): lock file maintenance (#4968)
renovate-bot Sep 2, 2024
93d43ce
Finish transition plan in readme
dyladan Sep 4, 2024
e725146
Review comments
dyladan Sep 4, 2024
0635448
Await server.listen in before to be sure it finishes
dyladan Sep 4, 2024
253c840
Lint
dyladan Sep 4, 2024
2533480
Merge remote-tracking branch 'origin/main' into http-client-semconv-s…
dyladan Sep 4, 2024
e4ccec8
Changelog
dyladan Sep 4, 2024
1bdce0f
Merge remote-tracking branch 'origin/main' into http-client-semconv-s…
dyladan Sep 4, 2024
1c9abb5
Update http server semconv to 1.27
dyladan Sep 4, 2024
9a0c2de
Add server to readme
dyladan Sep 4, 2024
3703045
Lint
dyladan Sep 4, 2024
397ebc6
Fix compilation and test
dyladan Sep 5, 2024
98e02f5
Merge remote-tracking branch 'origin/main' into http-server-semconv-s…
dyladan Sep 11, 2024
7addab4
Fix changelog
dyladan Sep 11, 2024
12d0489
Add tests for server spans
dyladan Sep 11, 2024
4c19670
Lint
dyladan Sep 11, 2024
1054a44
Changelog
dyladan Sep 11, 2024
966eaa5
Change address for tests
dyladan Sep 11, 2024
090f394
Fix node 14 tests
dyladan Sep 11, 2024
46a42e0
remove console
dyladan Sep 11, 2024
826e79b
Add a more detailed changelog
dyladan Sep 17, 2024
0df46dd
Normalize request methods
dyladan Sep 17, 2024
01b90c5
Only set attributes if they are collected
dyladan Sep 17, 2024
bc4d955
More robust host header parsing
dyladan Sep 17, 2024
52c10d4
Lint
dyladan Sep 17, 2024
8a02c47
Merge remote-tracking branch 'origin/main' into http-server-semconv-s…
dyladan Sep 17, 2024
6354a8f
Lint eqeqeq
dyladan Sep 17, 2024
c9df445
Merge branch 'main' into http-server-semconv-span-stable
dyladan Sep 20, 2024
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
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to experimental packages in this project will be documented

* 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 server span semantic conventions 1.27 [#4978](https://github.com/open-telemetry/opentelemetry-js/pull/4978) @dyladan
dyladan marked this conversation as resolved.
Show resolved Hide resolved

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ The following options are deprecated:

## Semantic Conventions

### Client Spans
### Client and Server Spans
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how best to update without making this big PR even bigger, but the section with the table of attributes will need to be updated in some way (starting on line 114 that still references Server Spans). It may be worth having two tables - 1 representing old, 1 representing new. I'm happy to help craft that table in a separate issue and we can issue a follow-up PR. I would recommend at least a small change to that section though along the lines of "These are the attributes in the default behavior using only experimental attributes".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the old table as-is for now. The new semconv is documented already in the semconv repo. When we remove this fallback the old table will be removed entirely. Would that be sufficient or do you think we need a table here as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gotten a lot of requests for showing what attributes are included in instrumentation, especially as different instrumentations (and languages) are in different versions. Folks also ask about other attributes (non-semconv) in different instrumentations, e.g. express.name, though that's less relevant for http here. The semconv might be in the semconv repo but it doesn't specify what we include, and may not be easily navigable by end users. Ideally we autogenerate a list of attributes vs handcrafting a table though, but with the table already there we may as well use it.


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).

Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

NEW DEPENDENCY

This is needed to parse the somewhat complex forwarded header

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing this out - I also had a quick look and it does not seem like there's a easy way to do it 😞
I think using the library is justified.

"semver": "^7.5.2"
},
"homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
request,
instrumentation.getConfig().startIncomingSpanHook
),
semconvStability: instrumentation._semconvStability,
});

const spanOptions: SpanOptions = {
Expand Down Expand Up @@ -760,7 +761,8 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
) {
const attributes = getIncomingRequestAttributesOnResponse(
request,
response
response,
this._semconvStability
);
metricAttributes = Object.assign(
metricAttributes,
Expand Down
199 changes: 180 additions & 19 deletions experimental/packages/opentelemetry-instrumentation-http/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Attributes,
} from '@opentelemetry/api';
import {
ATTR_CLIENT_ADDRESS,
ATTR_ERROR_TYPE,
ATTR_HTTP_REQUEST_METHOD,
ATTR_HTTP_REQUEST_METHOD_ORIGINAL,
Expand All @@ -33,6 +34,9 @@ import {
ATTR_SERVER_ADDRESS,
ATTR_SERVER_PORT,
ATTR_URL_FULL,
ATTR_URL_PATH,
ATTR_URL_SCHEME,
ATTR_USER_AGENT_ORIGINAL,
NETTRANSPORTVALUES_IP_TCP,
NETTRANSPORTVALUES_IP_UDP,
SEMATTRS_HTTP_CLIENT_IP,
Expand Down Expand Up @@ -74,6 +78,7 @@ import {
ParsedRequestOptions,
SemconvStability,
} from './types';
import forwardedParse = require('forwarded-parse');

/**
* Get an absolute url
Expand Down Expand Up @@ -536,17 +541,134 @@ export const getOutgoingRequestMetricAttributesOnResponse = (
return metricAttributes;
};

function parseHostHeader(
hostHeader: string,
proto?: string
): { host: string; port?: string } {
const [host, port] = hostHeader.split(':');
dyladan marked this conversation as resolved.
Show resolved Hide resolved
if (host && port) {
return { host, port };
}

if (proto === 'http') {
return { host, port: '80' };
}

if (proto === 'https') {
return { host, port: '443' };
}

return { host };
}

/**
* 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;
Expand All @@ -560,8 +682,26 @@ export const getIncomingRequestAttributes = (
requestUrl?.hostname ||
host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') ||
'localhost';

const serverAddress = getServerAddress(request, options.component);
const serverName = options.serverName;
const attributes: SpanAttributes = {

const newAttributes: Attributes = {
[ATTR_HTTP_REQUEST_METHOD]: request.method,
dyladan marked this conversation as resolved.
Show resolved Hide resolved
[ATTR_URL_PATH]: requestUrl?.pathname ?? undefined,
[ATTR_URL_SCHEME]: options.component,
[ATTR_SERVER_ADDRESS]: serverAddress?.host,
[ATTR_SERVER_PORT]: serverAddress?.port
? Number(serverAddress.port)
: undefined,
[ATTR_CLIENT_ADDRESS]: getRemoteClientAddress(request) ?? undefined,
dyladan marked this conversation as resolved.
Show resolved Hide resolved
[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,
};

const oldAttributes: Attributes = {
[SEMATTRS_HTTP_URL]: getAbsoluteUrl(
requestUrl,
headers,
Expand All @@ -574,23 +714,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, 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);
}
setRequestContentLengthAttribute(request, attributes);
setAttributesFromHttpKind(httpVersion, attributes);
return Object.assign(attributes, options.hookAttributes);

return Object.assign(oldAttributes, newAttributes, options.hookAttributes);
};

/**
Expand All @@ -617,31 +765,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;
}
return attributes;

switch (semconvStability) {
case SemconvStability.STABLE:
return newAttributes;
case SemconvStability.OLD:
return oldAttributes;
}

return Object.assign(oldAttributes, newAttributes);
};

/**
Expand Down
Loading