diff --git a/examples/grpc/README.md b/examples/grpc/README.md new file mode 100644 index 00000000..bc212b99 --- /dev/null +++ b/examples/grpc/README.md @@ -0,0 +1,68 @@ +# 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 [http://localhost:9411/zipkin/traces/(your-trace-id)]() (e.g http://localhost:9411/zipkin/traces/4815c3d576d930189725f1f1d1bdfcc6) + +

+ +### 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 [http://localhost:50051/trace/(your-trace-id)]() (e.g http://localhost:50051/trace/4815c3d576d930189725f1f1d1bdfcc6) + +

+ +## 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/client.js b/examples/grpc/client.js new file mode 100644 index 00000000..8303b74b --- /dev/null +++ b/examples/grpc/client.js @@ -0,0 +1,47 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/core'); +const config = require('./setup'); + +/** + * The trace instance needs to be initialized first, if you want to enable + * automatic tracing for built-in plugins (gRPC in this case). + */ +config.setupTracerAndExporters('grpc-client-service'); + +const grpc = require('grpc'); + +const messages = require('./helloworld_pb'); +const services = require('./helloworld_grpc_pb'); +const PORT = 50051; +const tracer = opentelemetry.getTracer(); + +/** A function which makes requests and handles response. */ +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) { + user = process.argv[2]; + } else { + user = 'world'; + } + request.setName(user); + client.sayHello(request, function(err, response) { + span.end(); + if (err) throw err; + console.log('Greeting:', response.getMessage()); + }); + }); +} + +main(); diff --git a/examples/grpc/helloworld_grpc_pb.js b/examples/grpc/helloworld_grpc_pb.js new file mode 100644 index 00000000..e0eed2ea --- /dev/null +++ b/examples/grpc/helloworld_grpc_pb.js @@ -0,0 +1,62 @@ +// 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'; +var grpc = require('grpc'); +var 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. +var 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/helloworld_pb.js b/examples/grpc/helloworld_pb.js new file mode 100644 index 00000000..1f69378f --- /dev/null +++ b/examples/grpc/helloworld_pb.js @@ -0,0 +1,329 @@ +/** + * @fileoverview + * @enhanceable + * @public + */ +// GENERATED CODE -- DO NOT EDIT! + +var jspb = require('google-protobuf'); +var goog = jspb; +var 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) { + var f, + 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) { + var reader = new jspb.BinaryReader(bytes); + var 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; + } + var 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() { + var 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 +) { + var f = undefined; + 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) { + var f, + 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) { + var reader = new jspb.BinaryReader(bytes); + var 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; + } + var 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() { + var 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 +) { + var f = undefined; + 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/images/jaeger.png b/examples/grpc/images/jaeger.png new file mode 100644 index 00000000..20eead3b Binary files /dev/null and b/examples/grpc/images/jaeger.png differ diff --git a/examples/grpc/images/zipkin.png b/examples/grpc/images/zipkin.png new file mode 100644 index 00000000..d1dcd125 Binary files /dev/null and b/examples/grpc/images/zipkin.png differ diff --git a/examples/grpc/package.json b/examples/grpc/package.json new file mode 100644 index 00000000..390bd2a8 --- /dev/null +++ b/examples/grpc/package.json @@ -0,0 +1,44 @@ +{ + "name": "grpc-example", + "version": "0.0.1", + "description": "Example of gRPC 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": { + "@opentelemetry/basic-tracer": "^0.0.1", + "@opentelemetry/core": "^0.0.1", + "@opentelemetry/exporter-jaeger": "^0.0.1", + "@opentelemetry/exporter-zipkin": "^0.0.1", + "@opentelemetry/node-sdk": "^0.0.1", + "@opentelemetry/plugin-grpc": "^0.0.1", + "google-protobuf": "^3.9.2", + "grpc": "^1.23.3", + "node-pre-gyp": "0.12.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": { + "cross-env": "^6.0.0" + } +} diff --git a/examples/grpc/server.js b/examples/grpc/server.js new file mode 100644 index 00000000..b1e3c407 --- /dev/null +++ b/examples/grpc/server.js @@ -0,0 +1,45 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/core'); + +/** + * The trace instance needs to be initialized first, if you want to enable + * automatic tracing for built-in plugins (gRPC in this case). + */ +const config = require('./setup'); +config.setupTracerAndExporters('grpc-server-service'); + +const grpc = require('grpc'); +const tracer = opentelemetry.getTracer(); + +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: 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/setup.js b/examples/grpc/setup.js new file mode 100644 index 00000000..c910145d --- /dev/null +++ b/examples/grpc/setup.js @@ -0,0 +1,42 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/core'); +const { NodeTracer } = require('@opentelemetry/node-sdk'); +const { SimpleSpanProcessor } = require('@opentelemetry/basic-tracer'); +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); +const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); +const EXPORTER = process.env.EXPORTER || ''; + +function setupTracerAndExporters(service) { + let exporter; + const options = { + serviceName: service + }; + const tracer = new NodeTracer({ + plugins: { + grpc: { + enabled: true, + // if it can't find the module, put the absolute path since the packages are not published yet + path: '@opentelemetry/plugin-grpc' + } + } + }); + if (EXPORTER.toLowerCase().startsWith('z')) { + // need ignoreOutgoingUrls: [/spans/] to avoid infinity loops + // TODO: manage this situation + const zipkinExporter = new ZipkinExporter(options); + exporter = new SimpleSpanProcessor(zipkinExporter); + } else { + // need to shutdown exporter in order to flush spans + // TODO: check once PR #301 is merged + const jaegerExporter = new JaegerExporter(options); + exporter = new SimpleSpanProcessor(jaegerExporter); + } + + tracer.addSpanProcessor(exporter); + + // Initialize the OpenTelemetry APIs to use the BasicTracer bindings + opentelemetry.initGlobalTracer(tracer); +} + +exports.setupTracerAndExporters = setupTracerAndExporters;