Skip to content

Commit 7d31077

Browse files
markwolffmayurkale22
authored andcommitted
fix(grpc): fix client/server span propagation (open-telemetry#325)
* fix(grpc): fix client/server span propagation * fix(test): uncomment grpc patch * fix: linting, add missing unwrap * docs(grpc): add supported versions to readme
1 parent fa21bef commit 7d31077

File tree

6 files changed

+82
-43
lines changed

6 files changed

+82
-43
lines changed

packages/opentelemetry-plugin-grpc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![devDependencies][devDependencies-image]][devDependencies-url]
55
[![Apache License][license-image]][license-image]
66

7-
This module provides automatic instrumentation for [`grpc`](https://grpc.github.io/grpc/node/).
7+
This module provides automatic instrumentation for [`grpc`](https://grpc.github.io/grpc/node/). Currently, version [`1.x`](https://www.npmjs.com/package/grpc?activeTab=versions) of the Node.js gRPC library is supported.
88

99
For automatic instrumentation see the
1010
[@opentelemetry/node-sdk](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node-sdk) package.

packages/opentelemetry-plugin-grpc/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@
4141
"@types/mocha": "^5.2.7",
4242
"@types/node": "^12.6.9",
4343
"@types/shimmer": "^1.0.1",
44+
"@types/sinon": "^7.0.13",
4445
"codecov": "^3.5.0",
4546
"grpc": "^1.23.3",
4647
"gts": "^1.1.0",
4748
"mocha": "^6.2.0",
4849
"nyc": "^14.1.1",
4950
"node-pre-gyp": "^0.12.0",
5051
"rimraf": "^3.0.0",
52+
"sinon": "^7.5.0",
5153
"tslint-microsoft-contrib": "^6.2.0",
5254
"tslint-consistent-codestyle": "^1.15.1",
5355
"ts-mocha": "^6.0.0",

packages/opentelemetry-plugin-grpc/src/grpc.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ServerCallWithMeta,
3232
SendUnaryDataCallback,
3333
GrpcClientFunc,
34+
GrpcInternalClientTypes,
3435
} from './types';
3536
import {
3637
findIndex,
@@ -39,17 +40,18 @@ import {
3940
} from './utils';
4041

4142
import * as events from 'events';
42-
import * as grpcModule from 'grpc';
43+
import * as grpcTypes from 'grpc';
4344
import * as shimmer from 'shimmer';
4445
import * as path from 'path';
4546

4647
/** The metadata key under which span context is stored as a binary value. */
4748
export const GRPC_TRACE_KEY = 'grpc-trace-bin';
4849

49-
let grpcClientModule: object;
50+
let grpcClientModule: GrpcInternalClientTypes;
5051

5152
export class GrpcPlugin extends BasePlugin<grpc> {
5253
static readonly component = 'grpc';
54+
readonly supportedVersions = ['^1.23.3'];
5355

5456
protected _config!: GrpcPluginOptions;
5557

@@ -64,7 +66,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
6466
};
6567
protected readonly _basedir = basedir;
6668

67-
protected patch(): typeof grpcModule {
69+
protected patch(): typeof grpcTypes {
6870
this._logger.debug(
6971
'applying patch to %s@%s',
7072
this.moduleName,
@@ -80,12 +82,24 @@ export class GrpcPlugin extends BasePlugin<grpc> {
8082
);
8183
}
8284

85+
// Wrap the externally exported client constructor
86+
if (this._moduleExports.makeGenericClientConstructor) {
87+
shimmer.wrap(
88+
this._moduleExports,
89+
'makeGenericClientConstructor',
90+
this._patchClient()
91+
);
92+
}
93+
8394
if (this._internalFilesExports['client']) {
84-
grpcClientModule = this._internalFilesExports['client'] as object;
95+
grpcClientModule = this._internalFilesExports[
96+
'client'
97+
] as GrpcInternalClientTypes;
8598

99+
// Wrap the internally used client constructor
86100
shimmer.wrap(
87101
grpcClientModule,
88-
'makeClientConstructor' as never,
102+
'makeClientConstructor',
89103
this._patchClient()
90104
);
91105
}
@@ -103,12 +117,16 @@ export class GrpcPlugin extends BasePlugin<grpc> {
103117
shimmer.unwrap(this._moduleExports.Server.prototype, 'register');
104118
}
105119

120+
if (this._moduleExports.makeGenericClientConstructor) {
121+
shimmer.unwrap(this._moduleExports, 'makeGenericClientConstructor');
122+
}
123+
106124
if (grpcClientModule) {
107-
shimmer.unwrap(grpcClientModule, 'makeClientConstructor' as never);
125+
shimmer.unwrap(grpcClientModule, 'makeClientConstructor');
108126
}
109127
}
110128

111-
private _getSpanContext(metadata: grpcModule.Metadata): SpanContext | null {
129+
private _getSpanContext(metadata: grpcTypes.Metadata): SpanContext | null {
112130
const metadataValue = metadata.getMap()[GRPC_TRACE_KEY] as Buffer;
113131
// Entry doesn't exist
114132
if (!metadataValue) {
@@ -118,7 +136,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
118136
}
119137

120138
private _setSpanContext(
121-
metadata: grpcModule.Metadata,
139+
metadata: grpcTypes.Metadata,
122140
spanContext: SpanContext
123141
): void {
124142
const serializedSpanContext = this._tracer
@@ -129,17 +147,17 @@ export class GrpcPlugin extends BasePlugin<grpc> {
129147
}
130148

131149
private _patchServer() {
132-
return (originalRegister: typeof grpcModule.Server.prototype.register) => {
150+
return (originalRegister: typeof grpcTypes.Server.prototype.register) => {
133151
const plugin = this;
134152
plugin._logger.debug('patched gRPC server');
135153

136154
return function register<RequestType, ResponseType>(
137155
// tslint:disable-next-line:no-any
138-
this: grpcModule.Server & { handlers: any },
156+
this: grpcTypes.Server & { handlers: any },
139157
name: string,
140-
handler: grpcModule.handleCall<RequestType, ResponseType>,
141-
serialize: grpcModule.serialize<RequestType>,
142-
deserialize: grpcModule.deserialize<RequestType>,
158+
handler: grpcTypes.handleCall<RequestType, ResponseType>,
159+
serialize: grpcTypes.serialize<RequestType>,
160+
deserialize: grpcTypes.deserialize<RequestType>,
143161
type: string
144162
) {
145163
// tslint:disable-next-line:no-any
@@ -149,7 +167,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
149167
shimmer.wrap(
150168
handlerSet,
151169
'func',
152-
(originalFunc: grpcModule.handleCall<RequestType, ResponseType>) => {
170+
(originalFunc: grpcTypes.handleCall<RequestType, ResponseType>) => {
153171
return function func(
154172
this: typeof handlerSet,
155173
call: ServerCallWithMeta,
@@ -216,16 +234,16 @@ export class GrpcPlugin extends BasePlugin<grpc> {
216234
call: ServerCallWithMeta,
217235
callback: SendUnaryDataCallback,
218236
original:
219-
| grpcModule.handleCall<RequestType, ResponseType>
220-
| grpcModule.ClientReadableStream<RequestType>,
237+
| grpcTypes.handleCall<RequestType, ResponseType>
238+
| grpcTypes.ClientReadableStream<RequestType>,
221239
self: {}
222240
) {
223241
function patchedCallback(
224-
err: grpcModule.ServiceError,
242+
err: grpcTypes.ServiceError,
225243
// tslint:disable-next-line:no-any
226244
value: any,
227-
trailer: grpcModule.Metadata,
228-
flags: grpcModule.writeFlags
245+
trailer: grpcTypes.Metadata,
246+
flags: grpcTypes.writeFlags
229247
) {
230248
if (err) {
231249
if (err.code) {
@@ -246,7 +264,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
246264
span.setStatus({ code: CanonicalCode.OK });
247265
span.setAttribute(
248266
AttributeNames.GRPC_STATUS_CODE,
249-
grpcModule.status.OK.toString()
267+
plugin._moduleExports.status.OK.toString()
250268
);
251269
}
252270
span.addEvent('received');
@@ -264,7 +282,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
264282
plugin: GrpcPlugin,
265283
span: Span,
266284
call: ServerCallWithMeta,
267-
original: grpcModule.handleCall<RequestType, ResponseType>,
285+
original: grpcTypes.handleCall<RequestType, ResponseType>,
268286
self: {}
269287
) {
270288
let spanEnded = false;
@@ -290,7 +308,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
290308
}
291309
});
292310

293-
call.on('error', (err: grpcModule.ServiceError) => {
311+
call.on('error', (err: grpcTypes.ServiceError) => {
294312
span.addEvent('finished with error');
295313
span.setAttributes({
296314
[AttributeNames.GRPC_ERROR_NAME]: err.name,
@@ -305,15 +323,13 @@ export class GrpcPlugin extends BasePlugin<grpc> {
305323

306324
private _patchClient() {
307325
const plugin = this;
308-
return (
309-
original: typeof grpcModule.makeGenericClientConstructor
310-
): never => {
326+
return (original: typeof grpcTypes.makeGenericClientConstructor): never => {
311327
plugin._logger.debug('patching client');
312328
return function makeClientConstructor<ImplementationType>(
313-
this: typeof grpcModule.Client,
314-
methods: grpcModule.ServiceDefinition<ImplementationType>,
329+
this: typeof grpcTypes.Client,
330+
methods: grpcTypes.ServiceDefinition<ImplementationType>,
315331
serviceName: string,
316-
options: grpcModule.GenericClientOptions
332+
options: grpcTypes.GenericClientOptions
317333
) {
318334
// tslint:disable-next-line:no-any
319335
const client = original.apply(this, arguments as any);
@@ -332,7 +348,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
332348
const plugin = this;
333349
return (original: GrpcClientFunc) => {
334350
plugin._logger.debug('patch all client methods');
335-
return function clientMethodTrace(this: grpcModule.Client) {
351+
return function clientMethodTrace(this: grpcTypes.Client) {
336352
const name = `grpc.${original.path.replace('/', '')}`;
337353
const args = Array.prototype.slice.call(arguments);
338354
const currentSpan = plugin._tracer.getCurrentSpan();
@@ -356,7 +372,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
356372
original: GrpcClientFunc,
357373
// tslint:disable-next-line:no-any
358374
args: any[],
359-
self: grpcModule.Client,
375+
self: grpcTypes.Client,
360376
plugin: GrpcPlugin
361377
) {
362378
/**
@@ -366,10 +382,10 @@ export class GrpcPlugin extends BasePlugin<grpc> {
366382
function patchedCallback(
367383
span: Span,
368384
callback: SendUnaryDataCallback,
369-
metadata: grpcModule.Metadata
385+
metadata: grpcTypes.Metadata
370386
) {
371387
// tslint:disable-next-line:no-any
372-
const wrappedFn = (err: grpcModule.ServiceError, res: any) => {
388+
const wrappedFn = (err: grpcTypes.ServiceError, res: any) => {
373389
if (err) {
374390
if (err.code) {
375391
span.setStatus(_grpcStatusCodeToSpanStatus(err.code));
@@ -386,7 +402,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
386402
span.setStatus({ code: CanonicalCode.OK });
387403
span.setAttribute(
388404
AttributeNames.GRPC_STATUS_CODE,
389-
grpcModule.status.OK.toString()
405+
plugin._moduleExports.status.OK.toString()
390406
);
391407
}
392408

@@ -439,7 +455,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
439455
plugin._tracer.bind(call);
440456
((call as unknown) as events.EventEmitter).on(
441457
'error',
442-
(err: grpcModule.ServiceError) => {
458+
(err: grpcTypes.ServiceError) => {
443459
span.setStatus({
444460
code: _grpcStatusCodeToCanonicalCode(err.code),
445461
message: err.message,
@@ -472,8 +488,8 @@ export class GrpcPlugin extends BasePlugin<grpc> {
472488
original: GrpcClientFunc,
473489
// tslint:disable-next-line:no-any
474490
args: any[]
475-
): grpcModule.Metadata {
476-
let metadata: grpcModule.Metadata;
491+
): grpcTypes.Metadata {
492+
let metadata: grpcTypes.Metadata;
477493

478494
// This finds an instance of Metadata among the arguments.
479495
// A possible issue that could occur is if the 'options' parameter from
@@ -489,7 +505,7 @@ export class GrpcPlugin extends BasePlugin<grpc> {
489505
);
490506
});
491507
if (metadataIndex === -1) {
492-
metadata = new grpcModule.Metadata();
508+
metadata = new this._moduleExports.Metadata();
493509
if (!original.requestStream) {
494510
// unary or server stream
495511
if (args.length === 0) {
@@ -516,5 +532,4 @@ export class GrpcPlugin extends BasePlugin<grpc> {
516532

517533
const basedir = path.dirname(require.resolve('grpc'));
518534
const version = require(path.join(basedir, 'package.json')).version;
519-
const plugin = new GrpcPlugin(GrpcPlugin.component, version);
520-
export { plugin };
535+
export const plugin = new GrpcPlugin(GrpcPlugin.component, version);

packages/opentelemetry-plugin-grpc/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export type GrpcClientFunc = typeof Function & {
5353
responseStream: boolean;
5454
};
5555

56+
export type GrpcInternalClientTypes = {
57+
makeClientConstructor: typeof grpcModule.makeGenericClientConstructor;
58+
};
59+
5660
// TODO: Delete if moving internal file loaders to BasePlugin
5761
/**
5862
* Maps a name (key) representing a internal file module and its exports

packages/opentelemetry-plugin-grpc/src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { CanonicalCode, Status } from '@opentelemetry/types';
18-
import * as grpcModule from 'grpc'; // For types only
18+
import * as grpcTypes from 'grpc'; // For types only
1919

2020
// Equivalent to lodash _.findIndex
2121
export const findIndex: <T>(args: T[], fn: (arg: T) => boolean) => number = (
@@ -37,7 +37,7 @@ export const findIndex: <T>(args: T[], fn: (arg: T) => boolean) => number = (
3737
* @param status
3838
*/
3939
export const _grpcStatusCodeToCanonicalCode = (
40-
status?: grpcModule.status
40+
status?: grpcTypes.status
4141
): CanonicalCode => {
4242
if (status !== 0 && !status) {
4343
return CanonicalCode.UNKNOWN;

packages/opentelemetry-plugin-grpc/test/grpc.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { NoopLogger } from '@opentelemetry/core';
17+
import { NoopLogger, NoopTracer } from '@opentelemetry/core';
1818
import {
1919
InMemorySpanExporter,
2020
SimpleSpanProcessor,
@@ -29,6 +29,7 @@ import { SendUnaryDataCallback } from '../src/types';
2929
import * as assert from 'assert';
3030
import * as semver from 'semver';
3131
import * as grpc from 'grpc';
32+
import * as sinon from 'sinon';
3233

3334
const PROTO_PATH = __dirname + '/fixtures/grpc-test.proto';
3435
const memoryExporter = new InMemorySpanExporter();
@@ -286,6 +287,23 @@ describe('GrpcPlugin', () => {
286287
assert.deepStrictEqual('grpc', plugin.moduleName);
287288
});
288289

290+
describe('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => {
291+
const clientPatchStub = sinon.stub(
292+
plugin,
293+
'_getPatchedClientMethods' as never
294+
);
295+
after(() => {
296+
clientPatchStub.restore();
297+
plugin.disable();
298+
});
299+
300+
it('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => {
301+
plugin.enable(grpc, new NoopTracer(), new NoopLogger());
302+
(plugin['_moduleExports'] as any).makeGenericClientConstructor({});
303+
assert.strictEqual(clientPatchStub.callCount, 1);
304+
});
305+
});
306+
289307
const requestList: TestRequestResponse[] = [{ num: 100 }, { num: 50 }];
290308
const resultSum = {
291309
num: requestList.reduce((sum, x) => {

0 commit comments

Comments
 (0)