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

feat: merge grpc-js into grpc instrumentation #1657 #1806

Merged
merged 10 commits into from
Feb 11, 2021
6 changes: 3 additions & 3 deletions packages/opentelemetry-instrumentation-grpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![devDependencies][devDependencies-image]][devDependencies-url]
[![Apache License][license-image]][license-image]

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

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

## Usage

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

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

Expand All @@ -42,7 +42,7 @@ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();
```

See [examples/grpc](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/grpc) for a short example.
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.

### gRPC Instrumentation Options

Expand Down
3 changes: 2 additions & 1 deletion packages/opentelemetry-instrumentation-grpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"dependencies": {
"@opentelemetry/api": "^0.16.0",
"@opentelemetry/instrumentation": "^0.16.0",
"@opentelemetry/semantic-conventions": "^0.16.0"
"@opentelemetry/semantic-conventions": "^0.16.0",
"@opentelemetry/api-metrics": "^0.16.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { GrpcJsInstrumentation } from './';
import type { GrpcClientFunc, SendUnaryDataCallback } from './types';
import {
SpanKind,
Span,
SpanStatusCode,
SpanStatus,
propagation,
context,
} from '@opentelemetry/api';
import { RpcAttribute } from '@opentelemetry/semantic-conventions';
import type * as grpcJs from '@grpc/grpc-js';
import {
_grpcStatusCodeToSpanStatus,
_grpcStatusCodeToOpenTelemetryStatusCode,
_methodIsIgnored,
} from '../utils';
import { CALL_SPAN_ENDED } from './serverUtils';
import { EventEmitter } from 'events';

/**
* Parse a package method list and return a list of methods to patch
* with both possible casings e.g. "TestMethod" & "testMethod"
*/
export function getMethodsToWrap(
this: GrpcJsInstrumentation,
client: typeof grpcJs.Client,
methods: { [key: string]: { originalName?: string } }
): string[] {
const methodList: string[] = [];

// For a method defined in .proto as "UnaryMethod"
Object.entries(methods).forEach(([name, { originalName }]) => {
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved
if (!_methodIsIgnored(name, this._config.ignoreGrpcMethods)) {
methodList.push(name); // adds camel case method name: "unaryMethod"
if (
originalName &&
// eslint-disable-next-line no-prototype-builtins
client.prototype.hasOwnProperty(originalName) &&
name !== originalName // do not add duplicates
) {
// adds original method name: "UnaryMethod",
methodList.push(originalName);
}
}
});

return methodList;
}

/**
* Execute grpc client call. Apply completitionspan properties and end the
* span on callback or receiving an emitted event.
*/
export function makeGrpcClientRemoteCall(
original: GrpcClientFunc,
args: unknown[],
metadata: grpcJs.Metadata,
self: grpcJs.Client
): (span: Span) => EventEmitter {
/**
* Patches a callback so that the current span for this trace is also ended
* when the callback is invoked.
*/
function patchedCallback(
span: Span,
callback: SendUnaryDataCallback<ResponseType>
) {
const wrappedFn: SendUnaryDataCallback<ResponseType> = (
err: grpcJs.ServiceError | null,
res: any
) => {
if (err) {
if (err.code) {
span.setStatus(_grpcStatusCodeToSpanStatus(err.code));
span.setAttribute(RpcAttribute.GRPC_STATUS_CODE, err.code.toString());
}
span.setAttributes({
[RpcAttribute.GRPC_ERROR_NAME]: err.name,
[RpcAttribute.GRPC_ERROR_MESSAGE]: err.message,
});
} else {
span.setStatus({ code: SpanStatusCode.UNSET });
span.setAttribute(
RpcAttribute.GRPC_STATUS_CODE,
SpanStatusCode.UNSET.toString()
);
}

span.end();
callback(err, res);
};
return context.bind(wrappedFn);
}

return (span: Span) => {
// if unary or clientStream
if (!original.responseStream) {
const callbackFuncIndex = args.findIndex(arg => {
return typeof arg === 'function';
});
if (callbackFuncIndex !== -1) {
args[callbackFuncIndex] = patchedCallback(
span,
args[callbackFuncIndex] as SendUnaryDataCallback<ResponseType>
);
}
}

span.setAttributes({
[RpcAttribute.GRPC_METHOD]: original.path,
[RpcAttribute.GRPC_KIND]: SpanKind.CLIENT,
});

setSpanContext(metadata);
const call = original.apply(self, args);

// if server stream or bidi
if (original.responseStream) {
// Both error and status events can be emitted
// the first one emitted set spanEnded to true
let spanEnded = false;
const endSpan = () => {
if (!spanEnded) {
span.end();
spanEnded = true;
}
};
context.bind(call);
call.on('error', (err: grpcJs.ServiceError) => {
if (call[CALL_SPAN_ENDED]) {
return;
}
call[CALL_SPAN_ENDED] = true;

span.setStatus({
code: _grpcStatusCodeToOpenTelemetryStatusCode(err.code),
message: err.message,
});
span.setAttributes({
[RpcAttribute.GRPC_ERROR_NAME]: err.name,
[RpcAttribute.GRPC_ERROR_MESSAGE]: err.message,
});

endSpan();
});

call.on('status', (status: SpanStatus) => {
if (call[CALL_SPAN_ENDED]) {
return;
}
call[CALL_SPAN_ENDED] = true;

span.setStatus(_grpcStatusCodeToSpanStatus(status.code));

endSpan();
});
}
return call;
};
}

/**
* Returns the metadata argument from user provided arguments (`args`)
*/
export function getMetadata(
this: GrpcJsInstrumentation,
grpcClient: typeof grpcJs,
original: GrpcClientFunc,
args: Array<unknown | grpcJs.Metadata>
): grpcJs.Metadata {
let metadata: grpcJs.Metadata;

// This finds an instance of Metadata among the arguments.
// A possible issue that could occur is if the 'options' parameter from
// the user contains an '_internal_repr' as well as a 'getMap' function,
// but this is an extremely rare case.
let metadataIndex = args.findIndex((arg: unknown | grpcJs.Metadata) => {
return (
arg &&
typeof arg === 'object' &&
(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
typeof (arg as grpcJs.Metadata).getMap === 'function'
);
});
if (metadataIndex === -1) {
metadata = new grpcClient.Metadata();
if (!original.requestStream) {
// unary or server stream
metadataIndex = 1;
} else {
// client stream or bidi
metadataIndex = 0;
}
args.splice(metadataIndex, 0, metadata);
} else {
metadata = args[metadataIndex] as grpcJs.Metadata;
}
return metadata;
}

/**
* Inject opentelemetry trace context into `metadata` for use by another
* grpc receiver
* @param metadata
*/
export function setSpanContext(metadata: grpcJs.Metadata): void {
propagation.inject(context.active(), metadata, {
set: (metadata, k, v) => metadata.set(k, v as grpcJs.MetadataValue),
});
}
Loading