diff --git a/README.md b/README.md index 2072fda1d4e..2a0659be6d4 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User ##### Core - [@opentelemetry/plugin-grpc][otel-plugin-grpc] +- [@opentelemetry/plugin-grpc-js][otel-plugin-grpc-js] - [@opentelemetry/plugin-http][otel-plugin-http] - [@opentelemetry/plugin-https][otel-plugin-https] @@ -242,6 +243,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [otel-plugin-fetch]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-fetch [otel-plugin-grpc]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-grpc +[otel-plugin-grpc-js]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-grpc-js [otel-plugin-http]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http [otel-plugin-https]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-https [otel-plugin-xml-http-request]: https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-xml-http-request diff --git a/examples/.eslintrc b/examples/.eslintrc index 5feabb97f57..6000f93619e 100644 --- a/examples/.eslintrc +++ b/examples/.eslintrc @@ -12,5 +12,6 @@ "no-console": "off", "import/no-unresolved": "off", "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - } + }, + "ignorePatterns": "**/*_pb.js" } diff --git a/examples/grpc-js/README.md b/examples/grpc-js/README.md new file mode 100644 index 00000000000..00012cbbf51 --- /dev/null +++ b/examples/grpc-js/README.md @@ -0,0 +1,71 @@ +# Overview + +OpenTelemetry gRPC Instrumentation allows the user to automatically collect trace data and export them to the backend of choice (we can use Zipkin or Jaeger for this example), to give observability to distributed systems. + +## Installation + +```sh +# from this directory +npm install +``` + +Setup [Zipkin Tracing](https://zipkin.io/pages/quickstart.html) +or +Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) + +## Run the Application + +### Zipkin + +- Run the server + + ```sh + # from this directory + npm run zipkin:server + ``` + +- Run the client + + ```sh + # from this directory + npm run zipkin:client + ``` + +#### Zipkin UI + +`zipkin:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`). +Go to Zipkin with your browser (e.g + +

+ +### Jaeger + +- Run the server + + ```sh + # from this directory + npm run jaeger:server + ``` + +- Run the client + + ```sh + # from this directory + npm run jaeger:client + ``` + +#### Jaeger UI + +`jaeger:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`). +Go to Jaeger with your browser (e.g + +

