Skip to content

Commit 87fff2e

Browse files
authored
fix(instrumentation-grpc): instrument @grpc/grpc-js Client methods (#3804)
1 parent 1a7488e commit 87fff2e

File tree

13 files changed

+1440
-266
lines changed

13 files changed

+1440
-266
lines changed

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ All notable changes to experimental packages in this project will be documented
8181

8282
* fix(sdk-node): use resource interface instead of concrete class [#3803](https://github.com/open-telemetry/opentelemetry-js/pull/3803) @blumamir
8383
* fix(sdk-logs): remove includeTraceContext configuration and use LogRecord context when available [#3817](https://github.com/open-telemetry/opentelemetry-js/pull/3817) @hectorhdzg
84+
* fix(instrumentation-grpc): instrument @grpc/grpc-js Client methods [#3804](https://github.com/open-telemetry/opentelemetry-js/pull/3804) @pichlermarc
8485

8586
## 0.39.1
8687

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
build
2+
test/proto

experimental/packages/opentelemetry-instrumentation-grpc/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"prepublishOnly": "npm run compile",
1010
"compile": "tsc --build",
1111
"clean": "tsc --build --clean",
12-
"test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts",
12+
"test": "npm run protos:generate && nyc ts-mocha -p tsconfig.json test/**/*.test.ts",
1313
"tdd": "npm run test -- --watch-extensions ts --watch",
1414
"lint": "eslint . --ext .ts",
1515
"lint:fix": "eslint . --ext .ts --fix",
@@ -18,7 +18,8 @@
1818
"watch": "tsc --build --watch",
1919
"precompile": "cross-var lerna run version --scope $npm_package_name --include-dependencies",
2020
"prewatch": "node ../../../scripts/version-update.js",
21-
"peer-api-check": "node ../../../scripts/peer-api-check.js"
21+
"peer-api-check": "node ../../../scripts/peer-api-check.js",
22+
"protos:generate": "cd test/fixtures && buf generate"
2223
},
2324
"keywords": [
2425
"opentelemetry",
@@ -45,6 +46,10 @@
4546
"access": "public"
4647
},
4748
"devDependencies": {
49+
"@bufbuild/buf": "1.21.0-1",
50+
"@protobuf-ts/grpc-transport": "2.9.0",
51+
"@protobuf-ts/runtime-rpc": "2.9.0",
52+
"@protobuf-ts/runtime": "2.9.0",
4853
"@grpc/grpc-js": "^1.7.1",
4954
"@grpc/proto-loader": "^0.7.3",
5055
"@opentelemetry/api": "1.4.1",

experimental/packages/opentelemetry-instrumentation-grpc/src/grpc-js/clientUtils.ts

Lines changed: 124 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ import type { GrpcJsInstrumentation } from './';
2222
import type { GrpcClientFunc, SendUnaryDataCallback } from './types';
2323
import type { metadataCaptureType } from '../internal-types';
2424

25-
import { SpanStatusCode, propagation, context } from '@opentelemetry/api';
25+
import { propagation, context } from '@opentelemetry/api';
2626
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
27-
import { CALL_SPAN_ENDED } from './serverUtils';
2827
import { AttributeNames } from '../enums/AttributeNames';
2928
import { GRPC_STATUS_CODE_OK } from '../status-code';
3029
import {
3130
_grpcStatusCodeToSpanStatus,
3231
_grpcStatusCodeToOpenTelemetryStatusCode,
3332
_methodIsIgnored,
3433
} from '../utils';
34+
import { errorMonitor } from 'events';
3535

3636
/**
3737
* Parse a package method list and return a list of methods to patch
@@ -63,6 +63,91 @@ export function getMethodsToWrap(
6363
return methodList;
6464
}
6565

66+
/**
67+
* Patches a callback so that the current span for this trace is also ended
68+
* when the callback is invoked.
69+
*/
70+
export function patchedCallback(
71+
span: Span,
72+
callback: SendUnaryDataCallback<ResponseType>
73+
) {
74+
const wrappedFn: SendUnaryDataCallback<ResponseType> = (
75+
err: grpcJs.ServiceError | null,
76+
res?: ResponseType
77+
) => {
78+
if (err) {
79+
if (err.code) {
80+
span.setStatus(_grpcStatusCodeToSpanStatus(err.code));
81+
span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, err.code);
82+
}
83+
span.setAttributes({
84+
[AttributeNames.GRPC_ERROR_NAME]: err.name,
85+
[AttributeNames.GRPC_ERROR_MESSAGE]: err.message,
86+
});
87+
} else {
88+
span.setAttribute(
89+
SemanticAttributes.RPC_GRPC_STATUS_CODE,
90+
GRPC_STATUS_CODE_OK
91+
);
92+
}
93+
94+
span.end();
95+
callback(err, res);
96+
};
97+
return context.bind(context.active(), wrappedFn);
98+
}
99+
100+
export function patchResponseMetadataEvent(
101+
span: Span,
102+
call: EventEmitter,
103+
metadataCapture: metadataCaptureType
104+
) {
105+
call.on('metadata', (responseMetadata: any) => {
106+
metadataCapture.client.captureResponseMetadata(span, responseMetadata);
107+
});
108+
}
109+
110+
export function patchResponseStreamEvents(span: Span, call: EventEmitter) {
111+
// Both error and status events can be emitted
112+
// the first one emitted set spanEnded to true
113+
let spanEnded = false;
114+
const endSpan = () => {
115+
if (!spanEnded) {
116+
span.end();
117+
spanEnded = true;
118+
}
119+
};
120+
context.bind(context.active(), call);
121+
call.on(errorMonitor, (err: ServiceError) => {
122+
if (spanEnded) {
123+
return;
124+
}
125+
126+
span.setStatus({
127+
code: _grpcStatusCodeToOpenTelemetryStatusCode(err.code),
128+
message: err.message,
129+
});
130+
span.setAttributes({
131+
[AttributeNames.GRPC_ERROR_NAME]: err.name,
132+
[AttributeNames.GRPC_ERROR_MESSAGE]: err.message,
133+
[SemanticAttributes.RPC_GRPC_STATUS_CODE]: err.code,
134+
});
135+
136+
endSpan();
137+
});
138+
139+
call.on('status', (status: SpanStatus) => {
140+
if (spanEnded) {
141+
return;
142+
}
143+
144+
span.setStatus(_grpcStatusCodeToSpanStatus(status.code));
145+
span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, status.code);
146+
147+
endSpan();
148+
});
149+
}
150+
66151
/**
67152
* Execute grpc client call. Apply completitionspan properties and end the
68153
* span on callback or receiving an emitted event.
@@ -71,44 +156,9 @@ export function makeGrpcClientRemoteCall(
71156
metadataCapture: metadataCaptureType,
72157
original: GrpcClientFunc,
73158
args: unknown[],
74-
metadata: Metadata,
75-
self: Client
159+
metadata: grpcJs.Metadata,
160+
self: grpcJs.Client
76161
): (span: Span) => EventEmitter {
77-
/**
78-
* Patches a callback so that the current span for this trace is also ended
79-
* when the callback is invoked.
80-
*/
81-
function patchedCallback(
82-
span: Span,
83-
callback: SendUnaryDataCallback<ResponseType>
84-
) {
85-
const wrappedFn: SendUnaryDataCallback<ResponseType> = (
86-
err: ServiceError | null,
87-
res?: ResponseType
88-
) => {
89-
if (err) {
90-
if (err.code) {
91-
span.setStatus(_grpcStatusCodeToSpanStatus(err.code));
92-
span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, err.code);
93-
}
94-
span.setAttributes({
95-
[AttributeNames.GRPC_ERROR_NAME]: err.name,
96-
[AttributeNames.GRPC_ERROR_MESSAGE]: err.message,
97-
});
98-
} else {
99-
span.setStatus({ code: SpanStatusCode.UNSET });
100-
span.setAttribute(
101-
SemanticAttributes.RPC_GRPC_STATUS_CODE,
102-
GRPC_STATUS_CODE_OK
103-
);
104-
}
105-
106-
span.end();
107-
callback(err, res);
108-
};
109-
return context.bind(context.active(), wrappedFn);
110-
}
111-
112162
return (span: Span) => {
113163
// if unary or clientStream
114164
if (!original.responseStream) {
@@ -132,90 +182,64 @@ export function makeGrpcClientRemoteCall(
132182

133183
// if server stream or bidi
134184
if (original.responseStream) {
135-
// Both error and status events can be emitted
136-
// the first one emitted set spanEnded to true
137-
let spanEnded = false;
138-
const endSpan = () => {
139-
if (!spanEnded) {
140-
span.end();
141-
spanEnded = true;
142-
}
143-
};
144-
context.bind(context.active(), call);
145-
call.on('error', (err: ServiceError) => {
146-
if (call[CALL_SPAN_ENDED]) {
147-
return;
148-
}
149-
call[CALL_SPAN_ENDED] = true;
150-
151-
span.setStatus({
152-
code: _grpcStatusCodeToOpenTelemetryStatusCode(err.code),
153-
message: err.message,
154-
});
155-
span.setAttributes({
156-
[AttributeNames.GRPC_ERROR_NAME]: err.name,
157-
[AttributeNames.GRPC_ERROR_MESSAGE]: err.message,
158-
[SemanticAttributes.RPC_GRPC_STATUS_CODE]: err.code,
159-
});
160-
161-
endSpan();
162-
});
163-
164-
call.on('status', (status: SpanStatus) => {
165-
if (call[CALL_SPAN_ENDED]) {
166-
return;
167-
}
168-
call[CALL_SPAN_ENDED] = true;
169-
170-
span.setStatus(_grpcStatusCodeToSpanStatus(status.code));
171-
span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, status.code);
172-
173-
endSpan();
174-
});
185+
patchResponseStreamEvents(span, call);
175186
}
176187
return call;
177188
};
178189
}
179190

180-
/**
181-
* Returns the metadata argument from user provided arguments (`args`)
182-
*/
183-
export function getMetadata(
184-
this: GrpcJsInstrumentation,
185-
original: GrpcClientFunc,
186-
grpcClient: typeof grpcJs,
187-
args: Array<unknown | Metadata>
188-
): Metadata {
189-
let metadata: Metadata;
190-
191+
export function getMetadataIndex(args: Array<unknown | Metadata>): number {
191192
// This finds an instance of Metadata among the arguments.
192193
// A possible issue that could occur is if the 'options' parameter from
193194
// the user contains an '_internal_repr' as well as a 'getMap' function,
194195
// but this is an extremely rare case.
195-
let metadataIndex = args.findIndex((arg: unknown | Metadata) => {
196+
return args.findIndex((arg: unknown | Metadata) => {
196197
return (
197198
arg &&
198199
typeof arg === 'object' &&
199200
(arg as Metadata)['internalRepr'] && // changed from _internal_repr in grpc --> @grpc/grpc-js https://github.com/grpc/grpc-node/blob/95289edcaf36979cccf12797cc27335da8d01f03/packages/grpc-js/src/metadata.ts#L88
200201
typeof (arg as Metadata).getMap === 'function'
201202
);
202203
});
204+
}
205+
206+
/**
207+
* Returns the metadata argument from user provided arguments (`args`)
208+
* If no metadata is provided in `args`: adds empty metadata to `args` and returns that empty metadata
209+
*/
210+
export function extractMetadataOrSplice(
211+
grpcLib: typeof grpcJs,
212+
args: Array<unknown | grpcJs.Metadata>,
213+
spliceIndex: number
214+
) {
215+
let metadata: grpcJs.Metadata;
216+
const metadataIndex = getMetadataIndex(args);
203217
if (metadataIndex === -1) {
204-
metadata = new grpcClient.Metadata();
205-
if (!original.requestStream) {
206-
// unary or server stream
207-
metadataIndex = 1;
208-
} else {
209-
// client stream or bidi
210-
metadataIndex = 0;
211-
}
212-
args.splice(metadataIndex, 0, metadata);
218+
// Create metadata if it does not exist
219+
metadata = new grpcLib.Metadata();
220+
args.splice(spliceIndex, 0, metadata);
213221
} else {
214222
metadata = args[metadataIndex] as Metadata;
215223
}
216224
return metadata;
217225
}
218226

227+
/**
228+
* Returns the metadata argument from user provided arguments (`args`)
229+
* Adds empty metadata to arguments if the default is used.
230+
*/
231+
export function extractMetadataOrSpliceDefault(
232+
grpcClient: typeof grpcJs,
233+
original: GrpcClientFunc,
234+
args: Array<unknown | grpcJs.Metadata>
235+
): grpcJs.Metadata {
236+
return extractMetadataOrSplice(
237+
grpcClient,
238+
args,
239+
original.requestStream ? 0 : 1
240+
);
241+
}
242+
219243
/**
220244
* Inject opentelemetry trace context into `metadata` for use by another
221245
* grpc receiver

0 commit comments

Comments
 (0)