Skip to content

Commit 8e5c8a0

Browse files
authored
feat(core): add specific context key to store rpc metadata (#2208)
1 parent ac81e17 commit 8e5c8a0

File tree

6 files changed

+175
-82
lines changed

6 files changed

+175
-82
lines changed

packages/opentelemetry-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './platform';
2727
export * from './propagation/composite';
2828
export * from './trace/HttpTraceContextPropagator';
2929
export * from './trace/IdGenerator';
30+
export * from './trace/rpc-metadata';
3031
export * from './trace/sampler/AlwaysOffSampler';
3132
export * from './trace/sampler/AlwaysOnSampler';
3233
export * from './trace/sampler/ParentBasedSampler';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Context, createContextKey, Span } from '@opentelemetry/api';
18+
19+
const RPC_METADATA_KEY = createContextKey(
20+
'OpenTelemetry SDK Context Key RPC_METADATA'
21+
);
22+
23+
export enum RPCType {
24+
HTTP = 'http',
25+
}
26+
27+
type HTTPMetadata = {
28+
type: RPCType.HTTP;
29+
route?: string;
30+
span: Span;
31+
};
32+
33+
/**
34+
* Allows for future rpc metadata to be used with this mechanism
35+
*/
36+
export type RPCMetadata = HTTPMetadata;
37+
38+
export function setRPCMetadata(context: Context, meta: RPCMetadata): Context {
39+
return context.setValue(RPC_METADATA_KEY, meta);
40+
}
41+
42+
export function deleteRPCMetadata(context: Context): Context {
43+
return context.deleteValue(RPC_METADATA_KEY);
44+
}
45+
46+
export function getRPCMetadata(context: Context): RPCMetadata | undefined {
47+
return context.getValue(RPC_METADATA_KEY) as RPCMetadata | undefined;
48+
}

packages/opentelemetry-instrumentation-http/src/http.ts

Lines changed: 66 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
isWrapped,
5151
safeExecuteInTheMiddle,
5252
} from '@opentelemetry/instrumentation';
53+
import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core';
5354