+ +## Useful links + +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry for Node.js, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/grpc-js/client.js b/examples/grpc-js/client.js new file mode 100644 index 00000000000..ee4090260ed --- /dev/null +++ b/examples/grpc-js/client.js @@ -0,0 +1,46 @@ +'use strict'; + +const tracer = require('./tracer')('example-grpc-client'); +// eslint-disable-next-line import/order +const grpc = require('@grpc/grpc-js'); +const messages = require('./helloworld_pb'); +const services = require('./helloworld_grpc_pb'); + +const PORT = 50051; + +/** Send a test gRPC Hello Request to the Greeter Service (server.js) */ +function main() { + // span corresponds to outgoing requests. Here, we have manually created + // the span, which is created to track work that happens outside of the + // request lifecycle entirely. + const span = tracer.startSpan('client.js:main()'); + tracer.withSpan(span, () => { + console.log('Client traceId ', span.context().traceId); + const client = new services.GreeterClient( + `localhost:${PORT}`, + grpc.credentials.createInsecure(), + ); + const request = new messages.HelloRequest(); + let user; + if (process.argv.length >= 3) { + // eslint-disable-next-line prefer-destructuring + user = process.argv[2]; + } else { + user = 'world'; + } + request.setName(user); + client.sayHello(request, (err, response) => { + span.end(); + if (err) throw err; + console.log('Greeting:', response.getMessage()); + }); + }); + + // The process must live for at least the interval past any traces that + // must be exported, or some risk being lost if they are recorded after the + // last export. + console.log('Sleeping 5 seconds before shutdown to ensure all records are flushed.'); + setTimeout(() => { console.log('Completed.'); }, 5000); +} + +main(); diff --git a/examples/grpc-js/helloworld_grpc_pb.js b/examples/grpc-js/helloworld_grpc_pb.js new file mode 100644 index 00000000000..1e39a0f46ab --- /dev/null +++ b/examples/grpc-js/helloworld_grpc_pb.js @@ -0,0 +1,64 @@ +// GENERATED CODE -- DO NOT EDIT! + +// Original file comments: +// Copyright 2015 gRPC 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 +// +// http://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. +// + +'use strict'; + +const grpc = require('grpc'); +const helloworld_pb = require('./helloworld_pb.js'); + +function serialize_HelloReply(arg) { + if (!(arg instanceof helloworld_pb.HelloReply)) { + throw new Error('Expected argument of type HelloReply'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_HelloReply(buffer_arg) { + return helloworld_pb.HelloReply.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_HelloRequest(arg) { + if (!(arg instanceof helloworld_pb.HelloRequest)) { + throw new Error('Expected argument of type HelloRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_HelloRequest(buffer_arg) { + return helloworld_pb.HelloRequest.deserializeBinary( + new Uint8Array(buffer_arg), + ); +} + +// The greeting service definition. +const GreeterService = (exports.GreeterService = { + // Sends a greeting + sayHello: { + path: '/helloworld.Greeter/SayHello', + requestStream: false, + responseStream: false, + requestType: helloworld_pb.HelloRequest, + responseType: helloworld_pb.HelloReply, + requestSerialize: serialize_HelloRequest, + requestDeserialize: deserialize_HelloRequest, + responseSerialize: serialize_HelloReply, + responseDeserialize: deserialize_HelloReply, + }, +}); + +exports.GreeterClient = grpc.makeGenericClientConstructor(GreeterService); diff --git a/examples/grpc-js/helloworld_pb.js b/examples/grpc-js/helloworld_pb.js new file mode 100644 index 00000000000..066acd68e8f --- /dev/null +++ b/examples/grpc-js/helloworld_pb.js @@ -0,0 +1,332 @@ +/** + * @fileoverview + * @enhanceable + * @public + */ +// GENERATED CODE -- DO NOT EDIT! + +const jspb = require('google-protobuf'); + +const goog = jspb; +const global = Function('return this')(); + +goog.exportSymbol('proto.helloworld.HelloReply', null, global); +goog.exportSymbol('proto.helloworld.HelloRequest', null, global); + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.helloworld.HelloRequest = function (opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.helloworld.HelloRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.helloworld.HelloRequest.displayName = 'proto.helloworld.HelloRequest'; +} + +if (jspb.Message.GENERATE_TO_OBJECT) { + /** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ + proto.helloworld.HelloRequest.prototype.toObject = function ( + opt_includeInstance, + ) { + return proto.helloworld.HelloRequest.toObject(opt_includeInstance, this); + }; + + /** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.helloworld.HelloRequest} msg The msg instance to transform. + * @return {!Object} + */ + proto.helloworld.HelloRequest.toObject = function (includeInstance, msg) { + let f; + + const obj = { + name: msg.getName(), + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; + }; +} + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.helloworld.HelloRequest} + */ +proto.helloworld.HelloRequest.deserializeBinary = function (bytes) { + const reader = new jspb.BinaryReader(bytes); + const msg = new proto.helloworld.HelloRequest(); + return proto.helloworld.HelloRequest.deserializeBinaryFromReader(msg, reader); +}; + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.helloworld.HelloRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.helloworld.HelloRequest} + */ +proto.helloworld.HelloRequest.deserializeBinaryFromReader = function ( + msg, + reader, +) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + const field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + +/** + * Class method variant: serializes the given message to binary data + * (in protobuf wire format), writing to the given BinaryWriter. + * @param {!proto.helloworld.HelloRequest} message + * @param {!jspb.BinaryWriter} writer + */ +proto.helloworld.HelloRequest.serializeBinaryToWriter = function ( + message, + writer, +) { + message.serializeBinaryToWriter(writer); +}; + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.helloworld.HelloRequest.prototype.serializeBinary = function () { + const writer = new jspb.BinaryWriter(); + this.serializeBinaryToWriter(writer); + return writer.getResultBuffer(); +}; + +/** + * Serializes the message to binary data (in protobuf wire format), + * writing to the given BinaryWriter. + * @param {!jspb.BinaryWriter} writer + */ +proto.helloworld.HelloRequest.prototype.serializeBinaryToWriter = function ( + writer, +) { + let f; + f = this.getName(); + if (f.length > 0) { + writer.writeString(1, f); + } +}; + +/** + * Creates a deep clone of this proto. No data is shared with the original. + * @return {!proto.helloworld.HelloRequest} The clone. + */ +proto.helloworld.HelloRequest.prototype.cloneMessage = function () { + return /** @type {!proto.helloworld.HelloRequest} */ (jspb.Message.cloneMessage( + this, + )); +}; + +/** + * optional string name = 1; + * @return {string} + */ +proto.helloworld.HelloRequest.prototype.getName = function () { + return /** @type {string} */ (jspb.Message.getFieldProto3(this, 1, '')); +}; + +/** @param {string} value */ +proto.helloworld.HelloRequest.prototype.setName = function (value) { + jspb.Message.setField(this, 1, value); +}; + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.helloworld.HelloReply = function (opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.helloworld.HelloReply, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.helloworld.HelloReply.displayName = 'proto.helloworld.HelloReply'; +} + +if (jspb.Message.GENERATE_TO_OBJECT) { + /** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ + proto.helloworld.HelloReply.prototype.toObject = function ( + opt_includeInstance, + ) { + return proto.helloworld.HelloReply.toObject(opt_includeInstance, this); + }; + + /** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.helloworld.HelloReply} msg The msg instance to transform. + * @return {!Object} + */ + proto.helloworld.HelloReply.toObject = function (includeInstance, msg) { + let f; + + const obj = { + message: msg.getMessage(), + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; + }; +} + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.helloworld.HelloReply} + */ +proto.helloworld.HelloReply.deserializeBinary = function (bytes) { + const reader = new jspb.BinaryReader(bytes); + const msg = new proto.helloworld.HelloReply(); + return proto.helloworld.HelloReply.deserializeBinaryFromReader(msg, reader); +}; + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.helloworld.HelloReply} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.helloworld.HelloReply} + */ +proto.helloworld.HelloReply.deserializeBinaryFromReader = function ( + msg, + reader, +) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + const field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setMessage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + +/** + * Class method variant: serializes the given message to binary data + * (in protobuf wire format), writing to the given BinaryWriter. + * @param {!proto.helloworld.HelloReply} message + * @param {!jspb.BinaryWriter} writer + */ +proto.helloworld.HelloReply.serializeBinaryToWriter = function ( + message, + writer, +) { + message.serializeBinaryToWriter(writer); +}; + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.helloworld.HelloReply.prototype.serializeBinary = function () { + const writer = new jspb.BinaryWriter(); + this.serializeBinaryToWriter(writer); + return writer.getResultBuffer(); +}; + +/** + * Serializes the message to binary data (in protobuf wire format), + * writing to the given BinaryWriter. + * @param {!jspb.BinaryWriter} writer + */ +proto.helloworld.HelloReply.prototype.serializeBinaryToWriter = function ( + writer, +) { + let f; + f = this.getMessage(); + if (f.length > 0) { + writer.writeString(1, f); + } +}; + +/** + * Creates a deep clone of this proto. No data is shared with the original. + * @return {!proto.helloworld.HelloReply} The clone. + */ +proto.helloworld.HelloReply.prototype.cloneMessage = function () { + return /** @type {!proto.helloworld.HelloReply} */ (jspb.Message.cloneMessage( + this, + )); +}; + +/** + * optional string message = 1; + * @return {string} + */ +proto.helloworld.HelloReply.prototype.getMessage = function () { + return /** @type {string} */ (jspb.Message.getFieldProto3(this, 1, '')); +}; + +/** @param {string} value */ +proto.helloworld.HelloReply.prototype.setMessage = function (value) { + jspb.Message.setField(this, 1, value); +}; + +goog.object.extend(exports, proto.helloworld); diff --git a/examples/grpc-js/images/jaeger.png b/examples/grpc-js/images/jaeger.png new file mode 100644 index 00000000000..20eead3ba33 Binary files /dev/null and b/examples/grpc-js/images/jaeger.png differ diff --git a/examples/grpc-js/images/zipkin.png b/examples/grpc-js/images/zipkin.png new file mode 100644 index 00000000000..d1dcd125a78 Binary files /dev/null and b/examples/grpc-js/images/zipkin.png differ diff --git a/examples/grpc-js/package.json b/examples/grpc-js/package.json new file mode 100644 index 00000000000..14ab4981fdb --- /dev/null +++ b/examples/grpc-js/package.json @@ -0,0 +1,44 @@ +{ + "name": "grpc-js-example", + "private": true, + "version": "0.8.3", + "description": "Example of @grpc/grpc-js integration with OpenTelemetry", + "main": "index.js", + "scripts": { + "zipkin:server": "cross-env EXPORTER=zipkin node ./server.js", + "zipkin:client": "cross-env EXPORTER=zipkin node ./client.js", + "jaeger:server": "cross-env EXPORTER=jaeger node ./server.js", + "jaeger:client": "cross-env EXPORTER=jaeger node ./client.js" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "grpc", + "tracing" + ], + "engines": { + "node": ">=8" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@grpc/grpc-js": "^1.0.5", + "@opentelemetry/api": "^0.8.3", + "@opentelemetry/exporter-jaeger": "^0.8.3", + "@opentelemetry/exporter-zipkin": "^0.8.3", + "@opentelemetry/node": "^0.8.3", + "@opentelemetry/plugin-grpc-js": "^0.8.3", + "@opentelemetry/tracing": "^0.8.3", + "google-protobuf": "^3.9.2" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": { + "cross-env": "^6.0.0" + } +} diff --git a/examples/grpc-js/server.js b/examples/grpc-js/server.js new file mode 100644 index 00000000000..1b9d5331728 --- /dev/null +++ b/examples/grpc-js/server.js @@ -0,0 +1,38 @@ +'use strict'; + +const tracer = require('./tracer')(('example-grpc-server')); +// eslint-disable-next-line import/order +const grpc = require('grpc'); + +const messages = require('./helloworld_pb'); +const services = require('./helloworld_grpc_pb'); + +const PORT = 50051; + +/** Starts a gRPC server that receives requests on sample server port. */ +function startServer() { + // Creates a server + const server = new grpc.Server(); + server.addService(services.GreeterService, { sayHello }); + server.bind(`0.0.0.0:${PORT}`, grpc.ServerCredentials.createInsecure()); + console.log(`binding server on 0.0.0.0:${PORT}`); + server.start(); +} + +function sayHello(call, callback) { + const currentSpan = tracer.getCurrentSpan(); + // display traceid in the terminal + console.log(`traceid: ${currentSpan.context().traceId}`); + const span = tracer.startSpan('server.js:sayHello()', { + parent: currentSpan, + kind: 1, // server + attributes: { key: 'value' }, + }); + span.addEvent(`invoking sayHello() to ${call.request.getName()}`); + const reply = new messages.HelloReply(); + reply.setMessage(`Hello ${call.request.getName()}`); + callback(null, reply); + span.end(); +} + +startServer(); diff --git a/examples/grpc-js/tracer.js b/examples/grpc-js/tracer.js new file mode 100644 index 00000000000..e5c1d2871a9 --- /dev/null +++ b/examples/grpc-js/tracer.js @@ -0,0 +1,38 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/api'); +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); +const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); + +const EXPORTER = process.env.EXPORTER || ''; + +module.exports = (serviceName) => { + const provider = new NodeTracerProvider({ + plugins: { + '@grpc/grpc-js': { + enabled: true, + path: '@opentelemetry/plugin-grpc-js', + }, + }, + }); + + let exporter; + if (EXPORTER.toLowerCase().startsWith('z')) { + exporter = new ZipkinExporter({ + serviceName, + }); + } else { + exporter = new JaegerExporter({ + serviceName, + }); + } + + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register(); + + return opentelemetry.trace.getTracer('grpc-js-example'); +}; diff --git a/examples/grpc/helloworld_grpc_pb.js b/examples/grpc/helloworld_grpc_pb.js index c32727f8daa..1e39a0f46ab 100644 --- a/examples/grpc/helloworld_grpc_pb.js +++ b/examples/grpc/helloworld_grpc_pb.js @@ -1,14 +1,3 @@ -/* eslint-disable no-multi-assign */ -/* eslint-disable prefer-const */ -/* eslint-disable no-var */ -/* eslint-disable vars-on-top */ -/* eslint-disable no-unused-vars */ -/* eslint-disable func-names */ -/* eslint-disable camelcase */ -/* eslint-disable no-undef */ -/* eslint-disable no-new-func */ -/* eslint-disable strict */ - // GENERATED CODE -- DO NOT EDIT! // Original file comments: diff --git a/examples/grpc/helloworld_pb.js b/examples/grpc/helloworld_pb.js index 8247c5842cc..066acd68e8f 100644 --- a/examples/grpc/helloworld_pb.js +++ b/examples/grpc/helloworld_pb.js @@ -1,13 +1,3 @@ -/* eslint-disable prefer-const */ -/* eslint-disable no-var */ -/* eslint-disable vars-on-top */ -/* eslint-disable no-unused-vars */ -/* eslint-disable func-names */ -/* eslint-disable camelcase */ -/* eslint-disable no-undef */ -/* eslint-disable no-new-func */ -/* eslint-disable strict */ - /** * @fileoverview * @enhanceable diff --git a/package.json b/package.json index 98f32fbae11..335b5a6714b 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,10 @@ "author": "OpenTelemetry Authors", "license": "Apache-2.0", "devDependencies": { - "@commitlint/cli": "9.0.1", - "@commitlint/config-conventional": "9.0.1", - "@typescript-eslint/eslint-plugin": "3.6.0", - "@typescript-eslint/parser": "3.6.0", + "@commitlint/cli": "9.1.1", + "@commitlint/config-conventional": "9.1.1", + "@typescript-eslint/eslint-plugin": "3.6.1", + "@typescript-eslint/parser": "3.6.1", "beautify-benchmark": "0.2.4", "benchmark": "2.1.4", "eslint": "7.4.0", @@ -56,7 +56,7 @@ "lerna": "3.22.1", "lerna-changelog": "1.0.1", "markdownlint-cli": "0.23.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "husky": { "hooks": { diff --git a/packages/opentelemetry-api/package.json b/packages/opentelemetry-api/package.json index 4f991fbfc86..cec27f995e2 100644 --- a/packages/opentelemetry-api/package.json +++ b/packages/opentelemetry-api/package.json @@ -55,10 +55,10 @@ "@opentelemetry/context-base": "^0.9.0" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/webpack-env": "1.15.2", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -70,10 +70,10 @@ "linkinator": "2.1.1", "mocha": "7.2.0", "nyc": "15.1.0", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "typedoc": "0.17.8", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0" } } diff --git a/packages/opentelemetry-context-async-hooks/package.json b/packages/opentelemetry-context-async-hooks/package.json index 3c7c74a6f11..506decad1e6 100644 --- a/packages/opentelemetry-context-async-hooks/package.json +++ b/packages/opentelemetry-context-async-hooks/package.json @@ -42,17 +42,17 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/shimmer": "1.0.1", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/context-base": "^0.9.0" diff --git a/packages/opentelemetry-context-base/package.json b/packages/opentelemetry-context-base/package.json index 3b1070ce570..8affde53955 100644 --- a/packages/opentelemetry-context-base/package.json +++ b/packages/opentelemetry-context-base/package.json @@ -44,15 +44,15 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", - "codecov": "3.7.0", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" } } diff --git a/packages/opentelemetry-context-zone-peer-dep/package.json b/packages/opentelemetry-context-zone-peer-dep/package.json index 290380c97a5..2bd0fac7c07 100644 --- a/packages/opentelemetry-context-zone-peer-dep/package.json +++ b/packages/opentelemetry-context-zone-peer-dep/package.json @@ -42,14 +42,14 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@babel/core": "7.10.5", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "@types/zone.js": "0.5.12", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -62,10 +62,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "zone.js": "0.10.3" diff --git a/packages/opentelemetry-context-zone/package.json b/packages/opentelemetry-context-zone/package.json index d99f93d8b8e..e16c47b891e 100644 --- a/packages/opentelemetry-context-zone/package.json +++ b/packages/opentelemetry-context-zone/package.json @@ -39,13 +39,13 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@babel/core": "7.10.5", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "karma": "5.1.0", "karma-chrome-launcher": "3.1.0", @@ -56,10 +56,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "webpack-merge": "5.0.9" diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index 1ec14aff253..aa8a1566478 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -52,12 +52,12 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/semver": "7.3.1", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -70,10 +70,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0" }, "dependencies": { diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 4e7c910471f..690110351d6 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -97,7 +97,7 @@ export class HttpCorrelationContext implements HttpTextPropagator { if (keyPair) { correlationContext[keyPair.key] = { value: keyPair.value }; } else { - // Fail to parse, return context without shim + // Fail to parse, return context without correlation return context; } } diff --git a/packages/opentelemetry-core/src/platform/browser/environment.ts b/packages/opentelemetry-core/src/platform/browser/environment.ts index 54a2a6c5711..e1671f40c00 100644 --- a/packages/opentelemetry-core/src/platform/browser/environment.ts +++ b/packages/opentelemetry-core/src/platform/browser/environment.ts @@ -24,7 +24,7 @@ import { /** * Gets the environment variables */ -export function getEnv(): ENVIRONMENT { +export function getEnv(): Required { const _window = window as typeof window & ENVIRONMENT_MAP; const globalEnv = parseEnvironment(_window); return Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv); diff --git a/packages/opentelemetry-core/src/platform/node/environment.ts b/packages/opentelemetry-core/src/platform/node/environment.ts index f3595cd9af1..db35828e6ba 100644 --- a/packages/opentelemetry-core/src/platform/node/environment.ts +++ b/packages/opentelemetry-core/src/platform/node/environment.ts @@ -24,7 +24,7 @@ import { /** * Gets the environment variables */ -export function getEnv(): ENVIRONMENT { +export function getEnv(): Required { const processEnv = parseEnvironment(process.env as ENVIRONMENT_MAP); return Object.assign({}, DEFAULT_ENVIRONMENT, processEnv); } diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index be56f892c01..9ada4030cd9 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -35,7 +35,7 @@ const ENVIRONMENT_NUMBERS: Partial[] = [ /** * Default environment variables */ -export const DEFAULT_ENVIRONMENT: ENVIRONMENT = { +export const DEFAULT_ENVIRONMENT: Required = { OTEL_NO_PATCH_MODULES: '', OTEL_LOG_LEVEL: LogLevel.ERROR, OTEL_SAMPLING_PROBABILITY: 1, diff --git a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts index 0f73780dd51..8d294d83629 100644 --- a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts +++ b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts @@ -164,7 +164,6 @@ describe('HttpCorrelationContext', () => { const extractedSpanContext = getCorrelationContext( httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) ); - console.log(extractedSpanContext); assert.deepStrictEqual(extractedSpanContext, undefined, testCase); }); }); diff --git a/packages/opentelemetry-exporter-collector/package.json b/packages/opentelemetry-exporter-collector/package.json index 3069d187f1c..1e2dcb4ac35 100644 --- a/packages/opentelemetry-exporter-collector/package.json +++ b/packages/opentelemetry-exporter-collector/package.json @@ -54,13 +54,13 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@babel/core": "7.10.5", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "cpx": "1.5.0", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", @@ -74,10 +74,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "webpack-merge": "5.0.9" diff --git a/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts b/packages/opentelemetry-exporter-collector/src/CollectorExporterBase.ts similarity index 72% rename from packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts rename to packages/opentelemetry-exporter-collector/src/CollectorExporterBase.ts index f1508ec0beb..1c6ff54f54e 100644 --- a/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts +++ b/packages/opentelemetry-exporter-collector/src/CollectorExporterBase.ts @@ -16,33 +16,32 @@ import { Attributes, Logger } from '@opentelemetry/api'; import { ExportResult, NoopLogger } from '@opentelemetry/core'; -import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; import { CollectorExporterError, CollectorExporterConfigBase, ExportServiceError, } from './types'; -const DEFAULT_SERVICE_NAME = 'collector-exporter'; - /** - * Collector Trace Exporter abstract base class + * Collector Exporter abstract base class */ -export abstract class CollectorTraceExporterBase< - T extends CollectorExporterConfigBase -> implements SpanExporter { +export abstract class CollectorExporterBase< + T extends CollectorExporterConfigBase, + ExportItem, + ServiceRequest +> { public readonly serviceName: string; public readonly url: string; public readonly logger: Logger; public readonly hostname: string | undefined; public readonly attributes?: Attributes; - private _isShutdown: boolean = false; + protected _isShutdown: boolean = false; /** * @param config */ constructor(config: T = {} as T) { - this.serviceName = config.serviceName || DEFAULT_SERVICE_NAME; + this.serviceName = this.getDefaultServiceName(config); this.url = this.getDefaultUrl(config); if (typeof config.hostname === 'string') { this.hostname = config.hostname; @@ -59,20 +58,17 @@ export abstract class CollectorTraceExporterBase< } /** - * Export spans. - * @param spans + * Export items. + * @param items * @param resultCallback */ - export( - spans: ReadableSpan[], - resultCallback: (result: ExportResult) => void - ) { + export(items: ExportItem[], resultCallback: (result: ExportResult) => void) { if (this._isShutdown) { resultCallback(ExportResult.FAILED_NOT_RETRYABLE); return; } - this._exportSpans(spans) + this._export(items) .then(() => { resultCallback(ExportResult.SUCCESS); }) @@ -88,13 +84,11 @@ export abstract class CollectorTraceExporterBase< }); } - private _exportSpans(spans: ReadableSpan[]): Promise { + private _export(items: ExportItem[]): Promise { return new Promise((resolve, reject) => { try { - this.logger.debug('spans to be sent', spans); - // Send spans to [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} - // it will use the appropriate transport layer automatically depends on platform - this.sendSpans(spans, resolve, reject); + this.logger.debug('items to be sent', items); + this.send(items, resolve, reject); } catch (e) { reject(e); } @@ -118,10 +112,12 @@ export abstract class CollectorTraceExporterBase< abstract onShutdown(): void; abstract onInit(config: T): void; - abstract sendSpans( - spans: ReadableSpan[], + abstract send( + items: ExportItem[], onSuccess: () => void, onError: (error: CollectorExporterError) => void ): void; abstract getDefaultUrl(config: T): string; + abstract getDefaultServiceName(config: T): string; + abstract convert(objects: ExportItem[]): ServiceRequest; } diff --git a/packages/opentelemetry-exporter-collector/src/CollectorMetricExporterBase.ts b/packages/opentelemetry-exporter-collector/src/CollectorMetricExporterBase.ts deleted file mode 100644 index 7580e5f8a73..00000000000 --- a/packages/opentelemetry-exporter-collector/src/CollectorMetricExporterBase.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 { MetricExporter, MetricRecord } from '@opentelemetry/metrics'; -import { Attributes, Logger } from '@opentelemetry/api'; -import { CollectorExporterConfigBase } from './types'; -import { NoopLogger, ExportResult } from '@opentelemetry/core'; -import * as collectorTypes from './types'; - -const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; - -/** - * Collector Metric Exporter abstract base class - */ -export abstract class CollectorMetricExporterBase< - T extends CollectorExporterConfigBase -> implements MetricExporter { - public readonly serviceName: string; - public readonly url: string; - public readonly logger: Logger; - public readonly hostname: string | undefined; - public readonly attributes?: Attributes; - protected readonly _startTime = new Date().getTime() * 1000000; - private _isShutdown: boolean = false; - - /** - * @param config - */ - constructor(config: T = {} as T) { - this.logger = config.logger || new NoopLogger(); - this.serviceName = config.serviceName || DEFAULT_SERVICE_NAME; - this.url = this.getDefaultUrl(config.url); - this.attributes = config.attributes; - if (typeof config.hostname === 'string') { - this.hostname = config.hostname; - } - this.onInit(); - } - - /** - * Export metrics - * @param metrics - * @param resultCallback - */ - export( - metrics: MetricRecord[], - resultCallback: (result: ExportResult) => void - ) { - if (this._isShutdown) { - resultCallback(ExportResult.FAILED_NOT_RETRYABLE); - return; - } - - this._exportMetrics(metrics) - .then(() => { - resultCallback(ExportResult.SUCCESS); - }) - .catch((error: collectorTypes.ExportServiceError) => { - if (error.message) { - this.logger.error(error.message); - } - if (error.code && error.code < 500) { - resultCallback(ExportResult.FAILED_NOT_RETRYABLE); - } else { - resultCallback(ExportResult.FAILED_RETRYABLE); - } - }); - } - - private _exportMetrics(metrics: MetricRecord[]): Promise { - return new Promise((resolve, reject) => { - try { - this.logger.debug('metrics to be sent', metrics); - // Send metrics to [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} - // it will use the appropriate transport layer automatically depends on platform - this.sendMetrics(metrics, resolve, reject); - } catch (e) { - reject(e); - } - }); - } - - /** - * Shutdown the exporter. - */ - shutdown(): void { - if (this._isShutdown) { - this.logger.debug('shutdown already started'); - return; - } - this._isShutdown = true; - this.logger.debug('shutdown started'); - - // platform dependent - this.onShutdown(); - } - - abstract getDefaultUrl(url: string | undefined): string; - abstract onInit(): void; - abstract onShutdown(): void; - abstract sendMetrics( - metrics: MetricRecord[], - onSuccess: () => void, - onError: (error: collectorTypes.CollectorExporterError) => void - ): void; -} diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts index 5d89ebce9ea..6984c91cf73 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { CollectorTraceExporterBase } from '../../CollectorTraceExporterBase'; -import { ReadableSpan } from '@opentelemetry/tracing'; +import { CollectorExporterBase } from '../../CollectorExporterBase'; +import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; import { toCollectorExportTraceServiceRequest } from '../../transform'; import { CollectorExporterConfigBrowser } from './types'; import * as collectorTypes from '../../types'; @@ -23,13 +23,18 @@ import { sendWithBeacon, sendWithXhr } from './util'; import { parseHeaders } from '../../util'; const DEFAULT_COLLECTOR_URL = 'http://localhost:55680/v1/trace'; +const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; /** * Collector Trace Exporter for Web */ -export class CollectorTraceExporter extends CollectorTraceExporterBase< - CollectorExporterConfigBrowser -> { +export class CollectorTraceExporter + extends CollectorExporterBase< + CollectorExporterConfigBrowser, + ReadableSpan, + collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest + > + implements SpanExporter { DEFAULT_HEADERS: Record = { [collectorTypes.OT_REQUEST_HEADER]: '1', }; @@ -59,15 +64,22 @@ export class CollectorTraceExporter extends CollectorTraceExporterBase< return config.url || DEFAULT_COLLECTOR_URL; } - sendSpans( + getDefaultServiceName(config: CollectorExporterConfigBrowser): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return toCollectorExportTraceServiceRequest(spans, this); + } + + send( spans: ReadableSpan[], onSuccess: () => void, onError: (error: collectorTypes.CollectorExporterError) => void ) { - const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( - spans, - this - ); + const exportTraceServiceRequest = this.convert(spans); const body = JSON.stringify(exportTraceServiceRequest); if (this._useXHR) { diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts new file mode 100644 index 00000000000..2e64bc39a4e --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts @@ -0,0 +1,115 @@ +/* + * 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 { CollectorExporterBase } from '../../CollectorExporterBase'; +import { CollectorExporterConfigNode, GRPCQueueItem } from './types'; +import { ServiceClient } from './types'; +import * as grpc from 'grpc'; +import { CollectorProtocolNode } from '../../enums'; +import * as collectorTypes from '../../types'; +import { parseHeaders } from '../../util'; +import { sendWithJson, initWithJson } from './utilWithJson'; +import { sendUsingGrpc, initWithGrpc } from './utilWithGrpc'; + +const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; + +/** + * Collector Metric Exporter abstract base class + */ +export abstract class CollectorExporterNodeBase< + ExportItem, + ServiceRequest +> extends CollectorExporterBase< + CollectorExporterConfigNode, + ExportItem, + ServiceRequest +> { + DEFAULT_HEADERS: Record = { + [collectorTypes.OT_REQUEST_HEADER]: '1', + }; + grpcQueue: GRPCQueueItem[]; + serviceClient?: ServiceClient = undefined; + credentials: grpc.ChannelCredentials; + metadata?: grpc.Metadata; + headers: Record; + protected readonly _protocol: CollectorProtocolNode; + + constructor(config: CollectorExporterConfigNode = {}) { + super(config); + this._protocol = + typeof config.protocolNode !== 'undefined' + ? config.protocolNode + : CollectorProtocolNode.GRPC; + if (this._protocol === CollectorProtocolNode.HTTP_JSON) { + this.logger.debug('CollectorExporter - using json over http'); + if (config.metadata) { + this.logger.warn('Metadata cannot be set when using json'); + } + } else { + this.logger.debug('CollectorExporter - using grpc'); + if (config.headers) { + this.logger.warn('Headers cannot be set when using grpc'); + } + } + this.grpcQueue = []; + this.credentials = config.credentials || grpc.credentials.createInsecure(); + this.metadata = config.metadata; + this.headers = + parseHeaders(config.headers, this.logger) || this.DEFAULT_HEADERS; + } + + onInit(config: CollectorExporterConfigNode): void { + this._isShutdown = false; + if (config.protocolNode === CollectorProtocolNode.HTTP_JSON) { + initWithJson(this, config); + } else { + initWithGrpc(this); + } + } + + send( + objects: ExportItem[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void + ): void { + if (this._isShutdown) { + this.logger.debug('Shutdown already started. Cannot send objects'); + return; + } + if (this._protocol === CollectorProtocolNode.HTTP_JSON) { + sendWithJson(this, objects, onSuccess, onError); + } else { + sendUsingGrpc(this, objects, onSuccess, onError); + } + } + + onShutdown(): void { + this._isShutdown = true; + if (this.serviceClient) { + this.serviceClient.close(); + } + } + + getDefaultServiceName(config: CollectorExporterConfigNode): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + abstract getServiceProtoPath(): string; + abstract getServiceClient( + packageObject: any, + serverAddress: string + ): ServiceClient; +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts new file mode 100644 index 00000000000..27f160a8948 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts @@ -0,0 +1,71 @@ +/* + * 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 { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; +import * as collectorTypes from '../../types'; +import { CollectorExporterConfigNode, ServiceClient } from './types'; +import { CollectorProtocolNode } from '../../enums'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; +import { toCollectorExportMetricServiceRequest } from '../../transformMetrics'; +import { DEFAULT_COLLECTOR_URL_GRPC } from './utilWithGrpc'; +const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; +const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55680/v1/metrics'; + +/** + * Collector Metric Exporter for Node + */ +export class CollectorMetricExporter + extends CollectorExporterNodeBase< + MetricRecord, + collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest + > + implements MetricExporter { + protected readonly _startTime = new Date().getTime() * 1000000; + + convert( + metrics: MetricRecord[] + ): collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { + return toCollectorExportMetricServiceRequest( + metrics, + this._startTime, + this + ); + } + + getDefaultUrl(config: CollectorExporterConfigNode): string { + if (!config.url) { + return config.protocolNode === CollectorProtocolNode.HTTP_JSON + ? DEFAULT_COLLECTOR_URL_JSON + : DEFAULT_COLLECTOR_URL_GRPC; + } + return config.url; + } + + getDefaultServiceName(config: CollectorExporterConfigNode): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClient(packageObject: any, serverAddress: string): ServiceClient { + return new packageObject.opentelemetry.proto.collector.metrics.v1.MetricsService( + serverAddress, + this.credentials + ); + } + + getServiceProtoPath(): string { + return 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; + } +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts index 4cdaf9e41de..bbdf9fe5525 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts @@ -14,102 +14,30 @@ * limitations under the License. */ -import { ReadableSpan } from '@opentelemetry/tracing'; -import * as grpc from 'grpc'; -import { CollectorTraceExporterBase } from '../../CollectorTraceExporterBase'; +import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; import * as collectorTypes from '../../types'; - import { CollectorProtocolNode } from '../../enums'; -import { parseHeaders } from '../../util'; -import { - GRPCSpanQueueItem, - ServiceClient, - CollectorExporterConfigNode, -} from './types'; +import { CollectorExporterConfigNode, ServiceClient } from './types'; +import { toCollectorExportTraceServiceRequest } from '../../transform'; +import { DEFAULT_COLLECTOR_URL_GRPC } from './utilWithGrpc'; -import { - DEFAULT_COLLECTOR_URL_GRPC, - onInitWithGrpc, - sendSpansUsingGrpc, -} from './utilWithGrpc'; -import { - DEFAULT_COLLECTOR_URL_JSON, - onInitWithJson, - sendSpansUsingJson, -} from './utilWithJson'; +const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; +const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55680/v1/trace'; /** * Collector Trace Exporter for Node */ -export class CollectorTraceExporter extends CollectorTraceExporterBase< - CollectorExporterConfigNode -> { - DEFAULT_HEADERS: Record = { - [collectorTypes.OT_REQUEST_HEADER]: '1', - }; - isShutDown: boolean = false; - traceServiceClient?: ServiceClient = undefined; - grpcSpansQueue: GRPCSpanQueueItem[] = []; - metadata?: grpc.Metadata; - headers: Record; - private readonly _protocol: CollectorProtocolNode; - - /** - * @param config - */ - constructor(config: CollectorExporterConfigNode = {}) { - super(config); - this._protocol = - typeof config.protocolNode !== 'undefined' - ? config.protocolNode - : CollectorProtocolNode.GRPC; - if (this._protocol === CollectorProtocolNode.HTTP_JSON) { - this.logger.debug('CollectorExporter - using json over http'); - if (config.metadata) { - this.logger.warn('Metadata cannot be set when using json'); - } - } else { - this.logger.debug('CollectorExporter - using grpc'); - if (config.headers) { - this.logger.warn('Headers cannot be set when using grpc'); - } - } - this.metadata = config.metadata; - this.headers = - parseHeaders(config.headers, this.logger) || this.DEFAULT_HEADERS; - } - - onShutdown(): void { - this.isShutDown = true; - if (this.traceServiceClient) { - this.traceServiceClient.close(); - } - } - - onInit(config: CollectorExporterConfigNode): void { - this.isShutDown = false; - - if (config.protocolNode === CollectorProtocolNode.HTTP_JSON) { - onInitWithJson(this, config); - } else { - onInitWithGrpc(this, config); - } - } - - sendSpans( - spans: ReadableSpan[], - onSuccess: () => void, - onError: (error: collectorTypes.CollectorExporterError) => void - ): void { - if (this.isShutDown) { - this.logger.debug('Shutdown already started. Cannot send spans'); - return; - } - if (this._protocol === CollectorProtocolNode.HTTP_JSON) { - sendSpansUsingJson(this, spans, onSuccess, onError); - } else { - sendSpansUsingGrpc(this, spans, onSuccess, onError); - } +export class CollectorTraceExporter + extends CollectorExporterNodeBase< + ReadableSpan, + collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest + > + implements SpanExporter { + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return toCollectorExportTraceServiceRequest(spans, this); } getDefaultUrl(config: CollectorExporterConfigNode): string { @@ -120,4 +48,19 @@ export class CollectorTraceExporter extends CollectorTraceExporterBase< } return config.url; } + + getDefaultServiceName(config: CollectorExporterConfigNode): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } + + getServiceClient(packageObject: any, serverAddress: string): ServiceClient { + return new packageObject.opentelemetry.proto.collector.trace.v1.TraceService( + serverAddress, + this.credentials + ); + } + + getServiceProtoPath(): string { + return 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; + } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/index.ts b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts index 1c17973de2e..fcbe012b52b 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/index.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts @@ -15,3 +15,4 @@ */ export * from './CollectorTraceExporter'; +export * from './CollectorMetricExporter'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts index a7eb8c74b0d..59992146daf 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts @@ -15,7 +15,6 @@ */ import * as grpc from 'grpc'; -import { ReadableSpan } from '@opentelemetry/tracing'; import { CollectorProtocolNode } from '../../enums'; import { CollectorExporterError, @@ -23,11 +22,11 @@ import { } from '../../types'; /** - * Queue item to be used to save temporary spans in case the GRPC service - * hasn't been fully initialised yet + * Queue item to be used to save temporary spans/metrics in case the GRPC service + * hasn't been fully initialized yet */ -export interface GRPCSpanQueueItem { - spans: ReadableSpan[]; +export interface GRPCQueueItem { + objects: ExportedItem[]; onSuccess: () => void; onError: (error: CollectorExporterError) => void; } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts index 49ca20fc872..6d56ab8757b 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts @@ -19,29 +19,20 @@ import * as grpc from 'grpc'; import * as path from 'path'; import * as collectorTypes from '../../types'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import { toCollectorExportTraceServiceRequest } from '../../transform'; -import { CollectorTraceExporter } from './CollectorTraceExporter'; -import { CollectorExporterConfigNode, GRPCSpanQueueItem } from './types'; +import { GRPCQueueItem } from './types'; import { removeProtocol } from './util'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; export const DEFAULT_COLLECTOR_URL_GRPC = 'localhost:55680'; -export function onInitWithGrpc( - collector: CollectorTraceExporter, - config: CollectorExporterConfigNode +export function initWithGrpc( + collector: CollectorExporterNodeBase ): void { - collector.grpcSpansQueue = []; const serverAddress = removeProtocol(collector.url); - const credentials: grpc.ChannelCredentials = - config.credentials || grpc.credentials.createInsecure(); - - const traceServiceProtoPath = - 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; const includeDirs = [path.resolve(__dirname, 'protos')]; protoLoader - .load(traceServiceProtoPath, { + .load(collector.getServiceProtoPath(), { keepCase: false, longs: String, enums: String, @@ -51,49 +42,44 @@ export function onInitWithGrpc( }) .then(packageDefinition => { const packageObject: any = grpc.loadPackageDefinition(packageDefinition); - collector.traceServiceClient = new packageObject.opentelemetry.proto.collector.trace.v1.TraceService( - serverAddress, - credentials + collector.serviceClient = collector.getServiceClient( + packageObject, + serverAddress ); - if (collector.grpcSpansQueue.length > 0) { - const queue = collector.grpcSpansQueue.splice(0); - queue.forEach((item: GRPCSpanQueueItem) => { - collector.sendSpans(item.spans, item.onSuccess, item.onError); + if (collector.grpcQueue.length > 0) { + const queue = collector.grpcQueue.splice(0); + queue.forEach((item: GRPCQueueItem) => { + collector.send(item.objects, item.onSuccess, item.onError); }); } }); } -export function sendSpansUsingGrpc( - collector: CollectorTraceExporter, - spans: ReadableSpan[], +export function sendUsingGrpc( + collector: CollectorExporterNodeBase, + objects: ExportItem[], onSuccess: () => void, onError: (error: collectorTypes.CollectorExporterError) => void ): void { - if (collector.traceServiceClient) { - const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( - spans, - collector - ); - collector.traceServiceClient.export( - exportTraceServiceRequest, + if (collector.serviceClient) { + const serviceRequest = collector.convert(objects); + + collector.serviceClient.export( + serviceRequest, collector.metadata, (err: collectorTypes.ExportServiceError) => { if (err) { - collector.logger.error( - 'exportTraceServiceRequest', - exportTraceServiceRequest - ); + collector.logger.error('Service request', serviceRequest); onError(err); } else { - collector.logger.debug('spans sent'); + collector.logger.debug('Objects sent'); onSuccess(); } } ); } else { - collector.grpcSpansQueue.push({ - spans, + collector.grpcQueue.push({ + objects, onSuccess, onError, }); diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts index a37638ac120..028f245d53c 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts @@ -18,33 +18,25 @@ import * as url from 'url'; import * as http from 'http'; import * as https from 'https'; -import { ReadableSpan } from '@opentelemetry/tracing'; import * as collectorTypes from '../../types'; -import { toCollectorExportTraceServiceRequest } from '../../transform'; -import { CollectorTraceExporter } from './CollectorTraceExporter'; +import { CollectorExporterNodeBase } from './CollectorExporterNodeBase'; import { CollectorExporterConfigNode } from './types'; -export const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55680/v1/trace'; - -export function onInitWithJson( - _collector: CollectorTraceExporter, +export function initWithJson( + _collector: CollectorExporterNodeBase, _config: CollectorExporterConfigNode ): void { // nothing to be done for json yet } -export function sendSpansUsingJson( - collector: CollectorTraceExporter, - spans: ReadableSpan[], +export function sendWithJson( + collector: CollectorExporterNodeBase, + objects: ExportItem[], onSuccess: () => void, onError: (error: collectorTypes.CollectorExporterError) => void ): void { - const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( - spans, - collector - ); - - const body = JSON.stringify(exportTraceServiceRequest); + const serviceRequest = collector.convert(objects); + const body = JSON.stringify(serviceRequest); const parsedUrl = new url.URL(collector.url); const options = { diff --git a/packages/opentelemetry-exporter-collector/src/transform.ts b/packages/opentelemetry-exporter-collector/src/transform.ts index c5aca6b9dd4..c2916f0bf53 100644 --- a/packages/opentelemetry-exporter-collector/src/transform.ts +++ b/packages/opentelemetry-exporter-collector/src/transform.ts @@ -24,7 +24,7 @@ import { import * as core from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { ReadableSpan } from '@opentelemetry/tracing'; -import { CollectorTraceExporterBase } from './CollectorTraceExporterBase'; +import { CollectorExporterBase } from './CollectorExporterBase'; import { COLLECTOR_SPAN_KIND_MAPPING, opentelemetryProto, @@ -199,7 +199,11 @@ export function toCollectorExportTraceServiceRequest< T extends CollectorExporterConfigBase >( spans: ReadableSpan[], - collectorTraceExporterBase: CollectorTraceExporterBase + collectorTraceExporterBase: CollectorExporterBase< + T, + ReadableSpan, + opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest + > ): opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { const groupedSpans: Map< Resource, diff --git a/packages/opentelemetry-exporter-collector/src/transformMetrics.ts b/packages/opentelemetry-exporter-collector/src/transformMetrics.ts index be14cea32aa..96263c2d8a6 100644 --- a/packages/opentelemetry-exporter-collector/src/transformMetrics.ts +++ b/packages/opentelemetry-exporter-collector/src/transformMetrics.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CollectorMetricExporterBase } from './CollectorMetricExporterBase'; import { MetricRecord, MetricKind, @@ -28,6 +27,7 @@ import * as api from '@opentelemetry/api'; import * as core from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { toCollectorResource } from './transform'; +import { CollectorExporterBase } from './CollectorExporterBase'; /** * Converts labels @@ -220,7 +220,11 @@ export function toCollectorExportMetricServiceRequest< >( metrics: MetricRecord[], startTime: number, - collectorMetricExporterBase: CollectorMetricExporterBase + collectorExporterBase: CollectorExporterBase< + T, + MetricRecord, + opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest + > ): opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { const groupedMetrics: Map< Resource, @@ -228,9 +232,9 @@ export function toCollectorExportMetricServiceRequest< > = groupMetricsByResourceAndLibrary(metrics); const additionalAttributes = Object.assign( {}, - collectorMetricExporterBase.attributes, + collectorExporterBase.attributes, { - 'service.name': collectorMetricExporterBase.serviceName, + 'service.name': collectorExporterBase.serviceName, } ); return { diff --git a/packages/opentelemetry-exporter-collector/test/common/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/common/CollectorMetricExporter.test.ts index 73e9ddc40eb..086a8ea82f9 100644 --- a/packages/opentelemetry-exporter-collector/test/common/CollectorMetricExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/CollectorMetricExporter.test.ts @@ -17,20 +17,31 @@ import { ExportResult, NoopLogger } from '@opentelemetry/core'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { CollectorMetricExporterBase } from '../../src/CollectorMetricExporterBase'; +import { CollectorExporterBase } from '../../src/CollectorExporterBase'; import { CollectorExporterConfigBase } from '../../src/types'; import { MetricRecord } from '@opentelemetry/metrics'; import { mockCounter, mockObserver } from '../helper'; +import * as collectorTypes from '../../src/types'; type CollectorExporterConfig = CollectorExporterConfigBase; -class CollectorMetricExporter extends CollectorMetricExporterBase< - CollectorExporterConfig +class CollectorMetricExporter extends CollectorExporterBase< + CollectorExporterConfig, + MetricRecord, + collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest > { onInit() {} onShutdown() {} - sendMetrics() {} - getDefaultUrl(url: string) { - return url || ''; + send() {} + getDefaultUrl(config: CollectorExporterConfig) { + return config.url || ''; + } + getDefaultServiceName(config: CollectorExporterConfig): string { + return config.serviceName || 'collector-metric-exporter'; + } + convert( + metrics: MetricRecord[] + ): collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { + return { resourceMetrics: [] }; } } @@ -107,7 +118,7 @@ describe('CollectorMetricExporter - common', () => { describe('export', () => { let spySend: any; beforeEach(() => { - spySend = sinon.stub(CollectorMetricExporter.prototype, 'sendMetrics'); + spySend = sinon.stub(CollectorMetricExporter.prototype, 'send'); collectorExporter = new CollectorMetricExporter(collectorExporterConfig); }); afterEach(() => { diff --git a/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts index 69c4a15de31..5ec87838762 100644 --- a/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts @@ -18,20 +18,32 @@ import { ExportResult, NoopLogger } from '@opentelemetry/core'; import { ReadableSpan } from '@opentelemetry/tracing'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { CollectorTraceExporterBase } from '../../src/CollectorTraceExporterBase'; +import { CollectorExporterBase } from '../../src/CollectorExporterBase'; import { CollectorExporterConfigBase } from '../../src/types'; import { mockedReadableSpan } from '../helper'; +import * as collectorTypes from '../../src/types'; type CollectorExporterConfig = CollectorExporterConfigBase; -class CollectorTraceExporter extends CollectorTraceExporterBase< - CollectorExporterConfig +class CollectorTraceExporter extends CollectorExporterBase< + CollectorExporterConfig, + ReadableSpan, + collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest > { onInit() {} onShutdown() {} - sendSpans() {} - getDefaultUrl(config: CollectorExporterConfig) { + send() {} + getDefaultUrl(config: CollectorExporterConfig): string { return config.url || ''; } + getDefaultServiceName(config: CollectorExporterConfig): string { + return config.serviceName || 'collector-exporter'; + } + + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return { resourceSpans: [] }; + } } describe('CollectorTraceExporter - common', () => { @@ -101,7 +113,7 @@ describe('CollectorTraceExporter - common', () => { describe('export', () => { let spySend: any; beforeEach(() => { - spySend = sinon.stub(CollectorTraceExporter.prototype, 'sendSpans'); + spySend = sinon.stub(CollectorTraceExporter.prototype, 'send'); collectorExporter = new CollectorTraceExporter(collectorExporterConfig); }); afterEach(() => { diff --git a/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts b/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts index 6e4563e6b6b..8cae6cc2cc5 100644 --- a/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/transformMetrics.test.ts @@ -29,30 +29,41 @@ import { ensureValueRecorderIsCorrect, mockValueRecorder, } from '../helper'; -import { HistogramAggregator } from '@opentelemetry/metrics'; import { hrTimeToNanoseconds } from '@opentelemetry/core'; describe('transformMetrics', () => { describe('toCollectorMetric', () => { - it('should convert metric', () => { + beforeEach(() => { + // Counter mockCounter.aggregator.update(1); + + // Observer + mockObserver.aggregator.update(10); + + // Histogram + mockHistogram.aggregator.update(7); + mockHistogram.aggregator.update(14); + + // ValueRecorder + mockValueRecorder.aggregator.update(5); + }); + + afterEach(() => { + mockCounter.aggregator.update(-1); // Reset counter + }); + it('should convert metric', () => { ensureCounterIsCorrect( transform.toCollectorMetric(mockCounter, 1592602232694000000), hrTimeToNanoseconds(mockCounter.aggregator.toPoint().timestamp) ); - mockObserver.aggregator.update(10); ensureObserverIsCorrect( transform.toCollectorMetric(mockObserver, 1592602232694000000), hrTimeToNanoseconds(mockObserver.aggregator.toPoint().timestamp) ); - mockHistogram.aggregator.update(7); - mockHistogram.aggregator.update(14); - (mockHistogram.aggregator as HistogramAggregator).reset(); ensureHistogramIsCorrect( transform.toCollectorMetric(mockHistogram, 1592602232694000000), hrTimeToNanoseconds(mockHistogram.aggregator.toPoint().timestamp) ); - mockValueRecorder.aggregator.update(5); ensureValueRecorderIsCorrect( transform.toCollectorMetric(mockValueRecorder, 1592602232694000000), hrTimeToNanoseconds(mockValueRecorder.aggregator.toPoint().timestamp) diff --git a/packages/opentelemetry-exporter-collector/test/helper.ts b/packages/opentelemetry-exporter-collector/test/helper.ts index f2ed9cf0487..7437460b421 100644 --- a/packages/opentelemetry-exporter-collector/test/helper.ts +++ b/packages/opentelemetry-exporter-collector/test/helper.ts @@ -38,6 +38,10 @@ if (typeof Buffer === 'undefined') { }; } +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + const traceIdArr = [ 31, 16, @@ -113,7 +117,7 @@ export const mockValueRecorder: MetricRecord = { instrumentationLibrary: { name: 'default', version: '0.0.1' }, }; -export const mockHistogram: MetricRecord = { +export const mockHistogram: Mutable = { descriptor: { name: 'test-hist', description: 'sample observer description', @@ -757,6 +761,104 @@ export function ensureHistogramIsCorrect( }); } +export function ensureExportedCounterIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-counter', + description: 'sample counter description', + unit: '1', + type: 'MONOTONIC_INT64', + temporality: 'CUMULATIVE', + }); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.ok(metric.int64DataPoints); + assert.deepStrictEqual(metric.int64DataPoints[0].labels, []); + assert.deepStrictEqual(metric.int64DataPoints[0].value, '1'); + assert.deepStrictEqual( + metric.int64DataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedObserverIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-observer', + description: 'sample observer description', + unit: '2', + type: 'DOUBLE', + temporality: 'DELTA', + }); + + assert.deepStrictEqual(metric.int64DataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.ok(metric.doubleDataPoints); + assert.deepStrictEqual(metric.doubleDataPoints[0].labels, []); + assert.deepStrictEqual(metric.doubleDataPoints[0].value, 10); + assert.deepStrictEqual( + metric.doubleDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedHistogramIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-hist', + description: 'sample observer description', + unit: '2', + type: 'HISTOGRAM', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.int64DataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.ok(metric.histogramDataPoints); + assert.deepStrictEqual(metric.histogramDataPoints[0].labels, []); + assert.deepStrictEqual(metric.histogramDataPoints[0].count, '2'); + assert.deepStrictEqual(metric.histogramDataPoints[0].sum, 21); + assert.deepStrictEqual(metric.histogramDataPoints[0].buckets, [ + { count: '1', exemplar: null }, + { count: '1', exemplar: null }, + { count: '0', exemplar: null }, + ]); + assert.deepStrictEqual(metric.histogramDataPoints[0].explicitBounds, [ + 10, + 20, + ]); + assert.deepStrictEqual( + metric.histogramDataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + +export function ensureExportedValueRecorderIsCorrect( + metric: collectorTypes.opentelemetryProto.metrics.v1.Metric +) { + assert.deepStrictEqual(metric.metricDescriptor, { + name: 'test-recorder', + description: 'sample recorder description', + unit: '3', + type: 'INT64', + temporality: 'DELTA', + }); + assert.deepStrictEqual(metric.histogramDataPoints, []); + assert.deepStrictEqual(metric.summaryDataPoints, []); + assert.deepStrictEqual(metric.doubleDataPoints, []); + assert.ok(metric.int64DataPoints); + assert.deepStrictEqual(metric.int64DataPoints[0].labels, []); + assert.deepStrictEqual( + metric.int64DataPoints[0].startTimeUnixNano, + '1592602232694000128' + ); +} + export function ensureResourceIsCorrect( resource: collectorTypes.opentelemetryProto.resource.v1.Resource ) { @@ -836,7 +938,11 @@ export function ensureExportMetricsServiceRequestIsSet( json: collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest ) { const resourceMetrics = json.resourceMetrics; - assert.strictEqual(resourceMetrics.length, 2, 'resourceMetrics is missing'); + assert.strictEqual( + resourceMetrics.length, + 4, + 'resourceMetrics is the incorrect length' + ); const resource = resourceMetrics[0].resource; assert.strictEqual(!!resource, true, 'resource is missing'); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts new file mode 100644 index 00000000000..abdd1d774c5 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporter.test.ts @@ -0,0 +1,254 @@ +/* + * 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 * as protoLoader from '@grpc/proto-loader'; +import * as grpc from 'grpc'; +import * as path from 'path'; +import * as fs from 'fs'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CollectorMetricExporter } from '../../src/platform/node'; +import * as collectorTypes from '../../src/types'; +import { MetricRecord, HistogramAggregator } from '@opentelemetry/metrics'; +import { + mockCounter, + mockObserver, + mockHistogram, + ensureExportedCounterIsCorrect, + ensureExportedObserverIsCorrect, + ensureMetadataIsCorrect, + ensureResourceIsCorrect, + ensureExportedHistogramIsCorrect, + ensureExportedValueRecorderIsCorrect, + mockValueRecorder, +} from '../helper'; +import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; +import { CollectorProtocolNode } from '../../src'; + +const metricsServiceProtoPath = + 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto'; +const includeDirs = [path.resolve(__dirname, '../../src/platform/node/protos')]; + +const address = 'localhost:1501'; + +type TestParams = { + useTLS?: boolean; + metadata?: grpc.Metadata; +}; + +const metadata = new grpc.Metadata(); +metadata.set('k', 'v'); + +const testCollectorMetricExporter = (params: TestParams) => + describe(`CollectorMetricExporter - node ${ + params.useTLS ? 'with' : 'without' + } TLS, ${params.metadata ? 'with' : 'without'} metadata`, () => { + let collectorExporter: CollectorMetricExporter; + let server: grpc.Server; + let exportedData: + | collectorTypes.opentelemetryProto.metrics.v1.ResourceMetrics[] + | undefined; + let metrics: MetricRecord[]; + let reqMetadata: grpc.Metadata | undefined; + + before(done => { + server = new grpc.Server(); + protoLoader + .load(metricsServiceProtoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }) + .then((packageDefinition: protoLoader.PackageDefinition) => { + const packageObject: any = grpc.loadPackageDefinition( + packageDefinition + ); + server.addService( + packageObject.opentelemetry.proto.collector.metrics.v1 + .MetricsService.service, + { + Export: (data: { + request: collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + metadata: grpc.Metadata; + }) => { + try { + exportedData = data.request.resourceMetrics; + reqMetadata = data.metadata; + } catch (e) { + exportedData = undefined; + } + }, + } + ); + const credentials = params.useTLS + ? grpc.ServerCredentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + [ + { + cert_chain: fs.readFileSync('./test/certs/server.crt'), + private_key: fs.readFileSync('./test/certs/server.key'), + }, + ] + ) + : grpc.ServerCredentials.createInsecure(); + server.bind(address, credentials); + server.start(); + done(); + }); + }); + + after(() => { + server.forceShutdown(); + }); + + beforeEach(done => { + const credentials = params.useTLS + ? grpc.credentials.createSsl( + fs.readFileSync('./test/certs/ca.crt'), + fs.readFileSync('./test/certs/client.key'), + fs.readFileSync('./test/certs/client.crt') + ) + : undefined; + collectorExporter = new CollectorMetricExporter({ + url: address, + credentials, + serviceName: 'basic-service', + metadata: params.metadata, + }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + metrics = []; + metrics.push(Object.assign({}, mockCounter)); + metrics.push(Object.assign({}, mockObserver)); + metrics.push(Object.assign({}, mockHistogram)); + metrics.push(Object.assign({}, mockValueRecorder)); + + metrics[0].aggregator.update(1); + metrics[1].aggregator.update(10); + metrics[2].aggregator.update(7); + metrics[2].aggregator.update(14); + metrics[3].aggregator.update(5); + done(); + }); + + afterEach(() => { + // Aggregator is not deep-copied + metrics[0].aggregator.update(-1); + mockHistogram.aggregator = new HistogramAggregator([10, 20]); + exportedData = undefined; + reqMetadata = undefined; + }); + + describe('instance', () => { + it('should warn about headers when using grpc', () => { + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorMetricExporter({ + logger, + serviceName: 'basic-service', + url: address, + headers: { + foo: 'bar', + }, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + }); + it('should warn about metadata when using json', () => { + const metadata = new grpc.Metadata(); + metadata.set('k', 'v'); + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorMetricExporter({ + logger, + serviceName: 'basic-service', + url: address, + metadata, + protocolNode: CollectorProtocolNode.HTTP_JSON, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Metadata cannot be set when using json'); + }); + }); + + describe('export', () => { + it('should export metrics', done => { + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + setTimeout(() => { + assert.ok( + typeof exportedData !== 'undefined', + 'resource' + " doesn't exist" + ); + let resource; + if (exportedData) { + resource = exportedData[0].resource; + const counter = + exportedData[0].instrumentationLibraryMetrics[0].metrics[0]; + const observer = + exportedData[1].instrumentationLibraryMetrics[0].metrics[0]; + const histogram = + exportedData[2].instrumentationLibraryMetrics[0].metrics[0]; + const recorder = + exportedData[3].instrumentationLibraryMetrics[0].metrics[0]; + ensureExportedCounterIsCorrect(counter); + ensureExportedObserverIsCorrect(observer); + ensureExportedHistogramIsCorrect(histogram); + ensureExportedValueRecorderIsCorrect(recorder); + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureResourceIsCorrect(resource); + } + } + if (params.metadata && reqMetadata) { + ensureMetadataIsCorrect(reqMetadata, params.metadata); + } + done(); + }, 500); + }); + }); + }); + +describe('CollectorMetricExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorMetricExporter({}); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], 'localhost:55680'); + done(); + }); + }); + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorMetricExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); + }); +}); + +testCollectorMetricExporter({ useTLS: true }); +testCollectorMetricExporter({ useTLS: false }); +testCollectorMetricExporter({ metadata }); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts new file mode 100644 index 00000000000..a3e61d42f85 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorMetricExporterWithJson.test.ts @@ -0,0 +1,233 @@ +/* + * 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 * as core from '@opentelemetry/core'; +import * as http from 'http'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CollectorProtocolNode } from '../../src/enums'; +import { CollectorMetricExporter } from '../../src/platform/node'; +import { CollectorExporterConfigNode } from '../../src/platform/node/types'; +import * as collectorTypes from '../../src/types'; + +import { + mockCounter, + mockObserver, + mockHistogram, + ensureExportMetricsServiceRequestIsSet, + ensureCounterIsCorrect, + mockValueRecorder, + ensureValueRecorderIsCorrect, + ensureHistogramIsCorrect, + ensureObserverIsCorrect, +} from '../helper'; +import { MetricRecord, HistogramAggregator } from '@opentelemetry/metrics'; + +const fakeRequest = { + end: function () {}, + on: function () {}, + write: function () {}, +}; + +const mockRes = { + statusCode: 200, +}; + +const mockResError = { + statusCode: 400, +}; + +describe('CollectorMetricExporter - node with json over http', () => { + let collectorExporter: CollectorMetricExporter; + let collectorExporterConfig: CollectorExporterConfigNode; + let spyRequest: sinon.SinonSpy; + let spyWrite: sinon.SinonSpy; + let metrics: MetricRecord[]; + describe('export', () => { + beforeEach(() => { + spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); + spyWrite = sinon.stub(fakeRequest, 'write'); + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + protocolNode: CollectorProtocolNode.HTTP_JSON, + hostname: 'foo', + logger: new core.NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorMetricExporter(collectorExporterConfig); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + metrics = []; + metrics.push(Object.assign({}, mockCounter)); + metrics.push(Object.assign({}, mockObserver)); + metrics.push(Object.assign({}, mockHistogram)); + metrics.push(Object.assign({}, mockValueRecorder)); + metrics[0].aggregator.update(1); + metrics[1].aggregator.update(10); + metrics[2].aggregator.update(7); + metrics[2].aggregator.update(14); + metrics[3].aggregator.update(5); + }); + afterEach(() => { + // Aggregator is not deep-copied + metrics[0].aggregator.update(-1); + mockHistogram.aggregator = new HistogramAggregator([10, 20]); + spyRequest.restore(); + spyWrite.restore(); + }); + + it('should open the connection', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + + assert.strictEqual(options.hostname, 'foo.bar.com'); + assert.strictEqual(options.method, 'POST'); + assert.strictEqual(options.path, '/'); + done(); + }); + }); + + it('should set custom headers', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + assert.strictEqual(options.headers['foo'], 'bar'); + done(); + }); + }); + + it('should successfully send the spans', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const writeArgs = spyWrite.args[0]; + const json = JSON.parse( + writeArgs[0] + ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const metric1 = + json.resourceMetrics[0].instrumentationLibraryMetrics[0].metrics[0]; + const metric2 = + json.resourceMetrics[1].instrumentationLibraryMetrics[0].metrics[0]; + const metric3 = + json.resourceMetrics[2].instrumentationLibraryMetrics[0].metrics[0]; + const metric4 = + json.resourceMetrics[3].instrumentationLibraryMetrics[0].metrics[0]; + assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); + ensureCounterIsCorrect( + metric1, + core.hrTimeToNanoseconds(metrics[0].aggregator.toPoint().timestamp) + ); + assert.ok(typeof metric2 !== 'undefined', "observer doesn't exist"); + ensureObserverIsCorrect( + metric2, + core.hrTimeToNanoseconds(metrics[1].aggregator.toPoint().timestamp) + ); + assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist"); + ensureHistogramIsCorrect( + metric3, + core.hrTimeToNanoseconds(metrics[2].aggregator.toPoint().timestamp) + ); + assert.ok( + typeof metric4 !== 'undefined', + "value recorder doesn't exist" + ); + ensureValueRecorderIsCorrect( + metric4, + core.hrTimeToNanoseconds(metrics[3].aggregator.toPoint().timestamp) + ); + + ensureExportMetricsServiceRequestIsSet(json); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockRes); + setTimeout(() => { + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'statusCode: 200'); + assert.strictEqual(spyLoggerError.args.length, 0); + assert.strictEqual(responseSpy.args[0][0], 0); + done(); + }); + }); + }); + + it('should log the error message', done => { + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + const responseSpy = sinon.spy(); + collectorExporter.export(metrics, responseSpy); + + setTimeout(() => { + const args = spyRequest.args[0]; + const callback = args[1]; + callback(mockResError); + setTimeout(() => { + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'statusCode: 400'); + + assert.strictEqual(responseSpy.args[0][0], 1); + done(); + }); + }); + }); + }); + describe('CollectorMetricExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorMetricExporter({ + protocolNode: CollectorProtocolNode.HTTP_JSON, + }); + setTimeout(() => { + assert.strictEqual( + collectorExporter['url'], + 'http://localhost:55680/v1/metrics' + ); + done(); + }); + }); + + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorMetricExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts similarity index 98% rename from packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts rename to packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts index 3b21ea2c638..105e5b346e1 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporterWithJson.test.ts @@ -44,7 +44,7 @@ const mockResError = { statusCode: 400, }; -describe('CollectorExporter - node with json over http', () => { +describe('CollectorTraceExporter - node with json over http', () => { let collectorExporter: CollectorTraceExporter; let collectorExporterConfig: CollectorExporterConfigNode; let spyRequest: sinon.SinonSpy; diff --git a/packages/opentelemetry-exporter-jaeger/package.json b/packages/opentelemetry-exporter-jaeger/package.json index 162e00b4dfd..13b3d22c950 100644 --- a/packages/opentelemetry-exporter-jaeger/package.json +++ b/packages/opentelemetry-exporter-jaeger/package.json @@ -43,9 +43,9 @@ }, "devDependencies": { "@opentelemetry/resources": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", - "codecov": "3.7.0", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nock": "12.0.3", @@ -53,7 +53,7 @@ "rimraf": "3.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-exporter-prometheus/package.json b/packages/opentelemetry-exporter-prometheus/package.json index b7991740597..cd45cad7b02 100644 --- a/packages/opentelemetry-exporter-prometheus/package.json +++ b/packages/opentelemetry-exporter-prometheus/package.json @@ -41,16 +41,16 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", - "codecov": "3.7.0", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "3.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index 285f2b8f6ee..7ffcaeac759 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -40,9 +40,9 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", - "codecov": "3.7.0", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nock": "12.0.3", @@ -50,7 +50,7 @@ "rimraf": "3.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-grpc-utils/.eslintignore b/packages/opentelemetry-grpc-utils/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/packages/opentelemetry-grpc-utils/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-grpc-utils/.eslintrc.js b/packages/opentelemetry-grpc-utils/.eslintrc.js new file mode 100644 index 00000000000..f726f3becb6 --- /dev/null +++ b/packages/opentelemetry-grpc-utils/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-grpc-utils/.npmignore b/packages/opentelemetry-grpc-utils/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/packages/opentelemetry-grpc-utils/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-grpc-utils/LICENSE b/packages/opentelemetry-grpc-utils/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-grpc-utils/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/packages/opentelemetry-grpc-utils/README.md b/packages/opentelemetry-grpc-utils/README.md new file mode 100644 index 00000000000..064b5be954b --- /dev/null +++ b/packages/opentelemetry-grpc-utils/README.md @@ -0,0 +1,69 @@ +# OpenTelemetry gRPC Instrumentation for Node.js + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![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. + +For automatic instrumentation see the +[@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. + +## Installation + +```sh +npm install --save @opentelemetry/plugin-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). + +To load a specific plugin (**gRPC** in this case), specify it in the Node Tracer's configuration. + +```javascript +const { NodeTracerProvider } = require('@opentelemetry/node'); + +const provider = new NodeTracerProvider({ + plugins: { + grpc: { + enabled: true, + // You may use a package name or absolute path to the file. + path: '@opentelemetry/plugin-grpc', + } + } +}); +``` + +To load all of the [supported plugins](https://github.com/open-telemetry/opentelemetry-js#plugins), use below approach. Each plugin is only loaded when the module that it patches is loaded; in other words, there is no computational overhead for listing plugins for unused modules. + +```javascript +const { NodeTracerProvider } = require('@opentelemetry/node'); + +const provider = new NodeTracerProvider(); +``` + +See [examples/grpc](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/grpc) for a short example. + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-grpc +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-grpc +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-grpc +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-grpc&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/plugin-grpc +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fplugin-grpc.svg diff --git a/packages/opentelemetry-grpc-utils/package.json b/packages/opentelemetry-grpc-utils/package.json new file mode 100644 index 00000000000..a259a5539f3 --- /dev/null +++ b/packages/opentelemetry-grpc-utils/package.json @@ -0,0 +1,75 @@ +{ + "name": "@opentelemetry/grpc-utils", + "version": "0.9.0", + "private": true, + "description": "OpenTelemetry grpc plugin utility functions.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "precompile": "tsc --version", + "version:update": "node ../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p .", + "prepare": "npm run compile" + }, + "keywords": [ + "opentelemetry", + "grpc", + "nodejs", + "tracing", + "profiling", + "plugin" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@grpc/grpc-js": "1.1.2", + "@grpc/proto-loader": "0.5.4", + "@opentelemetry/context-async-hooks": "^0.9.0", + "@opentelemetry/context-base": "^0.9.0", + "@opentelemetry/node": "^0.9.0", + "@opentelemetry/tracing": "^0.9.0", + "@types/mocha": "7.0.2", + "@types/node": "14.0.13", + "@types/semver": "7.2.0", + "@types/shimmer": "1.0.1", + "@types/sinon": "9.0.4", + "codecov": "3.7.0", + "grpc": "1.24.3", + "gts": "2.0.2", + "mocha": "7.2.0", + "node-pre-gyp": "0.15.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "semver": "7.3.2", + "sinon": "9.0.2", + "ts-mocha": "7.0.0", + "ts-node": "8.10.2", + "typescript": "3.9.5" + }, + "dependencies": { + "@opentelemetry/api": "^0.9.0", + "@opentelemetry/core": "^0.9.0", + "@opentelemetry/semantic-conventions": "^0.9.0", + "shimmer": "^1.2.1" + } +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/enums/AttributeNames.ts b/packages/opentelemetry-grpc-utils/src/version.ts similarity index 69% rename from packages/opentelemetry-plugin-grpc-js/src/enums/AttributeNames.ts rename to packages/opentelemetry-grpc-utils/src/version.ts index 3fdea85bc71..2c92beb616c 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/enums/AttributeNames.ts +++ b/packages/opentelemetry-grpc-utils/src/version.ts @@ -14,11 +14,5 @@ * limitations under the License. */ -export enum AttributeNames { - COMPONENT = 'component', - GRPC_KIND = 'grpc.kind', // SERVER or CLIENT - GRPC_METHOD = 'grpc.method', - GRPC_STATUS_CODE = 'grpc.status_code', - GRPC_ERROR_NAME = 'grpc.error_name', - GRPC_ERROR_MESSAGE = 'grpc.error_message', -} +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.9.0'; diff --git a/packages/opentelemetry-plugin-grpc/test/fixtures/grpc-test.proto b/packages/opentelemetry-grpc-utils/test/fixtures/grpc-test.proto similarity index 87% rename from packages/opentelemetry-plugin-grpc/test/fixtures/grpc-test.proto rename to packages/opentelemetry-grpc-utils/test/fixtures/grpc-test.proto index 54e16b85d54..4949dd5e0d1 100644 --- a/packages/opentelemetry-plugin-grpc/test/fixtures/grpc-test.proto +++ b/packages/opentelemetry-grpc-utils/test/fixtures/grpc-test.proto @@ -4,6 +4,7 @@ package pkg_test; service GrpcTester { rpc UnaryMethod (TestRequest) returns (TestReply) {} + rpc camelCaseMethod (TestRequest) returns (TestReply) {} rpc ClientStreamMethod (stream TestRequest) returns (TestReply) {} rpc ServerStreamMethod (TestRequest) returns (stream TestReply) {} rpc BidiStreamMethod (stream TestRequest) returns (stream TestReply) {} diff --git a/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts new file mode 100644 index 00000000000..e5f49b8b55a --- /dev/null +++ b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts @@ -0,0 +1,722 @@ +/* + * 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 { + context, + NoopTracerProvider, + SpanKind, + propagation, +} from '@opentelemetry/api'; +import { NoopLogger, HttpTraceContext, BasePlugin } from '@opentelemetry/core'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { ContextManager } from '@opentelemetry/context-base'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import * as protoLoader from '@grpc/proto-loader'; +import type * as grpcNapi from 'grpc'; +import type * as grpcJs from '@grpc/grpc-js'; +import { assertPropagation, assertSpan } from './utils/assertionUtils'; +import { promisify } from 'util'; + +const PROTO_PATH = + process.cwd() + + '/node_modules/@opentelemetry/grpc-utils/test/fixtures/grpc-test.proto'; +const memoryExporter = new InMemorySpanExporter(); + +const options = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}; + +interface TestRequestResponse { + num: number; +} + +type ServiceError = grpcNapi.ServiceError | grpcJs.ServiceError; +type Client = grpcNapi.Client | grpcJs.Client; +type Server = grpcNapi.Server | grpcJs.Server; +type ServerUnaryCall = + | grpcNapi.ServerUnaryCall + | grpcJs.ServerUnaryCall; +type RequestCallback = grpcJs.requestCallback; +type ServerReadableStream = + | grpcNapi.ServerReadableStream + | grpcJs.ServerReadableStream; +type ServerWriteableStream = + | grpcNapi.ServerWriteableStream + | grpcJs.ServerWritableStream; +type ServerDuplexStream = + | grpcNapi.ServerDuplexStream + | grpcJs.ServerDuplexStream; + +type TestGrpcClient = (typeof grpcJs | typeof grpcNapi)['Client'] & { + unaryMethod: any; + UnaryMethod: any; + camelCaseMethod: any; + clientStreamMethod: any; + serverStreamMethod: any; + bidiStreamMethod: any; +}; + +// Compare two arrays using an equal function f +const arrayIsEqual = (f: any) => ([x, ...xs]: any) => ([y, ...ys]: any): any => + x === undefined && y === undefined + ? true + : Boolean(f(x)(y)) && arrayIsEqual(f)(xs)(ys); + +// Return true if two requests has the same num value +const requestEqual = (x: TestRequestResponse) => (y: TestRequestResponse) => + x.num !== undefined && x.num === y.num; + +// Check if its equal requests or array of requests +const checkEqual = (x: TestRequestResponse | TestRequestResponse[]) => ( + y: TestRequestResponse | TestRequestResponse[] +) => + x instanceof Array && y instanceof Array + ? arrayIsEqual(requestEqual)(x as any)(y as any) + : !(x instanceof Array) && !(y instanceof Array) + ? requestEqual(x)(y) + : false; + +export const runTests = ( + plugin: BasePlugin, + moduleName: string, + grpc: typeof grpcNapi | typeof grpcJs, + grpcPort: number +) => { + const MAX_ERROR_STATUS = grpc.status.UNAUTHENTICATED; + + const grpcClient = { + unaryMethod: ( + client: TestGrpcClient, + request: TestRequestResponse + ): Promise => { + return new Promise((resolve, reject) => { + return client.unaryMethod( + request, + (err: ServiceError, response: TestRequestResponse) => { + if (err) { + reject(err); + } else { + resolve(response); + } + } + ); + }); + }, + + UnaryMethod: ( + client: TestGrpcClient, + request: TestRequestResponse + ): Promise => { + return new Promise((resolve, reject) => { + return client.UnaryMethod( + request, + (err: ServiceError, response: TestRequestResponse) => { + if (err) { + reject(err); + } else { + resolve(response); + } + } + ); + }); + }, + + camelCaseMethod: ( + client: TestGrpcClient, + request: TestRequestResponse + ): Promise => { + return new Promise((resolve, reject) => { + return client.camelCaseMethod( + request, + (err: ServiceError, response: TestRequestResponse) => { + if (err) { + reject(err); + } else { + resolve(response); + } + } + ); + }); + }, + + clientStreamMethod: ( + client: TestGrpcClient, + request: TestRequestResponse[] + ): Promise => { + return new Promise((resolve, reject) => { + const writeStream = client.clientStreamMethod( + (err: ServiceError, response: TestRequestResponse) => { + if (err) { + reject(err); + } else { + resolve(response); + } + } + ); + + request.forEach(element => { + writeStream.write(element); + }); + writeStream.end(); + }); + }, + + serverStreamMethod: ( + client: TestGrpcClient, + request: TestRequestResponse + ): Promise => { + return new Promise((resolve, reject) => { + const result: TestRequestResponse[] = []; + const readStream = client.serverStreamMethod(request); + + readStream.on('data', (data: TestRequestResponse) => { + result.push(data); + }); + readStream.on('error', (err: ServiceError) => { + reject(err); + }); + readStream.on('end', () => { + resolve(result); + }); + }); + }, + + bidiStreamMethod: ( + client: TestGrpcClient, + request: TestRequestResponse[] + ): Promise => { + return new Promise((resolve, reject) => { + const result: TestRequestResponse[] = []; + const bidiStream = client.bidiStreamMethod([]); + + bidiStream.on('data', (data: TestRequestResponse) => { + result.push(data); + }); + + request.forEach(element => { + bidiStream.write(element); + }); + + bidiStream.on('error', (err: ServiceError) => { + reject(err); + }); + + bidiStream.on('end', () => { + resolve(result); + }); + + bidiStream.end(); + }); + }, + }; + + let server: Server; + let client: Client; + + const replicate = (request: TestRequestResponse) => { + const result: TestRequestResponse[] = []; + for (let i = 0; i < request.num; i++) { + result.push(request); + } + return result; + }; + + async function startServer( + grpc: typeof grpcJs | typeof grpcNapi, + proto: any + ) { + const server = new grpc.Server(); + + function getError(msg: string, code: number): ServiceError | null { + const err: ServiceError = { + ...new Error(msg), + name: msg, + message: msg, + code, + details: msg, + }; + return err; + } + + server.addService(proto.GrpcTester.service, { + // An error is emitted every time + // request.num <= MAX_ERROR_STATUS = (status.UNAUTHENTICATED) + // in those cases, erro.code = request.num + + // This method returns the request + unaryMethod(call: ServerUnaryCall, callback: RequestCallback) { + call.request.num <= MAX_ERROR_STATUS + ? callback( + getError( + 'Unary Method Error', + call.request.num + ) as grpcJs.ServiceError + ) + : callback(null, { num: call.request.num }); + }, + + // This method returns the request + camelCaseMethod(call: ServerUnaryCall, callback: RequestCallback) { + call.request.num <= MAX_ERROR_STATUS + ? callback( + getError( + 'Unary Method Error', + call.request.num + ) as grpcJs.ServiceError + ) + : callback(null, { num: call.request.num }); + }, + + // This method sums the requests + clientStreamMethod( + call: ServerReadableStream, + callback: RequestCallback + ) { + let sum = 0; + let hasError = false; + let code = grpc.status.OK; + call.on('data', (data: TestRequestResponse) => { + sum += data.num; + if (data.num <= MAX_ERROR_STATUS) { + hasError = true; + code = data.num; + } + }); + call.on('end', () => { + hasError + ? callback(getError('Client Stream Method Error', code) as any) + : callback(null, { num: sum }); + }); + }, + + // This method returns an array that replicates the request, request.num of + // times + serverStreamMethod: (call: ServerWriteableStream) => { + const result = replicate(call.request); + + if (call.request.num <= MAX_ERROR_STATUS) { + call.emit( + 'error', + getError('Server Stream Method Error', call.request.num) + ); + } else { + result.forEach(element => { + call.write(element); + }); + } + call.end(); + }, + + // This method returns the request + bidiStreamMethod: (call: ServerDuplexStream) => { + call.on('data', (data: TestRequestResponse) => { + if (data.num <= MAX_ERROR_STATUS) { + call.emit( + 'error', + getError('Server Stream Method Error', data.num) + ); + } else { + call.write(data); + } + }); + call.on('end', () => { + call.end(); + }); + }, + }); + const bindAwait = promisify(server.bindAsync); + await bindAwait.call( + server, + 'localhost:' + grpcPort, + grpc.ServerCredentials.createInsecure() as grpcJs.ServerCredentials + ); + server.start(); + return server; + } + + function createClient(grpc: typeof grpcJs | typeof grpcNapi, proto: any) { + return new proto.GrpcTester( + 'localhost:' + grpcPort, + grpc.credentials.createInsecure() + ); + } + + return describe('GrpcPlugin', () => { + let contextManager: ContextManager; + + before(() => { + propagation.setGlobalPropagator(new HttpTraceContext()); + }); + + beforeEach(() => { + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(() => { + context.disable(); + }); + + it('moduleName should be grpc', () => { + assert.deepStrictEqual(moduleName, plugin.moduleName); + }); + + describe('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => { + after(() => { + plugin.disable(); + }); + + it('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => { + plugin.enable(grpc, new NoopTracerProvider(), new NoopLogger()); + (plugin['_moduleExports'] as any).makeGenericClientConstructor({}); + assert.ok( + plugin['_moduleExports'].makeGenericClientConstructor.__wrapped + ); + }); + }); + + const requestList: TestRequestResponse[] = [{ num: 100 }, { num: 50 }]; + const resultSum = { + num: requestList.reduce((sum, x) => { + return sum + x.num; + }, 0), + }; + const methodList = [ + { + description: 'unary call', + methodName: 'UnaryMethod', + method: grpcClient.unaryMethod, + request: requestList[0], + result: requestList[0], + }, + { + description: 'Unary call', + methodName: 'UnaryMethod', + method: grpcClient.UnaryMethod, + request: requestList[0], + result: requestList[0], + }, + { + description: 'camelCase unary call', + methodName: 'camelCaseMethod', + method: grpcClient.camelCaseMethod, + request: requestList[0], + result: requestList[0], + }, + { + description: 'clientStream call', + methodName: 'ClientStreamMethod', + method: grpcClient.clientStreamMethod, + request: requestList, + result: resultSum, + }, + { + description: 'serverStream call', + methodName: 'ServerStreamMethod', + method: grpcClient.serverStreamMethod, + request: resultSum, + result: replicate(resultSum), + }, + { + description: 'bidiStream call', + methodName: 'BidiStreamMethod', + method: grpcClient.bidiStreamMethod, + request: requestList, + result: requestList, + }, + ]; + + const runTest = ( + method: typeof methodList[0], + provider: NodeTracerProvider, + checkSpans = true + ) => { + it(`should ${ + checkSpans ? 'do' : 'not' + }: create a rootSpan for client and a childSpan for server - ${ + method.description + }`, async () => { + const args = [client, method.request]; + await (method.method as any) + .apply({}, args) + .then((result: TestRequestResponse | TestRequestResponse[]) => { + assert.ok( + checkEqual(result)(method.result), + 'gRPC call returns correct values' + ); + const spans = memoryExporter.getFinishedSpans(); + if (checkSpans) { + const incomingSpan = spans[0]; + const outgoingSpan = spans[1]; + const validations = { + name: `grpc.pkg_test.GrpcTester/${method.methodName}`, + status: grpc.status.OK, + }; + + assert.strictEqual(spans.length, 2); + assertSpan( + moduleName, + incomingSpan, + SpanKind.SERVER, + validations + ); + assertSpan( + moduleName, + outgoingSpan, + SpanKind.CLIENT, + validations + ); + assertPropagation(incomingSpan, outgoingSpan); + } else { + assert.strictEqual(spans.length, 0); + } + }); + }); + + it(`should raise an error for client childSpan/server rootSpan - ${method.description} - status = OK`, () => { + const expectEmpty = memoryExporter.getFinishedSpans(); + assert.strictEqual(expectEmpty.length, 0); + + const span = provider + .getTracer('default') + .startSpan('TestSpan', { kind: SpanKind.PRODUCER }); + return provider.getTracer('default').withSpan(span, async () => { + const rootSpan = provider.getTracer('default').getCurrentSpan(); + if (!rootSpan) { + return assert.ok(false); + } + assert.deepStrictEqual(rootSpan, span); + + const args = [client, method.request]; + await (method.method as any) + .apply({}, args) + .then(() => { + // Assert + if (checkSpans) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const serverSpan = spans[0]; + const clientSpan = spans[1]; + const validations = { + name: `grpc.pkg_test.GrpcTester/${method.methodName}`, + status: grpc.status.OK, + }; + assertSpan( + moduleName, + serverSpan, + SpanKind.SERVER, + validations + ); + assertSpan( + moduleName, + clientSpan, + SpanKind.CLIENT, + validations + ); + assertPropagation(serverSpan, clientSpan); + assert.strictEqual( + rootSpan.context().traceId, + serverSpan.spanContext.traceId + ); + assert.strictEqual( + rootSpan.context().spanId, + clientSpan.parentSpanId + ); + } + }) + .catch((err: ServiceError) => { + assert.ok(false, err); + }); + }); + }); + }; + + const insertError = ( + request: TestRequestResponse | TestRequestResponse[] + ) => (code: number) => + request instanceof Array ? [{ num: code }, ...request] : { num: code }; + + const runErrorTest = ( + method: typeof methodList[0], + key: string, + errorCode: number, + provider: NodeTracerProvider + ) => { + it(`should raise an error for client/server rootSpans: method=${method.methodName}, status=${key}`, async () => { + const expectEmpty = memoryExporter.getFinishedSpans(); + assert.strictEqual(expectEmpty.length, 0); + + const args = [client, insertError(method.request)(errorCode)]; + + await (method.method as any) + .apply({}, args) + .then(() => { + assert.ok(false); + }) + .catch((err: ServiceError) => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2, 'Expect 2 ended spans'); + + const validations = { + name: `grpc.pkg_test.GrpcTester/${method.methodName}`, + status: errorCode, + }; + const serverRoot = spans[0]; + const clientRoot = spans[1]; + assertSpan(moduleName, serverRoot, SpanKind.SERVER, validations); + assertSpan(moduleName, clientRoot, SpanKind.CLIENT, validations); + assertPropagation(serverRoot, clientRoot); + }); + }); + + it(`should raise an error for client childSpan/server rootSpan - ${method.description} - status = ${key}`, () => { + const expectEmpty = memoryExporter.getFinishedSpans(); + assert.strictEqual(expectEmpty.length, 0); + + const span = provider + .getTracer('default') + .startSpan('TestSpan', { kind: SpanKind.PRODUCER }); + return provider.getTracer('default').withSpan(span, async () => { + const rootSpan = provider.getTracer('default').getCurrentSpan(); + if (!rootSpan) { + return assert.ok(false); + } + assert.deepStrictEqual(rootSpan, span); + + const args = [client, insertError(method.request)(errorCode)]; + + await (method.method as any) + .apply({}, args) + .then(() => { + assert.ok(false); + }) + .catch((err: ServiceError) => { + // Assert + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const serverSpan = spans[0]; + const clientSpan = spans[1]; + const validations = { + name: `grpc.pkg_test.GrpcTester/${method.methodName}`, + status: errorCode, + }; + assertSpan(moduleName, serverSpan, SpanKind.SERVER, validations); + assertSpan(moduleName, clientSpan, SpanKind.CLIENT, validations); + assertPropagation(serverSpan, clientSpan); + assert.strictEqual( + rootSpan.context().traceId, + serverSpan.spanContext.traceId + ); + assert.strictEqual( + rootSpan.context().spanId, + clientSpan.parentSpanId + ); + }); + }); + }); + }; + + describe('enable()', () => { + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ logger }); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(async () => { + const config = { + // TODO: add plugin options here once supported + }; + const patchedGrpc = plugin.enable(grpc, provider, logger, config); + + const packageDefinition = await protoLoader.load(PROTO_PATH, options); + const proto = patchedGrpc.loadPackageDefinition(packageDefinition) + .pkg_test; + + server = await startServer(patchedGrpc, proto); + client = createClient(patchedGrpc, proto); + }); + + after(done => { + client.close(); + server.tryShutdown(() => { + plugin.disable(); + done(); + }); + }); + + methodList.forEach(method => { + describe(`Test automatic tracing for grpc remote method ${method.description}`, () => { + runTest(method, provider); + }); + }); + + methodList.forEach(method => { + describe(`Test error raising for grpc remote ${method.description}`, () => { + Object.keys(grpc.status).forEach((statusKey: string) => { + const errorCode = Number(grpc.status[statusKey as any]); + if (errorCode > grpc.status.OK) { + runErrorTest(method, statusKey, errorCode, provider); + } + }); + }); + }); + }); + + describe('disable()', () => { + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ logger }); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(async () => { + plugin.enable(grpc, provider, logger); + plugin.disable(); + + const packageDefinition = await protoLoader.load(PROTO_PATH, options); + const proto = grpc.loadPackageDefinition(packageDefinition).pkg_test; + + server = await startServer(grpc, proto); + client = createClient(grpc, proto); + }); + + after(done => { + client.close(); + server.tryShutdown(() => { + done(); + }); + }); + + methodList.map(method => { + describe(`Test automatic tracing for grpc remote method ${method.description}`, () => { + runTest(method, provider, false); + }); + }); + }); + }); +}; diff --git a/packages/opentelemetry-grpc-utils/test/index.ts b/packages/opentelemetry-grpc-utils/test/index.ts new file mode 100644 index 00000000000..169fad3841d --- /dev/null +++ b/packages/opentelemetry-grpc-utils/test/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export * from './grpcUtils.test'; diff --git a/packages/opentelemetry-plugin-grpc/test/utils/assertionUtils.ts b/packages/opentelemetry-grpc-utils/test/utils/assertionUtils.ts similarity index 92% rename from packages/opentelemetry-plugin-grpc/test/utils/assertionUtils.ts rename to packages/opentelemetry-grpc-utils/test/utils/assertionUtils.ts index f1f0a44fa7c..746f930f58e 100644 --- a/packages/opentelemetry-plugin-grpc/test/utils/assertionUtils.ts +++ b/packages/opentelemetry-grpc-utils/test/utils/assertionUtils.ts @@ -16,7 +16,8 @@ import { SpanKind } from '@opentelemetry/api'; import * as assert from 'assert'; -import * as grpc from 'grpc'; +import type * as grpc from 'grpc'; +import type * as grpcJs from '@grpc/grpc-js'; import { ReadableSpan } from '@opentelemetry/tracing'; import { hrTimeToMilliseconds, @@ -24,9 +25,10 @@ import { } from '@opentelemetry/core'; export const assertSpan = ( + component: string, span: ReadableSpan, kind: SpanKind, - validations: { name: string; status: grpc.status } + validations: { name: string; status: grpc.status | grpcJs.status } ) => { assert.strictEqual(span.spanContext.traceId.length, 32); assert.strictEqual(span.spanContext.spanId.length, 16); @@ -34,7 +36,6 @@ export const assertSpan = ( assert.ok(span.endTime); assert.strictEqual(span.links.length, 0); - assert.strictEqual(span.events.length, 1); assert.ok( hrTimeToMicroseconds(span.startTime) < hrTimeToMicroseconds(span.endTime) diff --git a/packages/opentelemetry-grpc-utils/tsconfig.json b/packages/opentelemetry-grpc-utils/tsconfig.json new file mode 100644 index 00000000000..a2042cd68b1 --- /dev/null +++ b/packages/opentelemetry-grpc-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-metrics/package.json b/packages/opentelemetry-metrics/package.json index 58dd2bd1665..2ce7d6b9761 100644 --- a/packages/opentelemetry-metrics/package.json +++ b/packages/opentelemetry-metrics/package.json @@ -42,10 +42,10 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", @@ -53,7 +53,7 @@ "sinon": "9.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-metrics/src/export/aggregators/histogram.ts b/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts similarity index 71% rename from packages/opentelemetry-metrics/src/export/aggregators/histogram.ts rename to packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts index 655afd8ff74..11cd33ee8f8 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/histogram.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/Histogram.ts @@ -23,9 +23,8 @@ import { hrTime } from '@opentelemetry/core'; * and provides the total sum and count of all observations. */ export class HistogramAggregator implements Aggregator { - private _lastCheckpoint: Histogram; - private _currentCheckpoint: Histogram; - private _lastCheckpointTime: HrTime; + private _current: Histogram; + private _lastUpdateTime: HrTime; private readonly _boundaries: number[]; constructor(boundaries: number[]) { @@ -35,36 +34,29 @@ export class HistogramAggregator implements Aggregator { // we need to an ordered set to be able to correctly compute count for each // boundary since we'll iterate on each in order. this._boundaries = boundaries.sort(); - this._lastCheckpoint = this._newEmptyCheckpoint(); - this._lastCheckpointTime = hrTime(); - this._currentCheckpoint = this._newEmptyCheckpoint(); + this._current = this._newEmptyCheckpoint(); + this._lastUpdateTime = hrTime(); } update(value: number): void { - this._currentCheckpoint.count += 1; - this._currentCheckpoint.sum += value; + this._current.count += 1; + this._current.sum += value; for (let i = 0; i < this._boundaries.length; i++) { if (value < this._boundaries[i]) { - this._currentCheckpoint.buckets.counts[i] += 1; + this._current.buckets.counts[i] += 1; return; } } // value is above all observed boundaries - this._currentCheckpoint.buckets.counts[this._boundaries.length] += 1; - } - - reset(): void { - this._lastCheckpointTime = hrTime(); - this._lastCheckpoint = this._currentCheckpoint; - this._currentCheckpoint = this._newEmptyCheckpoint(); + this._current.buckets.counts[this._boundaries.length] += 1; } toPoint(): Point { return { - value: this._lastCheckpoint, - timestamp: this._lastCheckpointTime, + value: this._current, + timestamp: this._lastUpdateTime, }; } diff --git a/packages/opentelemetry-metrics/src/export/aggregators/index.ts b/packages/opentelemetry-metrics/src/export/aggregators/index.ts index 93001f73d7c..9ff9f904ea8 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/index.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export * from './histogram'; +export * from './Histogram'; export * from './MinMaxLastSumCount'; export * from './Sum'; diff --git a/packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts b/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts similarity index 91% rename from packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts rename to packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts index 45f6fe80f65..9a2b43938c4 100644 --- a/packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts +++ b/packages/opentelemetry-metrics/test/export/aggregators/Histogram.test.ts @@ -40,18 +40,9 @@ describe('HistogramAggregator', () => { }); describe('.update()', () => { - it('should not update checkpoint', () => { - const aggregator = new HistogramAggregator([100, 200]); - aggregator.update(150); - const point = aggregator.toPoint().value as Histogram; - assert.equal(point.count, 0); - assert.equal(point.sum, 0); - }); - it('should update the second bucket', () => { const aggregator = new HistogramAggregator([100, 200]); aggregator.update(150); - aggregator.reset(); const point = aggregator.toPoint().value as Histogram; assert.equal(point.count, 1); assert.equal(point.sum, 150); @@ -63,7 +54,6 @@ describe('HistogramAggregator', () => { it('should update the second bucket', () => { const aggregator = new HistogramAggregator([100, 200]); aggregator.update(50); - aggregator.reset(); const point = aggregator.toPoint().value as Histogram; assert.equal(point.count, 1); assert.equal(point.sum, 50); @@ -75,7 +65,6 @@ describe('HistogramAggregator', () => { it('should update the third bucket since value is above all boundaries', () => { const aggregator = new HistogramAggregator([100, 200]); aggregator.update(250); - aggregator.reset(); const point = aggregator.toPoint().value as Histogram; assert.equal(point.count, 1); assert.equal(point.sum, 250); @@ -91,7 +80,6 @@ describe('HistogramAggregator', () => { let point = aggregator.toPoint().value as Histogram; assert.equal(point.count, point.count); aggregator.update(10); - aggregator.reset(); point = aggregator.toPoint().value as Histogram; assert.equal(point.count, 1); assert.equal(point.count, point.count); @@ -104,7 +92,6 @@ describe('HistogramAggregator', () => { let point = aggregator.toPoint().value as Histogram; assert.equal(point.sum, point.sum); aggregator.update(10); - aggregator.reset(); point = aggregator.toPoint().value as Histogram; assert.equal(point.sum, 10); }); @@ -126,7 +113,6 @@ describe('HistogramAggregator', () => { it('should update checkpoint', () => { const aggregator = new HistogramAggregator([100]); aggregator.update(10); - aggregator.reset(); const point = aggregator.toPoint().value as Histogram; assert.equal(point.count, 1); assert.equal(point.sum, 10); @@ -147,7 +133,6 @@ describe('HistogramAggregator', () => { it('should return last checkpoint if updated', () => { const aggregator = new HistogramAggregator([100]); aggregator.update(100); - aggregator.reset(); assert( aggregator .toPoint() diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index e09b88b79fe..19a27beb271 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -44,11 +44,11 @@ "devDependencies": { "@opentelemetry/context-base": "^0.9.0", "@opentelemetry/resources": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/semver": "7.3.1", "@types/shimmer": "1.0.1", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", @@ -56,7 +56,7 @@ "shimmer": "1.2.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-plugin-fetch/package.json b/packages/opentelemetry-plugin-fetch/package.json index 1fc53f2c23b..fbb3fe79428 100644 --- a/packages/opentelemetry-plugin-fetch/package.json +++ b/packages/opentelemetry-plugin-fetch/package.json @@ -44,16 +44,16 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", + "@babel/core": "7.10.5", "@opentelemetry/context-zone": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/shimmer": "1.0.1", "@types/sinon": "7.5.2", "@types/webpack-env": "1.15.2", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -66,10 +66,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "7.5.0", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "webpack-merge": "5.0.9" diff --git a/packages/opentelemetry-plugin-grpc-js/package.json b/packages/opentelemetry-plugin-grpc-js/package.json index 634aed051dd..5377b374f70 100644 --- a/packages/opentelemetry-plugin-grpc-js/package.json +++ b/packages/opentelemetry-plugin-grpc-js/package.json @@ -8,6 +8,7 @@ "repository": "open-telemetry/opentelemetry-js", "scripts": { "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", + "test:deubg": "ts-mocha --inspect-brk -p tsconfig.json test/**/*.test.ts", "tdd": "npm run test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "lint": "eslint . --ext .ts", @@ -47,14 +48,15 @@ "@grpc/grpc-js": "1.1.2", "@opentelemetry/context-async-hooks": "^0.9.0", "@opentelemetry/context-base": "^0.9.0", + "@opentelemetry/grpc-utils": "^0.9.0", "@opentelemetry/node": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/semver": "7.3.1", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", @@ -63,11 +65,12 @@ "sinon": "9.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", "@opentelemetry/core": "^0.9.0", + "@opentelemetry/semantic-conventions": "^0.9.0", "shimmer": "1.2.1" } } diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/index.ts b/packages/opentelemetry-plugin-grpc-js/src/client/index.ts new file mode 100644 index 00000000000..549cc29a6a4 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/client/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './loadPackageDefinition'; +export * from './patchClient'; diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts b/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts new file mode 100644 index 00000000000..46f0602cfa7 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/client/loadPackageDefinition.ts @@ -0,0 +1,69 @@ +/* + * 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 { GrpcJsPlugin } from '../grpcJs'; +import type * as grpcJs from '@grpc/grpc-js'; +import type { PackageDefinition } from '@grpc/grpc-js/build/src/make-client'; +import * as shimmer from 'shimmer'; +import { getMethodsToWrap, getPatchedClientMethods } from './utils'; + +/** + * Entry point for client patching for grpc.loadPackageDefinition(...) + * @param this - GrpcJsPlugin + */ +export function patchLoadPackageDefinition(this: GrpcJsPlugin) { + return (original: typeof grpcJs.loadPackageDefinition) => { + const plugin = this; + + plugin._logger.debug('patching loadPackageDefinition'); + + return function patchedLoadPackageDefinition( + this: null, + packageDef: PackageDefinition + ) { + const result: grpcJs.GrpcObject = original.call( + this, + packageDef + ) as grpcJs.GrpcObject; + _patchLoadedPackage.call(plugin, result); + return result; + } as typeof grpcJs.loadPackageDefinition; + }; +} + +/** + * Utility function to patch *all* functions loaded through a proto file. + * Recursively searches for Client classes and patches all methods, reversing the + * parsing done by grpc.loadPackageDefinition + * https://github.com/grpc/grpc-node/blob/1d14203c382509c3f36132bd0244c99792cb6601/packages/grpc-js/src/make-client.ts#L200-L217 + */ +function _patchLoadedPackage( + this: GrpcJsPlugin, + result: grpcJs.GrpcObject +): void { + Object.values(result).forEach(service => { + if (typeof service === 'function') { + shimmer.massWrap( + service.prototype, + getMethodsToWrap(service, service.service), + getPatchedClientMethods.call(this) + ); + } else if (typeof service.format !== 'string') { + // GrpcObject + _patchLoadedPackage.call(this, service as grpcJs.GrpcObject); + } + }); +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts b/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts new file mode 100644 index 00000000000..70de6e9d1db --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/client/patchClient.ts @@ -0,0 +1,49 @@ +/* + * 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 { GrpcJsPlugin } from '../grpcJs'; +import type * as grpcJs from '@grpc/grpc-js'; +import * as shimmer from 'shimmer'; +import { getMethodsToWrap, getPatchedClientMethods } from './utils'; + +type MakeClientConstructorFunction = typeof grpcJs.makeGenericClientConstructor; + +/** + * Entry point for applying client patches to `grpc.makeClientConstructor(...)` equivalents + * @param this GrpcJsPlugin + */ +export function patchClient( + this: GrpcJsPlugin +): (original: MakeClientConstructorFunction) => MakeClientConstructorFunction { + const plugin = this; + return (original: MakeClientConstructorFunction) => { + plugin._logger.debug('patching client'); + return function makeClientConstructor( + this: typeof grpcJs.Client, + methods: grpcJs.ServiceDefinition, + serviceName: string, + options?: object + ) { + const client = original.call(this, methods, serviceName, options); + shimmer.massWrap( + client.prototype, + getMethodsToWrap(client, methods), + getPatchedClientMethods.call(plugin) + ); + return client; + }; + }; +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts b/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts new file mode 100644 index 00000000000..f1b177ccf6e --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/client/utils.ts @@ -0,0 +1,244 @@ +/* + * 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 { GrpcJsPlugin } from '../grpcJs'; +import type { GrpcClientFunc, SendUnaryDataCallback } from '../types'; +import { + SpanKind, + Span, + CanonicalCode, + Status, + propagation, +} from '@opentelemetry/api'; +import { RpcAttribute } from '@opentelemetry/semantic-conventions'; +import type * as grpcJs from '@grpc/grpc-js'; +import { + grpcStatusCodeToSpanStatus, + grpcStatusCodeToCanonicalCode, + CALL_SPAN_ENDED, +} from '../utils'; +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( + 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 }]) => { + 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; +} + +/** + * Parse initial client call properties and start a span to trace its execution + */ +export function getPatchedClientMethods( + this: GrpcJsPlugin +): (original: GrpcClientFunc) => () => EventEmitter { + const plugin = this; + return (original: GrpcClientFunc) => { + plugin._logger.debug('patch all client methods'); + return function clientMethodTrace(this: grpcJs.Client) { + const name = `grpc.${original.path.replace('/', '')}`; + const args = [...arguments]; + const span = plugin.tracer.startSpan(name, { + kind: SpanKind.CLIENT, + }); + return plugin.tracer.withSpan(span, () => + makeGrpcClientRemoteCall(original, args, this, plugin)(span) + ); + }; + }; +} + +/** + * 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[], + self: grpcJs.Client, + plugin: GrpcJsPlugin +): (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 + ) { + const wrappedFn: SendUnaryDataCallback = ( + err: grpcJs.ServiceError | null, + res + ) => { + 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: CanonicalCode.OK }); + span.setAttribute( + RpcAttribute.GRPC_STATUS_CODE, + CanonicalCode.OK.toString() + ); + } + + span.end(); + callback(err, res); + }; + return plugin.tracer.bind(wrappedFn); + } + + return (span: Span) => { + const metadata = getMetadata.call(plugin, original, args); + // 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 + ); + } + } + + 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; + } + }; + plugin.tracer.bind(call); + call.on('error', (err: grpcJs.ServiceError) => { + if (call[CALL_SPAN_ENDED]) { + return; + } + call[CALL_SPAN_ENDED] = true; + + span.setStatus({ + code: grpcStatusCodeToCanonicalCode(err.code), + message: err.message, + }); + span.setAttributes({ + [RpcAttribute.GRPC_ERROR_NAME]: err.name, + [RpcAttribute.GRPC_ERROR_MESSAGE]: err.message, + }); + + endSpan(); + }); + + call.on('status', (status: Status) => { + 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`) + */ +function getMetadata( + this: GrpcJsPlugin, + original: GrpcClientFunc, + args: Array +): 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 this._moduleExports.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(metadata, (metadata, k, v) => + metadata.set(k, v as grpcJs.MetadataValue) + ); +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts b/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts index 6a2c6e89f86..e3ec95db0c5 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/grpcJs.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import type * as grpcJs from '@grpc/grpc-js'; import { BasePlugin } from '@opentelemetry/core'; +import * as shimmer from 'shimmer'; +import { patchClient, patchLoadPackageDefinition } from './client'; +import { patchServer } from './server'; import { VERSION } from './version'; -import * as path from 'path'; - -import * as grpcJs from '@grpc/grpc-js'; +import { Tracer, Logger } from '@opentelemetry/api'; /** * @grpc/grpc-js gRPC instrumentation plugin for Opentelemetry @@ -26,20 +28,74 @@ import * as grpcJs from '@grpc/grpc-js'; */ export class GrpcJsPlugin extends BasePlugin { static readonly component = '@grpc/grpc-js'; + readonly supportedVersions = ['1.*']; - constructor(readonly moduleName: string, readonly version: string) { + constructor(readonly moduleName: string) { super('@opentelemetry/plugin-grpc-js', VERSION); } + /** + * @internal + * Public reference to the protected BasePlugin `_tracer` instance to be used by this + * plugin's external helper functions + */ + get tracer(): Tracer { + return this._tracer; + } + + /** + * @internal + * Public reference to the protected BasePlugin `_logger` instance to be used by this + * plugin's external helper functions + */ + get logger(): Logger { + return this._logger; + } + protected patch(): typeof grpcJs { - throw new Error('Method not implemented.'); + // Patch Server methods + shimmer.wrap( + this._moduleExports.Server.prototype, + 'register', + patchServer.call(this) + ); + + // Patch Client methods + shimmer.wrap( + this._moduleExports, + 'makeClientConstructor', + patchClient.call(this) + ); + shimmer.wrap( + this._moduleExports, + 'makeGenericClientConstructor', + patchClient.call(this) + ); + shimmer.wrap( + this._moduleExports, + 'loadPackageDefinition', + patchLoadPackageDefinition.call(this) + ); + + return this._moduleExports; } + protected unpatch(): void { - throw new Error('Method not implemented.'); + this._logger.debug( + 'removing patch to %s@%s', + this.moduleName, + this.version + ); + + // Unpatch server + shimmer.unwrap(this._moduleExports.Server.prototype, 'register'); + + // Unpatch client + shimmer.unwrap(this._moduleExports, 'makeClientConstructor'); + shimmer.unwrap(this._moduleExports, 'makeGenericClientConstructor'); + shimmer.unwrap(this._moduleExports, 'loadPackageDefinition'); } } -const basedir = path.dirname(require.resolve(GrpcJsPlugin.component)); -const version = require(path.join(basedir, 'package.json')).version; -export const plugin = new GrpcJsPlugin(GrpcJsPlugin.component, version); +export const plugin = new GrpcJsPlugin(GrpcJsPlugin.component); diff --git a/packages/opentelemetry-plugin-grpc-js/src/server/clientStreamAndUnary.ts b/packages/opentelemetry-plugin-grpc-js/src/server/clientStreamAndUnary.ts new file mode 100644 index 00000000000..d8398134273 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/server/clientStreamAndUnary.ts @@ -0,0 +1,66 @@ +/* + * 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 { Span, CanonicalCode } from '@opentelemetry/api'; +import type { ServerCallWithMeta, SendUnaryDataCallback } from '../types'; +import { grpcStatusCodeToCanonicalCode } from '../utils'; +import { RpcAttribute } from '@opentelemetry/semantic-conventions'; +import type { GrpcJsPlugin } from '../grpcJs'; +import type * as grpcJs from '@grpc/grpc-js'; + +/** + * Handle patching for clientStream and unary type server handlers + */ +export function clientStreamAndUnaryHandler( + plugin: GrpcJsPlugin, + span: Span, + call: ServerCallWithMeta, + callback: SendUnaryDataCallback, + original: + | grpcJs.handleUnaryCall + | grpcJs.ClientReadableStream +): void { + const patchedCallback: SendUnaryDataCallback = ( + err: grpcJs.ServiceError | null, + value?: ResponseType + ) => { + if (err) { + if (err.code) { + span.setStatus({ + code: grpcStatusCodeToCanonicalCode(err.code), + message: err.message, + }); + 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: CanonicalCode.OK }); + span.setAttribute( + RpcAttribute.GRPC_STATUS_CODE, + CanonicalCode.OK.toString() + ); + } + + span.end(); + return callback(err, value); + }; + + plugin.tracer.bind(call); + return (original as Function).call({}, call, patchedCallback); +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/server/index.ts b/packages/opentelemetry-plugin-grpc-js/src/server/index.ts new file mode 100644 index 00000000000..99bb1743c10 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/server/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './patchServer'; diff --git a/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts b/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts new file mode 100644 index 00000000000..f75dd294a5a --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/server/patchServer.ts @@ -0,0 +1,154 @@ +/* + * 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 type * as grpcJs from '@grpc/grpc-js'; +import type { HandleCall } from '@grpc/grpc-js/build/src/server-call'; +import { GrpcJsPlugin } from '../grpcJs'; +import * as shimmer from 'shimmer'; +import { ServerCallWithMeta, SendUnaryDataCallback } from '../types'; +import { + context, + SpanOptions, + SpanKind, + propagation, + Span, +} from '@opentelemetry/api'; +import { RpcAttribute } from '@opentelemetry/semantic-conventions'; +import { clientStreamAndUnaryHandler } from './clientStreamAndUnary'; +import { serverStreamAndBidiHandler } from './serverStreamAndBidi'; + +type ServerRegisterFunction = typeof grpcJs.Server.prototype.register; + +/** + * Patch for grpc.Server.prototype.register(...) function. Provides auto-instrumentation for + * client_stream, server_stream, bidi, unary server handler calls. + */ +export function patchServer( + this: GrpcJsPlugin +): (originalRegister: ServerRegisterFunction) => ServerRegisterFunction { + return (originalRegister: ServerRegisterFunction) => { + const plugin = this; + + plugin.logger.debug('patched gRPC server'); + return function register( + this: grpcJs.Server, + name: string, + handler: HandleCall, + serialize: grpcJs.serialize, + deserialize: grpcJs.deserialize, + type: string + ): boolean { + const originalRegisterResult = originalRegister.call( + this, + name, + handler, + serialize, + deserialize, + type + ); + const handlerSet = this['handlers'].get(name); + + shimmer.wrap( + handlerSet, + 'func', + (originalFunc: HandleCall) => { + return function func( + this: typeof handlerSet, + call: ServerCallWithMeta, + callback: SendUnaryDataCallback + ) { + const self = this; + + const spanName = `grpc.${name.replace('/', '')}`; + const spanOptions: SpanOptions = { + kind: SpanKind.SERVER, + }; + + plugin.logger.debug('patch func: %s', JSON.stringify(spanOptions)); + + context.with( + propagation.extract(call.metadata, (carrier, key) => + carrier.get(key) + ), + () => { + const span = plugin.tracer + .startSpan(spanName, spanOptions) + .setAttributes({ + [RpcAttribute.GRPC_KIND]: spanOptions.kind, + }); + + plugin.tracer.withSpan(span, () => { + handleServerFunction.call( + self, + plugin, + span, + type, + originalFunc, + call, + callback + ); + }); + } + ); + }; + } + ); + return originalRegisterResult; + } as typeof grpcJs.Server.prototype.register; + }; +} + +/** + * Patch callback or EventEmitter provided by `originalFunc` and set appropriate `span` + * properties based on its result. + */ +function handleServerFunction( + this: unknown, + plugin: GrpcJsPlugin, + span: Span, + type: string, + originalFunc: HandleCall, + call: ServerCallWithMeta, + callback: SendUnaryDataCallback +): void { + switch (type) { + case 'unary': + case 'clientStream': + case 'client_stream': + return clientStreamAndUnaryHandler( + plugin, + span, + call, + callback, + originalFunc as + | grpcJs.handleUnaryCall + | grpcJs.ClientReadableStream + ); + case 'serverStream': + case 'server_stream': + case 'bidi': + return serverStreamAndBidiHandler( + plugin, + span, + call, + originalFunc as + | grpcJs.handleBidiStreamingCall + | grpcJs.handleServerStreamingCall + ); + default: + break; + } +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/server/serverStreamAndBidi.ts b/packages/opentelemetry-plugin-grpc-js/src/server/serverStreamAndBidi.ts new file mode 100644 index 00000000000..1fe8a14a44e --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/server/serverStreamAndBidi.ts @@ -0,0 +1,86 @@ +/* + * 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 { Span, CanonicalCode } from '@opentelemetry/api'; +import { RpcAttribute } from '@opentelemetry/semantic-conventions'; +import type * as grpcJs from '@grpc/grpc-js'; +import type { GrpcJsPlugin } from '../grpcJs'; +import { GrpcEmitter } from '../types'; +import { CALL_SPAN_ENDED, grpcStatusCodeToCanonicalCode } from '../utils'; + +/** + * Handle patching for serverStream and Bidi type server handlers + */ +export function serverStreamAndBidiHandler( + plugin: GrpcJsPlugin, + span: Span, + call: GrpcEmitter, + original: + | grpcJs.handleBidiStreamingCall + | grpcJs.handleServerStreamingCall +): void { + let spanEnded = false; + const endSpan = () => { + if (!spanEnded) { + spanEnded = true; + span.end(); + } + }; + + plugin.tracer.bind(call); + call.on('finish', () => { + // @grpc/js does not expose a way to check if this call also emitted an error, + // e.g. call.status.code !== 0 + if (call[CALL_SPAN_ENDED]) { + return; + } + + // Set the "grpc call had an error" flag + call[CALL_SPAN_ENDED] = true; + + span.setStatus({ + code: CanonicalCode.OK, + }); + span.setAttribute( + RpcAttribute.GRPC_STATUS_CODE, + CanonicalCode.OK.toString() + ); + + endSpan(); + }); + + call.on('error', (err: grpcJs.ServiceError) => { + if (call[CALL_SPAN_ENDED]) { + return; + } + + // Set the "grpc call had an error" flag + call[CALL_SPAN_ENDED] = true; + + span.setStatus({ + code: grpcStatusCodeToCanonicalCode(err.code), + message: err.message, + }); + span.setAttributes({ + [RpcAttribute.GRPC_ERROR_NAME]: err.name, + [RpcAttribute.GRPC_ERROR_MESSAGE]: err.message, + }); + endSpan(); + }); + + // Types of parameters 'call' and 'call' are incompatible. + return (original as Function).call({}, call); +} diff --git a/packages/opentelemetry-plugin-grpc-js/src/types.ts b/packages/opentelemetry-plugin-grpc-js/src/types.ts new file mode 100644 index 00000000000..a41dbf48d35 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/types.ts @@ -0,0 +1,54 @@ +/* + * 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 type * as grpcJs from '@grpc/grpc-js'; +import type { EventEmitter } from 'events'; +import type { CALL_SPAN_ENDED } from './utils'; + +/** + * Server Unary callback type + */ +export type SendUnaryDataCallback = grpcJs.requestCallback; + +/** + * Intersection type of all grpc server call types + */ +export type ServerCall = + | grpcJs.ServerUnaryCall + | grpcJs.ServerReadableStream + | grpcJs.ServerWritableStream + | grpcJs.ServerDuplexStream; + +/** + * {@link ServerCall} ServerCall extended with misc. missing utility types + */ +export type ServerCallWithMeta = ServerCall & { + metadata: grpcJs.Metadata; +}; + +/** + * EventEmitter with span ended symbol indicator + */ +export type GrpcEmitter = EventEmitter & { [CALL_SPAN_ENDED]?: boolean }; + +/** + * Grpc client callback function extended with missing utility types + */ +export type GrpcClientFunc = ((...args: unknown[]) => GrpcEmitter) & { + path: string; + requestStream: boolean; + responseStream: boolean; +}; diff --git a/packages/opentelemetry-plugin-grpc-js/src/utils.ts b/packages/opentelemetry-plugin-grpc-js/src/utils.ts new file mode 100644 index 00000000000..fcae2564423 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/src/utils.ts @@ -0,0 +1,46 @@ +/* + * 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 { CanonicalCode, Status } from '@opentelemetry/api'; +import type * as grpcTypes from '@grpc/grpc-js'; // For types only + +/** + * Symbol to include on grpc call if it has already emitted an error event. + * grpc events that emit 'error' will also emit 'finish' and so only the + * error event should be processed. + */ +export const CALL_SPAN_ENDED = Symbol('opentelemetry call span ended'); + +/** + * Convert a grpc status code to an opentelemetry Canonical code. For now, the enums are exactly the same + * @param status + */ +export const grpcStatusCodeToCanonicalCode = ( + status?: grpcTypes.status +): CanonicalCode => { + if (status !== 0 && !status) { + return CanonicalCode.UNKNOWN; + } + return status as number; +}; + +/** + * Convert grpc status code to an opentelemetry Status object. + * @param status + */ +export const grpcStatusCodeToSpanStatus = (status: number): Status => { + return { code: status }; +}; diff --git a/packages/opentelemetry-plugin-grpc-js/test/grpcJs.test.ts b/packages/opentelemetry-plugin-grpc-js/test/grpcJs.test.ts new file mode 100644 index 00000000000..7faf3e8b883 --- /dev/null +++ b/packages/opentelemetry-plugin-grpc-js/test/grpcJs.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { runTests } from '@opentelemetry/grpc-utils/test'; +import { plugin, GrpcJsPlugin } from '../src/grpcJs'; +import * as grpc from '@grpc/grpc-js'; + +describe(`#${GrpcJsPlugin.component}`, () => { + runTests(plugin, GrpcJsPlugin.component, grpc, 12346); +}); diff --git a/packages/opentelemetry-plugin-grpc/package.json b/packages/opentelemetry-plugin-grpc/package.json index 5f08b153008..f75165e7a4f 100644 --- a/packages/opentelemetry-plugin-grpc/package.json +++ b/packages/opentelemetry-plugin-grpc/package.json @@ -44,14 +44,15 @@ "devDependencies": { "@opentelemetry/context-async-hooks": "^0.9.0", "@opentelemetry/context-base": "^0.9.0", + "@opentelemetry/grpc-utils": "^0.9.0", "@opentelemetry/node": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/semver": "7.3.1", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", - "codecov": "3.7.0", + "codecov": "3.7.1", "grpc": "1.24.3", "gts": "2.0.2", "mocha": "7.2.0", @@ -62,7 +63,7 @@ "sinon": "9.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-plugin-grpc/src/grpc.ts b/packages/opentelemetry-plugin-grpc/src/grpc.ts index c9cbe1fa339..133757f7c27 100644 --- a/packages/opentelemetry-plugin-grpc/src/grpc.ts +++ b/packages/opentelemetry-plugin-grpc/src/grpc.ts @@ -329,17 +329,23 @@ export class GrpcPlugin extends BasePlugin { client: typeof grpcTypes.Client, methods: { [key: string]: { originalName?: string } } ): string[] { - const methodsToWrap = [ - ...Object.keys(methods), - ...(Object.keys(methods) - .map(methodName => methods[methodName].originalName) - .filter( - originalName => - // eslint-disable-next-line no-prototype-builtins - !!originalName && client.prototype.hasOwnProperty(originalName) - ) as string[]), - ]; - return methodsToWrap; + const methodList: string[] = []; + + // For a method defined in .proto as "UnaryMethod" + Object.entries(methods).forEach(([name, { originalName }]) => { + 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; } private _getPatchedClientMethods() { diff --git a/packages/opentelemetry-plugin-grpc/test/grpc.test.ts b/packages/opentelemetry-plugin-grpc/test/grpc.test.ts index ac113b0e010..a4ff260c2a2 100644 --- a/packages/opentelemetry-plugin-grpc/test/grpc.test.ts +++ b/packages/opentelemetry-plugin-grpc/test/grpc.test.ts @@ -14,620 +14,10 @@ * limitations under the License. */ -import { - context, - NoopTracerProvider, - SpanKind, - propagation, -} from '@opentelemetry/api'; -import { NoopLogger, HttpTraceContext } from '@opentelemetry/core'; -import { NodeTracerProvider } from '@opentelemetry/node'; -import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; -import { ContextManager } from '@opentelemetry/context-base'; -import { - InMemorySpanExporter, - SimpleSpanProcessor, -} from '@opentelemetry/tracing'; -import * as assert from 'assert'; +import { runTests } from '@opentelemetry/grpc-utils/test'; +import { plugin, GrpcPlugin } from '../src/grpc'; import * as grpc from 'grpc'; -import * as semver from 'semver'; -import * as sinon from 'sinon'; -import { GrpcPlugin, plugin } from '../src'; -import { SendUnaryDataCallback } from '../src/types'; -import { assertPropagation, assertSpan } from './utils/assertionUtils'; -const PROTO_PATH = __dirname + '/fixtures/grpc-test.proto'; -const memoryExporter = new InMemorySpanExporter(); - -type GrpcModule = typeof grpc; -const MAX_ERROR_STATUS = grpc.status.UNAUTHENTICATED; - -interface TestRequestResponse { - num: number; -} - -type TestGrpcClient = grpc.Client & { - unaryMethod: any; - UnaryMethod: any; - clientStreamMethod: any; - serverStreamMethod: any; - bidiStreamMethod: any; -}; - -// Compare two arrays using an equal function f -const arrayIsEqual = (f: any) => ([x, ...xs]: any) => ([y, ...ys]: any): any => - x === undefined && y === undefined - ? true - : Boolean(f(x)(y)) && arrayIsEqual(f)(xs)(ys); - -// Return true if two requests has the same num value -const requestEqual = (x: TestRequestResponse) => (y: TestRequestResponse) => - x.num !== undefined && x.num === y.num; - -// Check if its equal requests or array of requests -const checkEqual = (x: TestRequestResponse | TestRequestResponse[]) => ( - y: TestRequestResponse | TestRequestResponse[] -) => - x instanceof Array && y instanceof Array - ? arrayIsEqual(requestEqual)(x as any)(y as any) - : !(x instanceof Array) && !(y instanceof Array) - ? requestEqual(x)(y) - : false; - -const grpcClient = { - unaryMethod: ( - client: TestGrpcClient, - request: TestRequestResponse - ): Promise => { - return new Promise((resolve, reject) => { - return client.unaryMethod( - request, - (err: grpc.ServiceError, response: TestRequestResponse) => { - if (err) { - reject(err); - } else { - resolve(response); - } - } - ); - }); - }, - - UnaryMethod: ( - client: TestGrpcClient, - request: TestRequestResponse - ): Promise => { - return new Promise((resolve, reject) => { - return client.UnaryMethod( - request, - (err: grpc.ServiceError, response: TestRequestResponse) => { - if (err) { - reject(err); - } else { - resolve(response); - } - } - ); - }); - }, - - clientStreamMethod: ( - client: TestGrpcClient, - request: TestRequestResponse[] - ): Promise => { - return new Promise((resolve, reject) => { - const writeStream = client.clientStreamMethod( - (err: grpc.ServiceError, response: TestRequestResponse) => { - if (err) { - reject(err); - } else { - resolve(response); - } - } - ); - - request.forEach(element => { - writeStream.write(element); - }); - writeStream.end(); - }); - }, - - serverStreamMethod: ( - client: TestGrpcClient, - request: TestRequestResponse - ): Promise => { - return new Promise((resolve, reject) => { - const result: TestRequestResponse[] = []; - const readStream = client.serverStreamMethod(request); - - readStream.on('data', (data: TestRequestResponse) => { - result.push(data); - }); - readStream.on('error', (err: grpc.ServiceError) => { - reject(err); - }); - readStream.on('end', () => { - resolve(result); - }); - }); - }, - - bidiStreamMethod: ( - client: TestGrpcClient, - request: TestRequestResponse[] - ): Promise => { - return new Promise((resolve, reject) => { - const result: TestRequestResponse[] = []; - const bidiStream = client.bidiStreamMethod([]); - - bidiStream.on('data', (data: TestRequestResponse) => { - result.push(data); - }); - - request.forEach(element => { - bidiStream.write(element); - }); - - bidiStream.on('error', (err: grpc.ServiceError) => { - reject(err); - }); - - bidiStream.on('end', () => { - resolve(result); - }); - - bidiStream.end(); - }); - }, -}; - -let server: grpc.Server; -let client: grpc.Client; -const grpcPort = 12345; - -const replicate = (request: TestRequestResponse) => { - const result: TestRequestResponse[] = []; - for (let i = 0; i < request.num; i++) { - result.push(request); - } - return result; -}; - -function startServer(grpc: GrpcModule, proto: any) { - const server = new grpc.Server(); - - function getError(msg: string, code: number): grpc.ServiceError { - const err: grpc.ServiceError = new Error(msg); - err.name = msg; - err.message = msg; - err.code = code; - return err; - } - - server.addService(proto.GrpcTester.service, { - // An error is emitted every time - // request.num <= MAX_ERROR_STATUS = (status.UNAUTHENTICATED) - // in those cases, erro.code = request.num - - // This method returns the request - unaryMethod( - call: grpc.ServerUnaryCall, - callback: SendUnaryDataCallback - ) { - call.request.num <= MAX_ERROR_STATUS - ? callback(getError('Unary Method Error', call.request.num)) - : callback(null, { num: call.request.num }); - }, - - // This method sum the requests - clientStreamMethod( - call: grpc.ServerReadableStream, - callback: SendUnaryDataCallback - ) { - let sum = 0; - let hasError = false; - let code = grpc.status.OK; - call.on('data', (data: TestRequestResponse) => { - sum += data.num; - if (data.num <= MAX_ERROR_STATUS) { - hasError = true; - code = data.num; - } - }); - call.on('end', () => { - hasError - ? callback(getError('Client Stream Method Error', code)) - : callback(null, { num: sum }); - }); - }, - - // This method returns an array that replicates the request, request.num of - // times - serverStreamMethod: (call: grpc.ServerWriteableStream) => { - const result = replicate(call.request); - - if (call.request.num <= MAX_ERROR_STATUS) { - call.emit( - 'error', - getError('Server Stream Method Error', call.request.num) - ); - } else { - result.forEach(element => { - call.write(element); - }); - } - call.end(); - }, - - // This method returns the request - bidiStreamMethod: (call: grpc.ServerDuplexStream) => { - call.on('data', (data: TestRequestResponse) => { - if (data.num <= MAX_ERROR_STATUS) { - call.emit('error', getError('Server Stream Method Error', data.num)); - } else { - call.write(data); - } - }); - call.on('end', () => { - call.end(); - }); - }, - }); - server.bind('localhost:' + grpcPort, grpc.ServerCredentials.createInsecure()); - server.start(); - return server; -} - -function createClient(grpc: GrpcModule, proto: any) { - return new proto.GrpcTester( - 'localhost:' + grpcPort, - grpc.credentials.createInsecure() - ); -} - -describe('GrpcPlugin', () => { - let contextManager: ContextManager; - - before(() => { - propagation.setGlobalPropagator(new HttpTraceContext()); - }); - - beforeEach(() => { - contextManager = new AsyncHooksContextManager().enable(); - context.setGlobalContextManager(contextManager); - }); - - afterEach(() => { - context.disable(); - }); - - it('should return a plugin', () => { - assert.ok(plugin instanceof GrpcPlugin); - }); - - it('should match version', () => { - assert.ok(semver.satisfies(plugin.version, '^1.23.3')); - }); - - it('moduleName should be grpc', () => { - assert.deepStrictEqual('grpc', plugin.moduleName); - }); - - describe('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => { - const clientPatchStub = sinon.stub( - plugin, - '_getPatchedClientMethods' as never - ); - after(() => { - clientPatchStub.restore(); - plugin.disable(); - }); - - it('should patch client constructor makeClientConstructor() and makeGenericClientConstructor()', () => { - plugin.enable(grpc, new NoopTracerProvider(), new NoopLogger()); - (plugin['_moduleExports'] as any).makeGenericClientConstructor({}); - assert.strictEqual(clientPatchStub.callCount, 1); - }); - }); - - const requestList: TestRequestResponse[] = [{ num: 100 }, { num: 50 }]; - const resultSum = { - num: requestList.reduce((sum, x) => { - return sum + x.num; - }, 0), - }; - const methodList = [ - { - description: 'unary call', - methodName: 'UnaryMethod', - method: grpcClient.unaryMethod, - request: requestList[0], - result: requestList[0], - }, - { - description: 'Unary call', - methodName: 'UnaryMethod', - method: grpcClient.UnaryMethod, - request: requestList[0], - result: requestList[0], - }, - { - description: 'clientStream call', - methodName: 'ClientStreamMethod', - method: grpcClient.clientStreamMethod, - request: requestList, - result: resultSum, - }, - { - description: 'serverStream call', - methodName: 'ServerStreamMethod', - method: grpcClient.serverStreamMethod, - request: resultSum, - result: replicate(resultSum), - }, - { - description: 'bidiStream call', - methodName: 'BidiStreamMethod', - method: grpcClient.bidiStreamMethod, - request: requestList, - result: requestList, - }, - ]; - - const runTest = ( - method: typeof methodList[0], - provider: NodeTracerProvider, - checkSpans = true - ) => { - it(`should ${ - checkSpans ? 'do' : 'not' - }: create a rootSpan for client and a childSpan for server - ${ - method.description - }`, async () => { - const args = [client, method.request]; - await (method.method as any) - .apply({}, args) - .then((result: TestRequestResponse | TestRequestResponse[]) => { - assert.ok( - checkEqual(result)(method.result), - 'gRPC call returns correct values' - ); - const spans = memoryExporter.getFinishedSpans(); - if (checkSpans) { - const incomingSpan = spans[0]; - const outgoingSpan = spans[1]; - const validations = { - name: `grpc.pkg_test.GrpcTester/${method.methodName}`, - status: grpc.status.OK, - }; - - assert.strictEqual(spans.length, 2); - assertSpan(incomingSpan, SpanKind.SERVER, validations); - assertSpan(outgoingSpan, SpanKind.CLIENT, validations); - assertPropagation(incomingSpan, outgoingSpan); - } else { - assert.strictEqual(spans.length, 0); - } - }); - }); - - it(`should raise an error for client childSpan/server rootSpan - ${method.description} - status = OK`, () => { - const expectEmpty = memoryExporter.getFinishedSpans(); - assert.strictEqual(expectEmpty.length, 0); - - const span = provider - .getTracer('default') - .startSpan('TestSpan', { kind: SpanKind.PRODUCER }); - return provider.getTracer('default').withSpan(span, async () => { - const rootSpan = provider.getTracer('default').getCurrentSpan(); - if (!rootSpan) { - return assert.ok(false); - } - assert.deepStrictEqual(rootSpan, span); - - const args = [client, method.request]; - await (method.method as any) - .apply({}, args) - .then(() => { - // Assert - if (checkSpans) { - const spans = memoryExporter.getFinishedSpans(); - assert.strictEqual(spans.length, 2); - const serverSpan = spans[0]; - const clientSpan = spans[1]; - const validations = { - name: `grpc.pkg_test.GrpcTester/${method.methodName}`, - status: grpc.status.OK, - }; - assertSpan(serverSpan, SpanKind.SERVER, validations); - assertSpan(clientSpan, SpanKind.CLIENT, validations); - assertPropagation(serverSpan, clientSpan); - assert.strictEqual( - rootSpan.context().traceId, - serverSpan.spanContext.traceId - ); - assert.strictEqual( - rootSpan.context().spanId, - clientSpan.parentSpanId - ); - } - }) - .catch((err: grpc.ServiceError) => { - assert.ok(false, err); - }); - }); - }); - }; - - const insertError = ( - request: TestRequestResponse | TestRequestResponse[] - ) => (code: number) => - request instanceof Array - ? request.splice(0, 0, { num: code }) && request.slice(0, request.length) - : { num: code }; - - const runErrorTest = ( - method: typeof methodList[0], - key: string, - errorCode: number, - provider: NodeTracerProvider - ) => { - it(`should raise an error for client/server rootSpans: method=${method.methodName}, status=${key}`, async () => { - const expectEmpty = memoryExporter.getFinishedSpans(); - assert.strictEqual(expectEmpty.length, 0); - - const errRequest = - method.request instanceof Array - ? method.request.slice(0, method.request.length) - : method.request; - const args = [client, insertError(errRequest)(errorCode)]; - - await (method.method as any) - .apply({}, args) - .then(() => { - assert.ok(false); - }) - .catch((err: grpc.ServiceError) => { - const spans = memoryExporter.getFinishedSpans(); - assert.strictEqual(spans.length, 2, 'Expect 2 ended spans'); - - const validations = { - name: `grpc.pkg_test.GrpcTester/${method.methodName}`, - status: errorCode, - }; - const serverRoot = spans[0]; - const clientRoot = spans[1]; - assertSpan(serverRoot, SpanKind.SERVER, validations); - assertSpan(clientRoot, SpanKind.CLIENT, validations); - assertPropagation(serverRoot, clientRoot); - }); - }); - - it(`should raise an error for client childSpan/server rootSpan - ${method.description} - status = ${key}`, () => { - const expectEmpty = memoryExporter.getFinishedSpans(); - assert.strictEqual(expectEmpty.length, 0); - - const span = provider - .getTracer('default') - .startSpan('TestSpan', { kind: SpanKind.PRODUCER }); - return provider.getTracer('default').withSpan(span, async () => { - const rootSpan = provider.getTracer('default').getCurrentSpan(); - if (!rootSpan) { - return assert.ok(false); - } - assert.deepStrictEqual(rootSpan, span); - - const errRequest = - method.request instanceof Array - ? method.request.slice(0, method.request.length) - : method.request; - const args = [client, insertError(errRequest)(errorCode)]; - - await (method.method as any) - .apply({}, args) - .then(() => { - assert.ok(false); - }) - .catch((err: grpc.ServiceError) => { - // Assert - const spans = memoryExporter.getFinishedSpans(); - assert.strictEqual(spans.length, 2); - const serverSpan = spans[0]; - const clientSpan = spans[1]; - const validations = { - name: `grpc.pkg_test.GrpcTester/${method.methodName}`, - status: errorCode, - }; - assertSpan(serverSpan, SpanKind.SERVER, validations); - assertSpan(clientSpan, SpanKind.CLIENT, validations); - assertPropagation(serverSpan, clientSpan); - assert.strictEqual( - rootSpan.context().traceId, - serverSpan.spanContext.traceId - ); - assert.strictEqual( - rootSpan.context().spanId, - clientSpan.parentSpanId - ); - }); - }); - }); - }; - - describe('enable()', () => { - const logger = new NoopLogger(); - const provider = new NodeTracerProvider({ logger }); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); - beforeEach(() => { - memoryExporter.reset(); - }); - - before(() => { - const config = { - // TODO: add plugin options here once supported - }; - plugin.enable(grpc, provider, logger, config); - - const proto = grpc.load(PROTO_PATH).pkg_test; - server = startServer(grpc, proto); - client = createClient(grpc, proto); - }); - - after(done => { - client.close(); - server.tryShutdown(() => { - plugin.disable(); - done(); - }); - }); - - methodList.forEach(method => { - describe(`Test automatic tracing for grpc remote method ${method.description}`, () => { - runTest(method, provider); - }); - }); - - methodList.forEach(method => { - describe(`Test error raising for grpc remote ${method.description}`, () => { - Object.keys(grpc.status).forEach((statusKey: string) => { - const errorCode = Number(grpc.status[statusKey as any]); - if (errorCode > grpc.status.OK) { - runErrorTest(method, statusKey, errorCode, provider); - } - }); - }); - }); - }); - - describe('disable()', () => { - const logger = new NoopLogger(); - const provider = new NodeTracerProvider({ logger }); - provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); - beforeEach(() => { - memoryExporter.reset(); - }); - - before(() => { - plugin.enable(grpc, provider, logger); - plugin.disable(); - - const proto = grpc.load(PROTO_PATH).pkg_test; - server = startServer(grpc, proto); - client = createClient(grpc, proto); - }); - - after(done => { - client.close(); - server.tryShutdown(() => { - done(); - }); - }); - - methodList.map(method => { - describe(`Test automatic tracing for grpc remote method ${method.description}`, () => { - runTest(method, provider, false); - }); - }); - }); +describe(`#${GrpcPlugin.component}`, () => { + runTests(plugin, GrpcPlugin.component, grpc, 12345); }); diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 95cbbe5d148..200e5a33525 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -47,15 +47,15 @@ "@opentelemetry/node": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", "@types/got": "9.6.11", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/request-promise-native": "1.0.17", "@types/semver": "7.3.1", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "@types/superagent": "4.1.8", "axios": "0.19.2", - "codecov": "3.7.0", + "codecov": "3.7.1", "got": "9.6.0", "gts": "2.0.2", "mocha": "7.2.0", @@ -68,7 +68,7 @@ "superagent": "5.3.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-plugin-https/package.json b/packages/opentelemetry-plugin-https/package.json index 145ce6d046a..14495bc2353 100644 --- a/packages/opentelemetry-plugin-https/package.json +++ b/packages/opentelemetry-plugin-https/package.json @@ -47,15 +47,15 @@ "@opentelemetry/node": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", "@types/got": "9.6.11", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/request-promise-native": "1.0.17", "@types/semver": "7.3.1", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "@types/superagent": "4.1.8", "axios": "0.19.2", - "codecov": "3.7.0", + "codecov": "3.7.1", "got": "9.6.0", "gts": "2.0.2", "mocha": "7.2.0", @@ -68,7 +68,7 @@ "superagent": "5.3.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-plugin-xml-http-request/package.json b/packages/opentelemetry-plugin-xml-http-request/package.json index 0021cbe0a5f..a8b8d04e44f 100644 --- a/packages/opentelemetry-plugin-xml-http-request/package.json +++ b/packages/opentelemetry-plugin-xml-http-request/package.json @@ -44,16 +44,16 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", + "@babel/core": "7.10.5", "@opentelemetry/context-zone": "^0.9.0", "@opentelemetry/tracing": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/shimmer": "1.0.1", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -66,10 +66,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "webpack-merge": "5.0.9" diff --git a/packages/opentelemetry-resources/package.json b/packages/opentelemetry-resources/package.json index af07e192a72..7deacf8d5ab 100644 --- a/packages/opentelemetry-resources/package.json +++ b/packages/opentelemetry-resources/package.json @@ -45,10 +45,10 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nock": "12.0.3", @@ -57,7 +57,7 @@ "sinon": "9.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-semantic-conventions/package.json b/packages/opentelemetry-semantic-conventions/package.json index 63ef2d7684d..e9f15d734ae 100644 --- a/packages/opentelemetry-semantic-conventions/package.json +++ b/packages/opentelemetry-semantic-conventions/package.json @@ -41,10 +41,10 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nock": "12.0.3", @@ -53,6 +53,6 @@ "sinon": "9.0.2", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6" + "typescript": "3.9.7" } } diff --git a/packages/opentelemetry-shim-opentracing/package.json b/packages/opentelemetry-shim-opentracing/package.json index a765cdf99fb..90f28246164 100644 --- a/packages/opentelemetry-shim-opentracing/package.json +++ b/packages/opentelemetry-shim-opentracing/package.json @@ -41,9 +41,9 @@ }, "devDependencies": { "@opentelemetry/tracing": "^0.9.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", - "codecov": "3.7.0", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", + "codecov": "3.7.1", "gts": "2.0.2", "mocha": "7.2.0", "nyc": "15.1.0", @@ -52,7 +52,7 @@ "ts-node": "8.10.2", "tslint-consistent-codestyle": "1.16.0", "tslint-microsoft-contrib": "6.2.0", - "typescript": "3.9.6" + "typescript": "3.9.7" }, "dependencies": { "@opentelemetry/api": "^0.9.0", diff --git a/packages/opentelemetry-tracing/package.json b/packages/opentelemetry-tracing/package.json index cd6cfe740a8..294ba5a87de 100644 --- a/packages/opentelemetry-tracing/package.json +++ b/packages/opentelemetry-tracing/package.json @@ -50,11 +50,11 @@ "access": "public" }, "devDependencies": { - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -67,10 +67,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0" }, "dependencies": { diff --git a/packages/opentelemetry-web/package.json b/packages/opentelemetry-web/package.json index ecab3899410..a796fb1faf0 100644 --- a/packages/opentelemetry-web/package.json +++ b/packages/opentelemetry-web/package.json @@ -43,16 +43,16 @@ "access": "public" }, "devDependencies": { - "@babel/core": "7.10.4", + "@babel/core": "7.10.5", "@opentelemetry/context-zone": "^0.9.0", "@opentelemetry/resources": "^0.9.0", "@types/jquery": "3.5.0", - "@types/mocha": "7.0.2", - "@types/node": "14.0.22", + "@types/mocha": "8.0.0", + "@types/node": "14.0.23", "@types/sinon": "9.0.4", "@types/webpack-env": "1.15.2", "babel-loader": "8.1.0", - "codecov": "3.7.0", + "codecov": "3.7.1", "gts": "2.0.2", "istanbul-instrumenter-loader": "3.0.1", "karma": "5.1.0", @@ -66,10 +66,10 @@ "nyc": "15.1.0", "rimraf": "3.0.2", "sinon": "9.0.2", - "ts-loader": "8.0.0", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.6", + "typescript": "3.9.7", "webpack": "4.43.0", "webpack-cli": "3.3.12", "webpack-merge": "5.0.9"