Skip to content

Commit 11d9b19

Browse files
authored
feat: merge grpc-js into grpc instrumentation #1657 (#1806)
1 parent 1d682c2 commit 11d9b19

File tree

18 files changed

+1678
-625
lines changed

18 files changed

+1678
-625
lines changed

packages/opentelemetry-instrumentation-grpc/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![devDependencies][devDependencies-image]][devDependencies-url]
66
[![Apache License][license-image]][license-image]
77

8-
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.
8+
This module provides automatic instrumentation for [`grpc`](https://grpc.github.io/grpc/node/) and [`@grpc/grpc-js`](https://grpc.io/blog/grpc-js-1.0/). Currently, version [`1.x`](https://www.npmjs.com/package/grpc?activeTab=versions) of `grpc` and version [`1.x`](https://www.npmjs.com/package/@grpc/grpc-js?activeTab=versions) of `@grpc/grpc-js` is supported.
99

1010
For automatic instrumentation see the
1111
[@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-node) package.
@@ -18,7 +18,7 @@ npm install --save @opentelemetry/instrumentation-grpc
1818

1919
## Usage
2020

21-
OpenTelemetry gRPC Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems when working with [gRPC](https://www.npmjs.com/package/grpc).
21+
OpenTelemetry gRPC Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems when working with [gRPC](https://www.npmjs.com/package/grpc) or ([grpc-js](https://www.npmjs.com/package/@grpc/grpc-js)).
2222

2323
To load a specific instrumentation (**gRPC** in this case), specify it in the Node Tracer's configuration.
2424

@@ -42,7 +42,7 @@ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
4242
provider.register();
4343
```
4444

45-
See [examples/grpc](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/grpc) for a short example.
45+
See [examples/grpc](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/grpc) or [examples/grpc-js](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/grpc-js) for examples.
4646

4747
### gRPC Instrumentation Options
4848

packages/opentelemetry-instrumentation-grpc/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"dependencies": {
7070
"@opentelemetry/api": "^0.16.0",
7171
"@opentelemetry/instrumentation": "^0.16.0",
72-
"@opentelemetry/semantic-conventions": "^0.16.0"
72+
"@opentelemetry/semantic-conventions": "^0.16.0",
73+
"@opentelemetry/api-metrics": "^0.16.0"
7374
}
7475
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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 { GrpcJsInstrumentation } from './';
18+
import type { GrpcClientFunc, SendUnaryDataCallback } from './types';
19+
import {
20+
SpanKind,
21+
Span,
22+
SpanStatusCode,
23+
SpanStatus,
24+
propagation,
25+
context,
26+
} from '@opentelemetry/api';
27+
import { RpcAttribute } from '@opentelemetry/semantic-conventions';
28+
import type * as grpcJs from '@grpc/grpc-js';
29+
import {
30+
_grpcStatusCodeToSpanStatus,
31+
_grpcStatusCodeToOpenTelemetryStatusCode,
32+
_methodIsIgnored,
33+
} from '../utils';
34+
import { CALL_SPAN_ENDED } from './serverUtils';
35+
import { EventEmitter } from 'events';
36+
37+
/**
38+
* Parse a package method list and return a list of methods to patch
39+
* with both possible casings e.g. "TestMethod" & "testMethod"
40+
*/
41+
export function getMethodsToWrap(
42+
this: GrpcJsInstrumentation,
43+
client: typeof grpcJs.Client,
44+
methods: { [key: string]: { originalName?: string } }
45+
): string[] {
46+
const methodList: string[] = [];
47+
48+
// For a method defined in .proto as "UnaryMethod"
49+
Object.entries(methods).forEach(([name, { originalName }]) => {
50+
if (!_methodIsIgnored(name, this._config.ignoreGrpcMethods)) {
51+
methodList.push(name); // adds camel case method name: "unaryMethod"
52+
if (
53+
originalName &&
54+
// eslint-disable-next-line no-prototype-builtins
55+
client.prototype.hasOwnProperty(originalName) &&
56+
name !== originalName // do not add duplicates
57+
) {
58+
// adds original method name: "UnaryMethod",
59+
methodList.push(originalName);
60+
}
61+
}
62+
});
63+
64+
return methodList;
65+
}
66+
67+
/**
68+
* Execute grpc client call. Apply completitionspan properties and end the
69+
* span on callback or receiving an emitted event.
70+
*/
71+
export function makeGrpcClientRemoteCall(
72+
original: GrpcClientFunc,
73+
args: unknown[],
74+
metadata: grpcJs.Metadata,
75+
self: grpcJs.Client
76+
): (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: grpcJs.ServiceError | null,
87+
res: any
88+
) => {
89+
if (err) {
90+
if (err.code) {
91+
span.setStatus(_grpcStatusCodeToSpanStatus(err.code));
92+
span.setAttribute(RpcAttribute.GRPC_STATUS_CODE, err.code.toString());
93+
}
94+
span.setAttributes({
95+
[RpcAttribute.GRPC_ERROR_NAME]: err.name,
96+
[RpcAttribute.GRPC_ERROR_MESSAGE]: err.message,
97+
});
98+
} else {
99+
span.setStatus({ code: SpanStatusCode.UNSET });
100+
span.setAttribute(
101+
RpcAttribute.GRPC_STATUS_CODE,
102+
SpanStatusCode.UNSET.toString()
103+
);
104+
}
105+
106+
span.end();
107+
callback(err, res);
108+
};
109+
return context.bind(wrappedFn);
110+
}
111+
112+
return (span: Span) => {
113+
// if unary or clientStream
114+
if (!original.responseStream) {
115+
const callbackFuncIndex = args.findIndex(arg => {
116+
return typeof arg === 'function';
117+
});
118+
if (callbackFuncIndex !== -1) {
119+
args[callbackFuncIndex] = patchedCallback(
120+
span,
121+
args[callbackFuncIndex] as SendUnaryDataCallback<ResponseType>
122+
);
123+
}
124+
}
125+
126+
span.setAttributes({
127+
[RpcAttribute.GRPC_METHOD]: original.path,
128+
[RpcAttribute.GRPC_KIND]: SpanKind.CLIENT,
129+
});
130+
131+
setSpanContext(metadata);
132+
const call = original.apply(self, args);
133+
134+
// if server stream or bidi
135+
if (original.responseStream) {
136+
// Both error and status events can be emitted
137+
// the first one emitted set spanEnded to true
138+
let spanEnded = false;
139+
const endSpan = () => {
140+
if (!spanEnded) {
141+
span.end();
142+
spanEnded = true;
143+
}
144+
};
145+
context.bind(call);
146+
call.on('error', (err: grpcJs.ServiceError) => {
147+
if (call[CALL_SPAN_ENDED]) {
148+
return;
149+
}
150+
call[CALL_SPAN_ENDED] = true;
151+
152+
span.setStatus({
153+
code: _grpcStatusCodeToOpenTelemetryStatusCode(err.code),
154+
message: err.message,
155+
});
156+
span.setAttributes({
157+
[RpcAttribute.GRPC_ERROR_NAME]: err.name,
158+
[RpcAttribute.GRPC_ERROR_MESSAGE]: err.message,
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+
172+
endSpan();
173+
});
174+
}
175+
return call;
176+
};
177+
}
178+
179+
/**
180+
* Returns the metadata argument from user provided arguments (`args`)
181+
*/
182+
export function getMetadata(
183+
this: GrpcJsInstrumentation,
184+
grpcClient: typeof grpcJs,
185+
original: GrpcClientFunc,
186+
args: Array<unknown | grpcJs.Metadata>
187+
): grpcJs.Metadata {
188+
let metadata: grpcJs.Metadata;
189+
190+
// This finds an instance of Metadata among the arguments.
191+
// A possible issue that could occur is if the 'options' parameter from
192+
// the user contains an '_internal_repr' as well as a 'getMap' function,
193+
// but this is an extremely rare case.
194+
let metadataIndex = args.findIndex((arg: unknown | grpcJs.Metadata) => {
195+
return (
196+
arg &&
197+
typeof arg === 'object' &&
198+
(arg as grpcJs.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
199+
typeof (arg as grpcJs.Metadata).getMap === 'function'
200+
);
201+
});
202+
if (metadataIndex === -1) {
203+
metadata = new grpcClient.Metadata();
204+
if (!original.requestStream) {
205+
// unary or server stream
206+
metadataIndex = 1;
207+
} else {
208+
// client stream or bidi
209+
metadataIndex = 0;
210+
}
211+
args.splice(metadataIndex, 0, metadata);
212+
} else {
213+
metadata = args[metadataIndex] as grpcJs.Metadata;
214+
}
215+
return metadata;
216+
}
217+
218+
/**
219+
* Inject opentelemetry trace context into `metadata` for use by another
220+
* grpc receiver
221+
* @param metadata
222+
*/
223+
export function setSpanContext(metadata: grpcJs.Metadata): void {
224+
propagation.inject(context.active(), metadata, {
225+
set: (metadata, k, v) => metadata.set(k, v as grpcJs.MetadataValue),
226+
});
227+
}

0 commit comments

Comments
 (0)