5455
/**
5556
* Http instrumentation instrumentation for Opentelemetry
@@ -409,29 +410,72 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
409410
spanOptions,
410411
ctx
411412
);
413+
const rpcMetadata: RPCMetadata = {
414+
type: RPCType.HTTP,
415+
span,
416+
};
412417

413-
return context.with(trace.setSpan(ctx, span), () => {
414-
context.bind(request);
415-
context.bind(response);
418+
return context.with(
419+
setRPCMetadata(trace.setSpan(ctx, span), rpcMetadata),
420+
() => {
421+
context.bind(request);
422+
context.bind(response);
416423

417-
if (instrumentation._getConfig().requestHook) {
418-
instrumentation._callRequestHook(span, request);
419-
}
420-
if (instrumentation._getConfig().responseHook) {
421-
instrumentation._callResponseHook(span, response);
422-
}
424+
if (instrumentation._getConfig().requestHook) {
425+
instrumentation._callRequestHook(span, request);
426+
}
427+
if (instrumentation._getConfig().responseHook) {
428+
instrumentation._callResponseHook(span, response);
429+
}
423430

424-
// Wraps end (inspired by:
425-
// https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-connect.ts#L75)
426-
const originalEnd = response.end;
427-
response.end = function (
428-
this: http.ServerResponse,
429-
..._args: ResponseEndArgs
430-
) {
431-
response.end = originalEnd;
432-
// Cannot pass args of type ResponseEndArgs,
433-
const returned = safeExecuteInTheMiddle(
434-
() => response.end.apply(this, arguments as never),
431+
// Wraps end (inspired by:
432+
// https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-connect.ts#L75)
433+
const originalEnd = response.end;
434+
response.end = function (
435+
this: http.ServerResponse,
436+
..._args: ResponseEndArgs
437+
) {
438+
response.end = originalEnd;
439+
// Cannot pass args of type ResponseEndArgs,
440+
const returned = safeExecuteInTheMiddle(
441+
() => response.end.apply(this, arguments as never),
442+
error => {
443+
if (error) {
444+
utils.setSpanWithError(span, error);
445+
instrumentation._closeHttpSpan(span);
446+
throw error;
447+
}
448+
}
449+
);
450+
451+
const attributes = utils.getIncomingRequestAttributesOnResponse(
452+
request,
453+
response
454+
);
455+
456+
span
457+
.setAttributes(attributes)
458+
.setStatus(utils.parseResponseStatus(response.statusCode));
459+
460+
if (instrumentation._getConfig().applyCustomAttributesOnSpan) {
461+
safeExecuteInTheMiddle(
462+
() =>
463+
instrumentation._getConfig().applyCustomAttributesOnSpan!(
464+
span,
465+
request,
466+
response
467+
),
468+
() => {},
469+
true
470+
);
471+
}
472+
473+
instrumentation._closeHttpSpan(span);
474+
return returned;
475+
};
476+
477+
return safeExecuteInTheMiddle(
478+
() => original.apply(this, [event, ...args]),
435479
error => {
436480
if (error) {
437481
utils.setSpanWithError(span, error);
@@ -440,44 +484,8 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
440484
}
441485
}
442486
);
443-
444-
const attributes = utils.getIncomingRequestAttributesOnResponse(
445-
request,
446-
response
447-
);
448-
449-
span
450-
.setAttributes(attributes)
451-
.setStatus(utils.parseResponseStatus(response.statusCode));
452-
453-
if (instrumentation._getConfig().applyCustomAttributesOnSpan) {
454-
safeExecuteInTheMiddle(
455-
() =>
456-
instrumentation._getConfig().applyCustomAttributesOnSpan!(
457-
span,
458-
request,
459-
response
460-
),
461-
() => {},
462-
true
463-
);
464-
}
465-
466-
instrumentation._closeHttpSpan(span);
467-
return returned;
468-
};
469-
470-
return safeExecuteInTheMiddle(
471-
() => original.apply(this, [event, ...args]),
472-
error => {
473-
if (error) {
474-
utils.setSpanWithError(span, error);
475-
instrumentation._closeHttpSpan(span);
476-
throw error;
477-
}
478-
}
479-
);
480-
});
487+
}
488+
);
481489
};
482490
}
483491

packages/opentelemetry-instrumentation-http/src/utils.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SpanStatusCode,
1919
Span,
2020
SpanStatus,
21+
context,
2122
} from '@opentelemetry/api';
2223
import {
2324
NetTransportValues,
@@ -31,7 +32,7 @@ import {
3132
RequestOptions,
3233
ServerResponse,
3334
} from 'http';
34-
import { Socket } from 'net';
35+
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
3536
import * as url from 'url';
3637
import { AttributeNames } from './enums/AttributeNames';
3738
import { Err, IgnoreMatcher, ParsedRequestOptions } from './types';
@@ -465,25 +466,15 @@ export const getIncomingRequestAttributes = (
465466
* @param {(ServerResponse & { socket: Socket; })} response the response object
466467
*/
467468
export const getIncomingRequestAttributesOnResponse = (
468-
request: IncomingMessage & { __ot_middlewares?: string[] },
469-
response: ServerResponse & { socket: Socket }
469+
request: IncomingMessage,
470+
response: ServerResponse
470471
): SpanAttributes => {
471472
// take socket from the request,
472473
// since it may be detached from the response object in keep-alive mode
473474
const { socket } = request;
474475
const { statusCode, statusMessage } = response;
475476
const { localAddress, localPort, remoteAddress, remotePort } = socket;
476-
const { __ot_middlewares } = (request as unknown) as {
477-
[key: string]: unknown;
478-
};
479-
const route = Array.isArray(__ot_middlewares)
480-
? __ot_middlewares
481-
.filter(path => path !== '/')
482-
.map(path => {
483-
return path[0] === '/' ? path : '/' + path;
484-
})
485-
.join('')
486-
: undefined;
477+
const rpcMetadata = getRPCMetadata(context.active());
487478

488479
const attributes: SpanAttributes = {
489480
[SemanticAttributes.NET_HOST_IP]: localAddress,
@@ -494,8 +485,8 @@ export const getIncomingRequestAttributesOnResponse = (
494485
[AttributeNames.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(),
495486
};
496487

497-
if (route !== undefined) {
498-
attributes[SemanticAttributes.HTTP_ROUTE] = route;
488+
if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) {
489+
attributes[SemanticAttributes.HTTP_ROUTE] = rpcMetadata.route;
499490
}
500491
return attributes;
501492
};

packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { ContextManager } from '@opentelemetry/api';
4141
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
4242
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
4343
import { isWrapped } from '@opentelemetry/instrumentation';
44+
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
4445

4546
const instrumentation = new HttpInstrumentation();
4647
instrumentation.enable();
@@ -840,5 +841,32 @@ describe('HttpInstrumentation', () => {
840841
});
841842
});
842843
});
844+
describe('rpc metadata', () => {
845+
beforeEach(() => {
846+
memoryExporter.reset();
847+
instrumentation.setConfig({ requireParentforOutgoingSpans: true });
848+
instrumentation.enable();
849+
});
850+
851+
afterEach(() => {
852+
server.close();
853+
instrumentation.disable();
854+
});
855+
856+
it('should set rpc metadata for incoming http request', async () => {
857+
server = http.createServer((request, response) => {
858+
const rpcMemadata = getRPCMetadata(context.active());
859+
assert(typeof rpcMemadata !== 'undefined');
860+
assert(rpcMemadata.type === RPCType.HTTP);
861+
assert(rpcMemadata.span.setAttribute('key', 'value'));
862+
response.end('Test Server Response');
863+
});
864+
await new Promise<void>(resolve => server.listen(serverPort, resolve));
865+
await httpRequest.get(`${protocol}://${hostname}:${serverPort}`);
866+
const spans = memoryExporter.getFinishedSpans();
867+
assert.strictEqual(spans.length, 1);
868+
assert.strictEqual(spans[0].attributes.key, 'value');
869+
});
870+
});
843871
});
844872
});

packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ROOT_CONTEXT,
2020
SpanKind,
2121
TraceFlags,
22+
context,
2223
} from '@opentelemetry/api';
2324
import { BasicTracerProvider, Span } from '@opentelemetry/tracing';
2425
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
@@ -31,6 +32,8 @@ import * as url from 'url';
3132
import { IgnoreMatcher } from '../../src/types';
3233
import * as utils from '../../src/utils';
3334
import { AttributeNames } from '../../src/enums/AttributeNames';
35+
import { RPCType, setRPCMetadata } from '@opentelemetry/core';
36+
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
3437

3538
describe('Utility', () => {
3639
describe('parseResponseStatus()', () => {
@@ -285,16 +288,30 @@ describe('Utility', () => {
285288
});
286289

287290
describe('getIncomingRequestAttributesOnResponse()', () => {
288-
it('should correctly parse the middleware stack if present', () => {
291+
it('should correctly parse the middleware stack if present', done => {
292+
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
289293
const request = {
290-
__ot_middlewares: ['/test', '/toto', '/'],
291294
socket: {},
292-
} as IncomingMessage & { __ot_middlewares?: string[] };
293-
294-
const attributes = utils.getIncomingRequestAttributesOnResponse(request, {
295-
socket: {},
296-
} as ServerResponse & { socket: Socket });
297-
assert.deepEqual(attributes[SemanticAttributes.HTTP_ROUTE], '/test/toto');
295+
} as IncomingMessage;
296+
context.with(
297+
setRPCMetadata(context.active(), {
298+
type: RPCType.HTTP,
299+
route: '/user/:id',
300+
span: (null as unknown) as Span,
301+
}),
302+
() => {
303+
const attributes = utils.getIncomingRequestAttributesOnResponse(
304+
request,
305+
{} as ServerResponse
306+
);
307+
assert.deepStrictEqual(
308+
attributes[SemanticAttributes.HTTP_ROUTE],
309+
'/user/:id'
310+
);
311+
context.disable();
312+
return done();
313+
}
314+
);
298315
});
299316

300317
it('should succesfully process without middleware stack', () => {

0 commit comments

Comments
 (0)