diff --git a/examples/graphql/README.md b/examples/graphql/README.md
new file mode 100644
index 0000000000..465439fd47
--- /dev/null
+++ b/examples/graphql/README.md
@@ -0,0 +1,57 @@
+# Overview OpenTelemetry GraphQL Instrumentation Example
+
+This example shows how to use 2 popular graphql servers
+
+- [Apollo GraphQL](https://www.npmjs.com/package/apollo-server)
+- [GraphQL HTTP Server Middleware](https://www.npmjs.com/package/express-graphql)
+
+and [@opentelemetry/instrumentation-graphql](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-instrumentation-graphql) to instrument a simple Node.js application.
+
+This instrumentation should work with any graphql server as it instruments graphql directly.
+
+This example will export spans data simultaneously using [Exporter Collector](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-collector).
+
+## Installation
+
+```shell script
+# from this directory
+npm install
+```
+
+## Run the Application
+
+1. Run docker
+
+ ```shell script
+ # from this directory
+ npm run docker:start
+ ```
+
+2. Run server - depends on your preference
+
+ ```shell script
+ # from this directory
+ npm run server:express
+ // or
+ npm run server:apollo
+ ```
+
+3. Open page at - you should be able to see the spans in zipkin
+
+4. Run example client
+
+ ```shell script
+ # from this directory
+ npm run client
+ ```
+
+5. You can also write your own queries, open page `http://localhost:4000/graphql`
+
+## Useful links
+
+- For more information on OpenTelemetry, visit:
+- For more information on tracing, visit:
+
+## LICENSE
+
+Apache License 2.0
diff --git a/examples/graphql/client.js b/examples/graphql/client.js
new file mode 100644
index 0000000000..3aa487a1ad
--- /dev/null
+++ b/examples/graphql/client.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const url = require('url');
+const http = require('http');
+// Construct a schema, using GraphQL schema language
+
+const source = `
+query {
+ books {
+ name
+ authors {
+ name
+ address {
+ country
+ }
+ }
+ }
+}
+`;
+
+makeRequest(source).then(console.log);
+
+function makeRequest(query) {
+ return new Promise((resolve, reject) => {
+ const parsedUrl = new url.URL('http://localhost:4000/graphql');
+ const options = {
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port,
+ path: parsedUrl.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+ const req = http.request(options, (res) => {
+ const data = [];
+ res.on('data', (chunk) => data.push(chunk));
+ res.on('end', () => {
+ resolve(data.toString());
+ });
+ res.on('error', (err) => {
+ reject(err);
+ });
+ });
+
+ req.write(JSON.stringify({ query }));
+ req.end();
+ });
+}
diff --git a/examples/graphql/docker/collector-config.yaml b/examples/graphql/docker/collector-config.yaml
new file mode 100644
index 0000000000..e9a909d78f
--- /dev/null
+++ b/examples/graphql/docker/collector-config.yaml
@@ -0,0 +1,29 @@
+receivers:
+ otlp:
+ protocols:
+ grpc:
+ http:
+ cors_allowed_origins:
+ - http://*
+ - https://*
+
+exporters:
+ zipkin:
+ endpoint: "http://zipkin-all-in-one:9411/api/v2/spans"
+ prometheus:
+ endpoint: "0.0.0.0:9464"
+
+processors:
+ batch:
+ queued_retry:
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ exporters: [zipkin]
+ processors: [batch, queued_retry]
+ metrics:
+ receivers: [otlp]
+ exporters: [prometheus]
+ processors: [batch, queued_retry]
diff --git a/examples/graphql/docker/docker-compose.yaml b/examples/graphql/docker/docker-compose.yaml
new file mode 100644
index 0000000000..d4e0a42a19
--- /dev/null
+++ b/examples/graphql/docker/docker-compose.yaml
@@ -0,0 +1,30 @@
+version: "3"
+services:
+ # Collector
+ collector:
+# image: otel/opentelemetry-collector:latest
+ image: otel/opentelemetry-collector:0.13.0
+ command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"]
+ volumes:
+ - ./collector-config.yaml:/conf/collector-config.yaml
+ ports:
+ - "9464:9464"
+ - "55680:55680"
+ - "55681:55681"
+ depends_on:
+ - zipkin-all-in-one
+
+ # Zipkin
+ zipkin-all-in-one:
+ image: openzipkin/zipkin:latest
+ ports:
+ - "9411:9411"
+
+ # Prometheus
+ prometheus:
+ container_name: prometheus
+ image: prom/prometheus:latest
+ volumes:
+ - ./prometheus.yaml:/etc/prometheus/prometheus.yml
+ ports:
+ - "9090:9090"
diff --git a/examples/graphql/docker/prometheus.yaml b/examples/graphql/docker/prometheus.yaml
new file mode 100644
index 0000000000..b027daf9a0
--- /dev/null
+++ b/examples/graphql/docker/prometheus.yaml
@@ -0,0 +1,9 @@
+global:
+ scrape_interval: 15s # Default is every 1 minute.
+
+scrape_configs:
+ - job_name: 'collector'
+ # metrics_path defaults to '/metrics'
+ # scheme defaults to 'http'.
+ static_configs:
+ - targets: ['collector:9464']
diff --git a/examples/graphql/package.json b/examples/graphql/package.json
new file mode 100644
index 0000000000..1451eae8b3
--- /dev/null
+++ b/examples/graphql/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "opentelemetry-plugin-graphql-example",
+ "private": true,
+ "version": "0.11.0",
+ "description": "Example of using @opentelemetry/plugin-graphql with OpenTelemetry",
+ "main": "index.js",
+ "scripts": {
+ "client": "node ./client.js",
+ "docker:start": "cd ./docker && docker-compose down && docker-compose up",
+ "docker:startd": "cd ./docker && docker-compose down && docker-compose up -d",
+ "docker:stop": "cd ./docker && docker-compose down",
+ "server:express": "node ./server-express.js",
+ "server:apollo": "node ./server-apollo.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git"
+ },
+ "keywords": [
+ "opentelemetry",
+ "http",
+ "tracing",
+ "graphql"
+ ],
+ "engines": {
+ "node": ">=8"
+ },
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/open-telemetry/opentelemetry-js/issues"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^0.12.0",
+ "@opentelemetry/exporter-collector": "^0.12.0",
+ "@opentelemetry/node": "^0.12.0",
+ "@opentelemetry/plugin-express": "^0.10.0",
+ "@opentelemetry/instrumentation-graphql": "^0.11.0",
+ "@opentelemetry/plugin-http": "^0.12.0",
+ "@opentelemetry/plugin-https": "^0.12.0",
+ "@opentelemetry/tracing": "^0.12.0",
+ "apollo-server": "^2.18.1",
+ "express": "^4.17.1",
+ "express-graphql": "^0.11.0",
+ "graphql": "^15.3.0",
+ "qs-middleware": "^1.0.3"
+ },
+ "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme",
+ "devDependencies": {
+ "cross-env": "^6.0.0"
+ }
+}
diff --git a/examples/graphql/schema.js b/examples/graphql/schema.js
new file mode 100644
index 0000000000..be98379c40
--- /dev/null
+++ b/examples/graphql/schema.js
@@ -0,0 +1,202 @@
+'use strict';
+
+const https = require('https');
+const graphql = require('graphql');
+
+const url1 = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/package.json';
+
+function getData(url) {
+ return new Promise((resolve, reject) => {
+ https.get(url, (response) => {
+ let data = '';
+ response.on('data', (chunk) => {
+ data += chunk;
+ });
+ response.on('end', () => {
+ resolve(JSON.parse(data));
+ });
+ }).on('error', (err) => {
+ reject(err);
+ });
+ });
+}
+
+const authors = [];
+const books = [];
+
+function addBook(name, authorIds) {
+ let authorIdsLocal = authorIds;
+ if (typeof authorIdsLocal === 'string') {
+ authorIdsLocal = authorIdsLocal.split(',').map((id) => parseInt(id, 10));
+ }
+ const id = books.length;
+ books.push({
+ id,
+ name,
+ authorIds: authorIdsLocal,
+ });
+ return books[books.length - 1];
+}
+
+function addAuthor(name, country, city) {
+ const id = authors.length;
+ authors.push({
+ id,
+ name,
+ address: {
+ country,
+ city,
+ },
+ });
+ return authors[authors.length - 1];
+}
+
+function getBook(id) {
+ return books[id];
+}
+
+function getAuthor(id) {
+ return authors[id];
+}
+
+function prepareData() {
+ addAuthor('John', 'Poland', 'Szczecin');
+ addAuthor('Alice', 'Poland', 'Warsaw');
+ addAuthor('Bob', 'England', 'London');
+ addAuthor('Christine', 'France', 'Paris');
+ addBook('First Book', [0, 1]);
+ addBook('Second Book', [2]);
+ addBook('Third Book', [3]);
+}
+
+prepareData();
+module.exports = function buildSchema() {
+ const Author = new graphql.GraphQLObjectType({
+ name: 'Author',
+ fields: {
+ id: {
+ type: graphql.GraphQLString,
+ resolve(obj, _args) {
+ return obj.id;
+ },
+ },
+ name: {
+ type: graphql.GraphQLString,
+ resolve(obj, _args) {
+ return obj.name;
+ },
+ },
+ description: {
+ type: graphql.GraphQLString,
+ resolve(_obj, _args) {
+ return new Promise((resolve, reject) => {
+ getData(url1).then((response) => {
+ resolve(response.description);
+ }, reject);
+ });
+ },
+ },
+ address: {
+ type: new graphql.GraphQLObjectType({
+ name: 'Address',
+ fields: {
+ country: {
+ type: graphql.GraphQLString,
+ resolve(obj, _args) {
+ return obj.country;
+ },
+ },
+ city: {
+ type: graphql.GraphQLString,
+ resolve(obj, _args) {
+ return obj.city;
+ },
+ },
+ },
+ }),
+ resolve(obj, _args) {
+ return obj.address;
+ },
+ },
+ },
+ });
+
+ const Book = new graphql.GraphQLObjectType({
+ name: 'Book',
+ fields: {
+ id: {
+ type: graphql.GraphQLInt,
+ resolve(obj, _args) {
+ return obj.id;
+ },
+ },
+ name: {
+ type: graphql.GraphQLString,
+ resolve(obj, _args) {
+ return obj.name;
+ },
+ },
+ authors: {
+ type: new graphql.GraphQLList(Author),
+ resolve(obj, _args) {
+ return obj.authorIds.map((id) => authors[id]);
+ },
+ },
+ },
+ });
+
+ const query = new graphql.GraphQLObjectType({
+ name: 'Query',
+ fields: {
+ author: {
+ type: Author,
+ args: {
+ id: { type: graphql.GraphQLInt },
+ },
+ resolve(obj, args, _context) {
+ return Promise.resolve(getAuthor(args.id));
+ },
+ },
+ authors: {
+ type: new graphql.GraphQLList(Author),
+ resolve(_obj, _args, _context) {
+ return Promise.resolve(authors);
+ },
+ },
+ book: {
+ type: Book,
+ args: {
+ id: { type: graphql.GraphQLInt },
+ },
+ resolve(obj, args, _context) {
+ return Promise.resolve(getBook(args.id));
+ },
+ },
+ books: {
+ type: new graphql.GraphQLList(Book),
+ resolve(_obj, _args, _context) {
+ return Promise.resolve(books);
+ },
+ },
+ },
+ });
+
+ const mutation = new graphql.GraphQLObjectType({
+ name: 'Mutation',
+ fields: {
+ addBook: {
+ type: Book,
+ args: {
+ name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
+ authorIds: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
+ },
+ resolve(obj, args, _context) {
+ return Promise.resolve(addBook(args.name, args.authorIds));
+ },
+ },
+ },
+ });
+
+ const schema = new graphql.GraphQLSchema({ query, mutation });
+ return schema;
+};
diff --git a/examples/graphql/server-apollo.js b/examples/graphql/server-apollo.js
new file mode 100644
index 0000000000..287e95700d
--- /dev/null
+++ b/examples/graphql/server-apollo.js
@@ -0,0 +1,21 @@
+'use strict';
+
+require('./tracer');
+
+const { ApolloServer } = require('apollo-server');
+const buildSchema = require('./schema');
+
+// Construct a schema, using GraphQL schema language
+const schema = buildSchema();
+
+const server = new ApolloServer({ schema });
+server.listen().then(({ url }) => {
+ console.log(`🚀 Server ready at ${url}`);
+});
+
+// app.use('/graphql', server);
+// app.listen(4000);
+
+// server.applyMiddleware({ app });
+
+console.log('Running a GraphQL API server at http://localhost:4000/graphql');
diff --git a/examples/graphql/server-express.js b/examples/graphql/server-express.js
new file mode 100644
index 0000000000..6bba89f817
--- /dev/null
+++ b/examples/graphql/server-express.js
@@ -0,0 +1,19 @@
+'use strict';
+
+require('./tracer');
+
+const express = require('express');
+const { graphqlHTTP } = require('express-graphql');
+const buildSchema = require('./schema');
+
+const schema = buildSchema();
+
+const app = express();
+app.use('/graphql', graphqlHTTP({
+ schema,
+ graphiql: true,
+}));
+
+app.listen(4000);
+
+console.log('Running a GraphQL API server at http://localhost:4000/graphql');
diff --git a/examples/graphql/tracer.js b/examples/graphql/tracer.js
new file mode 100644
index 0000000000..4be5684dfd
--- /dev/null
+++ b/examples/graphql/tracer.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql');
+
+const { ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing');
+const { NodeTracerProvider } = require('@opentelemetry/node');
+const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector');
+
+const exporter = new CollectorTraceExporter({
+ serviceName: 'basic-service',
+});
+
+const provider = new NodeTracerProvider({
+ plugins: {
+ http: { enabled: false, path: '@opentelemetry/plugin-http' },
+ https: { enabled: false, path: '@opentelemetry/plugin-https' },
+ express: { enabled: false, path: '@opentelemetry/plugin-express' },
+ },
+});
+
+const graphQLInstrumentation = new GraphQLInstrumentation({
+ // allowAttributes: true,
+ // depth: 2,
+ // mergeItems: true,
+});
+
+graphQLInstrumentation.setTracerProvider(provider);
+
+graphQLInstrumentation.enable();
+
+provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
+provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+provider.register();
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/.eslintignore b/plugins/node/opentelemetry-instrumentation-graphql/.eslintignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-graphql/.eslintrc.js
new file mode 100644
index 0000000000..f756f4488b
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/.eslintrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ "env": {
+ "mocha": true,
+ "node": true
+ },
+ ...require('../../../eslint.config.js')
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/.gitignore b/plugins/node/opentelemetry-instrumentation-graphql/.gitignore
new file mode 100644
index 0000000000..ba80b4026b
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/.gitignore
@@ -0,0 +1,2 @@
+# Dependency directories
+!test/instrumentation/node_modules
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/.npmignore b/plugins/node/opentelemetry-instrumentation-graphql/.npmignore
new file mode 100644
index 0000000000..9505ba9450
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/.npmignore
@@ -0,0 +1,4 @@
+/bin
+/coverage
+/doc
+/test
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/LICENSE b/plugins/node/opentelemetry-instrumentation-graphql/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/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/plugins/node/opentelemetry-instrumentation-graphql/README.md b/plugins/node/opentelemetry-instrumentation-graphql/README.md
new file mode 100644
index 0000000000..80890ebbd5
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/README.md
@@ -0,0 +1,76 @@
+# OpenTelemetry Instrumentation GraphQL
+
+[![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 *automated instrumentation and tracing* for GraphQL in Node.js applications.
+
+## Installation
+
+```shell script
+npm install @opentelemetry/instrumentation-graphql
+```
+
+## Usage
+
+```js
+'use strict';
+
+const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql');
+
+const { ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing');
+const { NodeTracerProvider } = require('@opentelemetry/node');
+const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector');
+
+const exporter = new CollectorTraceExporter({
+ serviceName: 'basic-service',
+});
+
+const provider = new NodeTracerProvider({
+ plugins: {
+ http: { enabled: false, path: '@opentelemetry/plugin-http' },
+ https: { enabled: false, path: '@opentelemetry/plugin-https' },
+ express: { enabled: false, path: '@opentelemetry/plugin-express' },
+ },
+});
+
+const graphQLInstrumentation = new GraphQLInstrumentation({
+// optional params
+ // allowAttributes: true,
+ // depth: 2,
+ // mergeItems: true,
+});
+
+graphQLInstrumentation.setTracerProvider(provider);
+
+graphQLInstrumentation.enable();
+
+provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
+provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
+provider.register();
+
+```
+
+## 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-instrumentation-graphql
+[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-instrumentation-graphql
+[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-instrumentation-graphql
+[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-instrumentation-graphql&type=dev
+[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-graphql
+[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-graphql.svg
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/package.json b/plugins/node/opentelemetry-instrumentation-graphql/package.json
new file mode 100644
index 0000000000..0ed50176b2
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@opentelemetry/instrumentation-graphql",
+ "version": "0.11.0",
+ "description": "OpenTelemetry @opentelemetry/instrumentation-graphql automatic instrumentation package.",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": "open-telemetry/opentelemetry-js",
+ "scripts": {
+ "clean": "rimraf build/*",
+ "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../",
+ "compile": "npm run version:update && tsc -p .",
+ "lint": "eslint . --ext .ts",
+ "lint:fix": "eslint . --ext .ts --fix",
+ "precompile": "tsc --version",
+ "prepare": "npm run compile",
+ "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'",
+ "tdd": "npm run test -- --watch-extensions ts --watch",
+ "version:update": "node ../../../scripts/version-update.js",
+ "watch": "tsc -w"
+ },
+ "keywords": [
+ "opentelemetry",
+ "nodejs",
+ "tracing",
+ "metrics",
+ "stats",
+ "graphql"
+ ],
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "files": [
+ "build/src/**/*.js",
+ "build/src/**/*.js.map",
+ "build/src/**/*.d.ts",
+ "doc",
+ "LICENSE",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@opentelemetry/tracing": "^0.12.0",
+ "@types/graphql": "^14.5.0",
+ "@types/mocha": "8.0.1",
+ "@types/node": "14.0.27",
+ "@types/semver": "7.3.1",
+ "codecov": "3.7.2",
+ "graphql": "^15.3.0",
+ "gts": "2.0.2",
+ "mocha": "7.2.0",
+ "nyc": "15.1.0",
+ "rimraf": "3.0.2",
+ "semver": "^7.1.3",
+ "shimmer": "1.2.1",
+ "ts-mocha": "7.0.0",
+ "ts-node": "8.10.2",
+ "typescript": "3.9.7"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^0.12.0",
+ "@opentelemetry/instrumentation": "^0.12.0"
+ }
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/enum.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/enum.ts
new file mode 100644
index 0000000000..20d7e0c914
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/enum.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.
+ */
+
+export enum AllowedOperationTypes {
+ QUERY = 'query',
+ MUTATION = 'mutation',
+ SUBSCRIPTION = 'subscription',
+}
+
+export enum TokenKind {
+ SOF = '',
+ EOF = '',
+ BANG = '!',
+ DOLLAR = '$',
+ AMP = '&',
+ PAREN_L = '(',
+ PAREN_R = ')',
+ SPREAD = '...',
+ COLON = ':',
+ EQUALS = '=',
+ AT = '@',
+ BRACKET_L = '[',
+ BRACKET_R = ']',
+ BRACE_L = '{',
+ PIPE = '|',
+ BRACE_R = '}',
+ NAME = 'Name',
+ INT = 'Int',
+ FLOAT = 'Float',
+ STRING = 'String',
+ BLOCK_STRING = 'BlockString',
+ COMMENT = 'Comment',
+}
+
+export enum SpanAttributes {
+ COMPONENT = 'graphql',
+ SOURCE = 'graphql.source',
+ FIELD_NAME = 'graphql.field.name',
+ FIELD_PATH = 'graphql.field.path',
+ FIELD_TYPE = 'graphql.field.type',
+ OPERATION = 'graphql.operation.name',
+ VARIABLES = 'graphql.variables.',
+ ERROR_VALIDATION_NAME = 'graphql.validation.error',
+}
+
+export enum SpanNames {
+ EXECUTE = 'graphql.execute',
+ PARSE = 'graphql.parse',
+ RESOLVE = 'graphql.resolve',
+ VALIDATE = 'graphql.validate',
+ SCHEMA_VALIDATE = 'graphql.validateSchema',
+ SCHEMA_PARSE = 'graphql.parseSchema',
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/graphql.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/graphql.ts
new file mode 100644
index 0000000000..6260810b89
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/graphql.ts
@@ -0,0 +1,438 @@
+/*
+ * 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 {
+ isWrapped,
+ InstrumentationBase,
+ InstrumentationConfig,
+ InstrumentationNodeModuleDefinition,
+ InstrumentationNodeModuleFile,
+ safeExecuteInTheMiddle,
+} from '@opentelemetry/instrumentation';
+import { Maybe } from 'graphql/jsutils/Maybe';
+import type * as graphqlTypes from 'graphql';
+import { GraphQLFieldResolver } from 'graphql/type/definition';
+import { SpanAttributes, SpanNames } from './enum';
+import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_SPAN_SYMBOL } from './symbols';
+
+import {
+ executeFunctionWithObj,
+ executeArgumentsArray,
+ executeType,
+ parseType,
+ validateType,
+ GraphQLInstrumentationConfig,
+ GraphQLInstrumentationParsedConfig,
+ OtelExecutionArgs,
+ ObjectWithGraphQLData,
+ ObjectWithOtelSpan,
+ OPERATION_NOT_SUPPORTED,
+} from './types';
+import {
+ addSpanSource,
+ endSpan,
+ getOperation,
+ wrapFieldResolver,
+ wrapFields,
+} from './utils';
+
+import { VERSION } from './version';
+import * as api from '@opentelemetry/api';
+import type { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
+
+const DEFAULT_CONFIG: GraphQLInstrumentationConfig = {
+ mergeItems: false,
+ depth: -1,
+ allowValues: false,
+};
+
+export class GraphQLInstrumentation extends InstrumentationBase {
+ constructor(
+ config: GraphQLInstrumentationConfig & InstrumentationConfig = {}
+ ) {
+ super('graphql', VERSION, Object.assign({}, DEFAULT_CONFIG, config));
+ }
+
+ private _getConfig(): GraphQLInstrumentationParsedConfig {
+ return this._config as GraphQLInstrumentationParsedConfig;
+ }
+
+ setConfig(config: GraphQLInstrumentationConfig & InstrumentationConfig = {}) {
+ this._config = Object.assign({}, DEFAULT_CONFIG, config);
+ }
+
+ protected init() {
+ const module = new InstrumentationNodeModuleDefinition(
+ 'graphql',
+ ['15.*']
+ );
+ module.files.push(this._addPatchingExecute());
+ module.files.push(this._addPatchingParser());
+ module.files.push(this._addPatchingValidate());
+
+ return module;
+ }
+
+ private _addPatchingExecute(): InstrumentationNodeModuleFile<
+ typeof graphqlTypes
+ > {
+ return new InstrumentationNodeModuleFile(
+ 'graphql/execution/execute.js',
+ // cannot make it work with appropriate type as execute function has 2
+ //types and/cannot import function but only types
+ (moduleExports: any) => {
+ if (isWrapped(moduleExports.execute)) {
+ this._unwrap(moduleExports, 'execute');
+ }
+ this._wrap(
+ moduleExports,
+ 'execute',
+ this._patchExecute(moduleExports.defaultFieldResolver)
+ );
+ return moduleExports;
+ },
+ moduleExports => {
+ if (moduleExports) {
+ this._unwrap(moduleExports, 'execute');
+ }
+ }
+ );
+ }
+
+ private _addPatchingParser(): InstrumentationNodeModuleFile<
+ typeof graphqlTypes
+ > {
+ return new InstrumentationNodeModuleFile(
+ 'graphql/language/parser.js',
+ moduleExports => {
+ if (isWrapped(moduleExports.execute)) {
+ this._unwrap(moduleExports, 'parse');
+ }
+ this._wrap(moduleExports, 'parse', this._patchParse());
+ return moduleExports;
+ },
+ moduleExports => {
+ if (moduleExports) {
+ this._unwrap(moduleExports, 'parse');
+ }
+ }
+ );
+ }
+
+ private _addPatchingValidate(): InstrumentationNodeModuleFile<
+ typeof graphqlTypes
+ > {
+ return new InstrumentationNodeModuleFile(
+ 'graphql/validation/validate.js',
+ moduleExports => {
+ if (isWrapped(moduleExports.execute)) {
+ this._unwrap(moduleExports, 'validate');
+ }
+ this._wrap(moduleExports, 'validate', this._patchValidate());
+ return moduleExports;
+ },
+ moduleExports => {
+ if (moduleExports) {
+ this._unwrap(moduleExports, 'validate');
+ }
+ }
+ );
+ }
+
+ private _patchExecute(
+ defaultFieldResolved: GraphQLFieldResolver
+ ): (original: executeType) => executeType {
+ const instrumentation = this;
+ return function execute(original) {
+ return function patchExecute(
+ this: executeType
+ ): PromiseOrValue {
+ let processedArgs: OtelExecutionArgs;
+
+ // case when apollo server is used for example
+ if (arguments.length >= 2) {
+ const args = (arguments as unknown) as executeArgumentsArray;
+ processedArgs = instrumentation._wrapExecuteArgs(
+ args[0],
+ args[1],
+ args[2],
+ args[3],
+ args[4],
+ args[5],
+ args[6] || defaultFieldResolved,
+ args[7]
+ );
+ } else {
+ const args = arguments[0] as graphqlTypes.ExecutionArgs;
+ processedArgs = instrumentation._wrapExecuteArgs(
+ args.schema,
+ args.document,
+ args.rootValue,
+ args.contextValue,
+ args.variableValues,
+ args.operationName,
+ args.fieldResolver || defaultFieldResolved,
+ args.typeResolver
+ );
+ }
+
+ const operation = getOperation(
+ processedArgs.document,
+ processedArgs.operationName
+ );
+
+ const span = instrumentation._createExecuteSpan(
+ operation,
+ processedArgs
+ );
+
+ processedArgs.contextValue[OTEL_GRAPHQL_DATA_SYMBOL] = {
+ source: processedArgs.document
+ ? processedArgs.document ||
+ (processedArgs.document as ObjectWithGraphQLData)[
+ OTEL_GRAPHQL_DATA_SYMBOL
+ ]
+ : undefined,
+ span,
+ fields: {},
+ };
+
+ return instrumentation.tracer.withSpan(span, () => {
+ return safeExecuteInTheMiddle<
+ PromiseOrValue
+ >(
+ () => {
+ return (original as executeFunctionWithObj).apply(this, [
+ processedArgs,
+ ]);
+ },
+ err => {
+ endSpan(span, err);
+ }
+ );
+ });
+ };
+ };
+ }
+
+ private _patchParse(): (original: parseType) => parseType {
+ const instrumentation = this;
+ return function parse(original) {
+ return function patchParse(
+ this: parseType,
+ source: string | graphqlTypes.Source,
+ options?: graphqlTypes.ParseOptions
+ ): graphqlTypes.DocumentNode {
+ return instrumentation._parse(this, original, source, options);
+ };
+ };
+ }
+
+ private _patchValidate(): (original: validateType) => validateType {
+ const instrumentation = this;
+ return function validate(original: validateType) {
+ return function patchValidate(
+ this: validateType,
+ schema: graphqlTypes.GraphQLSchema,
+ documentAST: graphqlTypes.DocumentNode,
+ rules?: ReadonlyArray,
+ typeInfo?: graphqlTypes.TypeInfo,
+ options?: { maxErrors?: number }
+ ): ReadonlyArray {
+ return instrumentation._validate(
+ this,
+ original,
+ schema,
+ documentAST,
+ rules,
+ typeInfo,
+ options
+ );
+ };
+ };
+ }
+
+ private _parse(
+ obj: parseType,
+ original: parseType,
+ source: string | graphqlTypes.Source,
+ options?: graphqlTypes.ParseOptions
+ ): graphqlTypes.DocumentNode {
+ const config = this._getConfig();
+ const span = this.tracer.startSpan(SpanNames.PARSE);
+
+ return this.tracer.withSpan(span, () => {
+ return safeExecuteInTheMiddle<
+ graphqlTypes.DocumentNode & ObjectWithGraphQLData
+ >(
+ () => {
+ return original.call(obj, source, options);
+ },
+ (err, result) => {
+ if (result) {
+ (result as ObjectWithOtelSpan)[OTEL_SPAN_SYMBOL] = span;
+ const operation = getOperation(result);
+ if (!operation) {
+ span.updateName(SpanNames.SCHEMA_PARSE);
+ } else if (result.loc) {
+ addSpanSource(span, result.loc, config.allowValues);
+ }
+ }
+ endSpan(span, err);
+ }
+ );
+ });
+ }
+
+ private _validate(
+ obj: validateType,
+ original: validateType,
+ schema: graphqlTypes.GraphQLSchema,
+ documentAST: graphqlTypes.DocumentNode,
+ rules?: ReadonlyArray,
+ typeInfo?: graphqlTypes.TypeInfo,
+ options?: { maxErrors?: number }
+ ): ReadonlyArray {
+ const document = documentAST as ObjectWithOtelSpan;
+ const span = this.tracer.startSpan(SpanNames.VALIDATE, {
+ parent: document[OTEL_SPAN_SYMBOL],
+ });
+ document[OTEL_SPAN_SYMBOL] = span;
+
+ return this.tracer.withSpan(span, () => {
+ return safeExecuteInTheMiddle>(
+ () => {
+ return original.call(
+ obj,
+ schema,
+ documentAST,
+ rules,
+ typeInfo,
+ options
+ );
+ },
+ (err, errors) => {
+ if (!documentAST.loc) {
+ span.updateName(SpanNames.SCHEMA_VALIDATE);
+ }
+ if (errors && errors.length) {
+ span.recordException({
+ name: SpanAttributes.ERROR_VALIDATION_NAME,
+ message: JSON.stringify(errors),
+ });
+ }
+ endSpan(span, err);
+ }
+ );
+ });
+ }
+
+ private _createExecuteSpan(
+ operation: graphqlTypes.DefinitionNode | undefined,
+ processedArgs: graphqlTypes.ExecutionArgs
+ ): api.Span {
+ const config = this._getConfig();
+ const document = processedArgs.document as ObjectWithOtelSpan;
+ const span = this.tracer.startSpan(SpanNames.EXECUTE, {
+ parent: document[OTEL_SPAN_SYMBOL],
+ });
+ if (operation) {
+ const name = (operation as graphqlTypes.OperationDefinitionNode)
+ .operation;
+ if (name) {
+ span.setAttribute(SpanAttributes.OPERATION, name);
+ }
+ } else {
+ let operationName = ' ';
+ if (processedArgs.operationName) {
+ operationName = ` "${processedArgs.operationName}" `;
+ }
+ operationName = OPERATION_NOT_SUPPORTED.replace(
+ '$operationName$',
+ operationName
+ );
+ span.setAttribute(SpanAttributes.OPERATION, operationName);
+ }
+
+ if (processedArgs.document?.loc) {
+ addSpanSource(span, processedArgs.document.loc, config.allowValues);
+ }
+
+ if (processedArgs.variableValues && config.allowValues) {
+ Object.entries(processedArgs.variableValues).forEach(([key, value]) => {
+ span.setAttribute(`${SpanAttributes.VARIABLES}${String(key)}`, value);
+ });
+ }
+
+ return span;
+ }
+
+ private _wrapExecuteArgs(
+ schema: graphqlTypes.GraphQLSchema,
+ document: graphqlTypes.DocumentNode,
+ rootValue: any,
+ contextValue: any,
+ variableValues: Maybe<{ [key: string]: any }>,
+ operationName: Maybe,
+ fieldResolver: Maybe>,
+ typeResolver: Maybe>
+ ): OtelExecutionArgs {
+ if (!contextValue) {
+ contextValue = {};
+ }
+ if (contextValue[OTEL_GRAPHQL_DATA_SYMBOL]) {
+ return {
+ schema,
+ document,
+ rootValue,
+ contextValue,
+ variableValues,
+ operationName,
+ fieldResolver,
+ typeResolver,
+ };
+ }
+ fieldResolver = wrapFieldResolver(
+ this.tracer,
+ this._getConfig.bind(this),
+ fieldResolver
+ );
+
+ if (schema) {
+ wrapFields(
+ schema.getQueryType(),
+ this.tracer,
+ this._getConfig.bind(this)
+ );
+ wrapFields(
+ schema.getMutationType(),
+ this.tracer,
+ this._getConfig.bind(this)
+ );
+ }
+
+ return {
+ schema,
+ document,
+ rootValue,
+ contextValue,
+ variableValues,
+ operationName,
+ fieldResolver,
+ typeResolver,
+ };
+ }
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/index.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/index.ts
new file mode 100644
index 0000000000..9bdb560f36
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/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 './graphql';
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/symbols.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/symbols.ts
new file mode 100644
index 0000000000..88cff1c233
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/symbols.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.
+ */
+
+export const OTEL_PATCHED_SYMBOL = Symbol.for('opentelemetry.patched');
+
+export const OTEL_GRAPHQL_DATA_SYMBOL = Symbol.for(
+ 'opentelemetry.graphql_data'
+);
+
+export const OTEL_SPAN_SYMBOL = Symbol.for('opentelemetry.span');
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/types.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/types.ts
new file mode 100644
index 0000000000..9c157e5d92
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/types.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { InstrumentationConfig } from '@opentelemetry/instrumentation';
+import type * as graphqlTypes from 'graphql';
+import type * as api from '@opentelemetry/api';
+import type { Maybe } from 'graphql/jsutils/Maybe';
+import type { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
+import { DocumentNode } from 'graphql/language/ast';
+import {
+ GraphQLFieldResolver,
+ GraphQLTypeResolver,
+} from 'graphql/type/definition';
+import { GraphQLSchema } from 'graphql/type/schema';
+import {
+ OTEL_GRAPHQL_DATA_SYMBOL,
+ OTEL_PATCHED_SYMBOL,
+ OTEL_SPAN_SYMBOL,
+} from './symbols';
+
+export const OPERATION_NOT_SUPPORTED =
+ 'Operation$operationName$not' + ' supported';
+
+export interface GraphQLInstrumentationConfig {
+ /**
+ * When set to true it will not remove attributes values from schema source.
+ * By default all values that can be sensitive are removed and replaced
+ * with "*"
+ *
+ * @default false
+ */
+ allowValues?: boolean;
+ /**
+ * The maximum depth of fields/resolvers to instrument.
+ * When set to 0 it will not instrument fields and resolvers
+ *
+ * @default undefined
+ */
+ depth?: number;
+ /**
+ * Whether to merge list items into a single element.
+ *
+ * @example `users.*.name` instead of `users.0.name`, `users.1.name`
+ *
+ * @default false
+ */
+ mergeItems?: boolean;
+}
+
+/**
+ * Merged and parsed config of default instrumentation config and GraphQL
+ */
+export type GraphQLInstrumentationParsedConfig = Required<
+ GraphQLInstrumentationConfig
+> &
+ InstrumentationConfig;
+
+export type executeFunctionWithObj = (
+ args: graphqlTypes.ExecutionArgs
+) => PromiseOrValue;
+
+export type executeArgumentsArray = [
+ graphqlTypes.GraphQLSchema,
+ graphqlTypes.DocumentNode,
+ any,
+ any,
+ Maybe<{ [key: string]: any }>,
+ Maybe,
+ Maybe>,
+ Maybe>
+];
+
+export type executeFunctionWithArgs = (
+ schema: graphqlTypes.GraphQLSchema,
+ document: graphqlTypes.DocumentNode,
+ rootValue?: any,
+ contextValue?: any,
+ variableValues?: Maybe<{ [key: string]: any }>,
+ operationName?: Maybe,
+ fieldResolver?: Maybe>,
+ typeResolver?: Maybe>
+) => PromiseOrValue;
+
+export interface OtelExecutionArgs {
+ schema: GraphQLSchema;
+ document: DocumentNode & ObjectWithGraphQLData;
+ rootValue?: any;
+ contextValue?: any & ObjectWithGraphQLData;
+ variableValues?: Maybe<{ [key: string]: any }>;
+ operationName?: Maybe;
+ fieldResolver?: Maybe & OtelPatched>;
+ typeResolver?: Maybe>;
+}
+
+export type executeType = executeFunctionWithObj | executeFunctionWithArgs;
+
+export type parseType = (
+ source: string | graphqlTypes.Source,
+ options?: graphqlTypes.ParseOptions
+) => graphqlTypes.DocumentNode;
+
+export type validateType = (
+ schema: graphqlTypes.GraphQLSchema,
+ documentAST: graphqlTypes.DocumentNode,
+ rules?: ReadonlyArray,
+ typeInfo?: graphqlTypes.TypeInfo,
+ options?: { maxErrors?: number }
+) => ReadonlyArray;
+
+export interface GraphQLField {
+ parent: api.Span;
+ span: api.Span;
+ error: Error | null;
+}
+
+interface OtelGraphQLData {
+ source?: any;
+ span: api.Span;
+ fields: { [key: string]: GraphQLField };
+}
+
+export interface ObjectWithOtelSpan {
+ [OTEL_SPAN_SYMBOL]?: api.Span;
+}
+
+export interface ObjectWithGraphQLData {
+ [OTEL_GRAPHQL_DATA_SYMBOL]?: OtelGraphQLData;
+}
+
+export interface OtelPatched {
+ [OTEL_PATCHED_SYMBOL]?: boolean;
+}
+
+export interface GraphQLPath {
+ prev: GraphQLPath | undefined;
+ key: string | number;
+ typename: string | undefined;
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts
new file mode 100644
index 0000000000..74817454d0
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts
@@ -0,0 +1,376 @@
+/*
+ * 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 { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
+import type * as graphqlTypes from 'graphql';
+import * as api from '@opentelemetry/api';
+import type { Maybe } from 'graphql/jsutils/Maybe';
+import { GraphQLObjectType } from 'graphql/type/definition';
+import {
+ AllowedOperationTypes,
+ SpanAttributes,
+ SpanNames,
+ TokenKind,
+} from './enum';
+import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols';
+import {
+ GraphQLField,
+ GraphQLPath,
+ GraphQLInstrumentationConfig,
+ GraphQLInstrumentationParsedConfig,
+ ObjectWithGraphQLData,
+ OtelPatched,
+} from './types';
+
+const OPERATION_VALUES = Object.values(AllowedOperationTypes);
+
+export function addSpanSource(
+ span: api.Span,
+ loc: graphqlTypes.Location,
+ allowValues?: boolean,
+ start?: number,
+ end?: number
+): void {
+ const source = getSourceFromLocation(loc, allowValues, start, end);
+ span.setAttribute(SpanAttributes.SOURCE, source);
+}
+
+function createFieldIfNotExists(
+ tracer: api.Tracer,
+ getConfig: () => GraphQLInstrumentationParsedConfig,
+ contextValue: any,
+ info: graphqlTypes.GraphQLResolveInfo,
+ path: string[]
+): {
+ field: any;
+ spanAdded: boolean;
+} {
+ let field = getField(contextValue, path);
+
+ let spanAdded = false;
+
+ if (!field) {
+ spanAdded = true;
+ const parent = getParentField(contextValue, path);
+
+ field = {
+ parent,
+ span: createResolverSpan(
+ tracer,
+ getConfig,
+ contextValue,
+ info,
+ path,
+ parent.span
+ ),
+ error: null,
+ };
+
+ addField(contextValue, path, field);
+ }
+
+ return { spanAdded, field };
+}
+
+function createResolverSpan(
+ tracer: api.Tracer,
+ getConfig: () => GraphQLInstrumentationParsedConfig,
+ contextValue: any,
+ info: graphqlTypes.GraphQLResolveInfo,
+ path: string[],
+ parentSpan?: api.Span
+): api.Span {
+ const attributes: api.Attributes = {
+ [SpanAttributes.FIELD_NAME]: info.fieldName,
+ [SpanAttributes.FIELD_PATH]: path.join('.'),
+ [SpanAttributes.FIELD_TYPE]: info.returnType.toString(),
+ };
+
+ const span = tracer.startSpan(SpanNames.RESOLVE, {
+ attributes,
+ parent: parentSpan,
+ });
+
+ const document = contextValue[OTEL_GRAPHQL_DATA_SYMBOL].source;
+ const fieldNode = info.fieldNodes.find(
+ fieldNode => fieldNode.kind === 'Field'
+ );
+
+ if (fieldNode) {
+ addSpanSource(
+ span,
+ document.loc,
+ getConfig().allowValues,
+ fieldNode.loc?.start,
+ fieldNode.loc?.end
+ );
+ }
+
+ return span;
+}
+
+export function endSpan(span: api.Span, error?: Error): void {
+ if (error) {
+ span.recordException(error);
+ }
+ span.end();
+}
+
+export function getOperation(
+ document: graphqlTypes.DocumentNode,
+ operationName?: Maybe
+): graphqlTypes.DefinitionNode | undefined {
+ if (!document || !Array.isArray(document.definitions)) {
+ return undefined;
+ }
+
+ if (operationName) {
+ return document.definitions
+ .filter(
+ definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1
+ )
+ .find(
+ definition =>
+ operationName === (definition.name && definition.name.value)
+ );
+ } else {
+ return document.definitions.find(
+ definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1
+ );
+ }
+}
+
+function addField(contextValue: any, path: string[], field: GraphQLField) {
+ return (contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[
+ path.join('.')
+ ] = field);
+}
+
+function getField(contextValue: any, path: string[]) {
+ return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')];
+}
+
+function getParentField(contextValue: any, path: string[]) {
+ for (let i = path.length - 1; i > 0; i--) {
+ const field = getField(contextValue, path.slice(0, i));
+
+ if (field) {
+ return field;
+ }
+ }
+
+ return {
+ span: contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span,
+ };
+}
+
+function pathToArray(mergeItems: boolean, path: GraphQLPath): string[] {
+ const flattened: string[] = [];
+ let curr: GraphQLPath | undefined = path;
+ while (curr) {
+ let key = curr.key;
+
+ if (mergeItems && typeof key === 'number') {
+ key = '*';
+ }
+ flattened.push(String(key));
+ curr = curr.prev;
+ }
+ return flattened.reverse();
+}
+
+function repeatBreak(i: number): string {
+ return repeatChar('\n', i);
+}
+
+function repeatSpace(i: number): string {
+ return repeatChar(' ', i);
+}
+
+function repeatChar(char: string, to: number): string {
+ let text = '';
+ for (let i = 0; i < to; i++) {
+ text += char;
+ }
+ return text;
+}
+
+const KindsToBeRemoved: string[] = [
+ TokenKind.FLOAT,
+ TokenKind.STRING,
+ TokenKind.INT,
+ TokenKind.BLOCK_STRING,
+];
+
+export function getSourceFromLocation(
+ loc: graphqlTypes.Location,
+ allowValues = false,
+ start: number = loc.start,
+ end: number = loc.end
+): string {
+ let source = '';
+
+ if (loc.startToken) {
+ let next: graphqlTypes.Token | null = loc.startToken.next;
+ let previousLine: number | undefined = 1;
+ while (next) {
+ if (next.start < start) {
+ next = next.next;
+ previousLine = next?.line;
+ continue;
+ }
+ if (next.end > end) {
+ next = next.next;
+ previousLine = next?.line;
+ continue;
+ }
+ let value = next.value || next.kind;
+ let space = '';
+ if (!allowValues && KindsToBeRemoved.indexOf(next.kind) >= 0) {
+ // value = repeatChar('*', value.length);
+ value = '*';
+ }
+ if (next.kind === TokenKind.STRING) {
+ value = `"${value}"`;
+ }
+ if (next.kind === TokenKind.EOF) {
+ value = '';
+ }
+ if (next.line > previousLine!) {
+ source += repeatBreak(next.line - previousLine!);
+ previousLine = next.line;
+ space = repeatSpace(next.column - 1);
+ } else {
+ if (next.line === next.prev?.line) {
+ space = repeatSpace(next.start - (next.prev?.end || 0));
+ }
+ }
+ source += space + value;
+ if (next) {
+ next = next.next!;
+ }
+ }
+ }
+
+ return source;
+}
+
+export function wrapFields(
+ type: Maybe,
+ tracer: api.Tracer,
+ getConfig: () => GraphQLInstrumentationParsedConfig
+): void {
+ if (
+ !type ||
+ typeof type.getFields !== 'function' ||
+ type[OTEL_PATCHED_SYMBOL]
+ ) {
+ return;
+ }
+ const fields = type.getFields();
+
+ type[OTEL_PATCHED_SYMBOL] = true;
+
+ Object.keys(fields).forEach(key => {
+ const field = fields[key];
+
+ if (!field) {
+ return;
+ }
+
+ if (field.resolve) {
+ field.resolve = wrapFieldResolver(tracer, getConfig, field.resolve);
+ }
+
+ if (field.type) {
+ let unwrappedType: any = field.type;
+
+ while (unwrappedType.ofType) {
+ unwrappedType = unwrappedType.ofType;
+ }
+ wrapFields(unwrappedType, tracer, getConfig);
+ }
+ });
+}
+
+export function wrapFieldResolver(
+ tracer: api.Tracer,
+ getConfig: () => Required,
+ fieldResolver: Maybe<
+ graphqlTypes.GraphQLFieldResolver & OtelPatched
+ >
+): graphqlTypes.GraphQLFieldResolver & OtelPatched {
+ if (
+ (wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] ||
+ typeof fieldResolver !== 'function'
+ ) {
+ return fieldResolver!;
+ }
+
+ function wrappedFieldResolver(
+ this: graphqlTypes.GraphQLFieldResolver,
+ source: TSource,
+ args: TArgs,
+ contextValue: TContext & ObjectWithGraphQLData,
+ info: graphqlTypes.GraphQLResolveInfo
+ ) {
+ if (!fieldResolver) {
+ return undefined;
+ }
+ const config = getConfig();
+
+ if (!contextValue[OTEL_GRAPHQL_DATA_SYMBOL]) {
+ return fieldResolver.call(this, source, args, contextValue, info);
+ }
+ const path = pathToArray(config.mergeItems, info && info.path);
+ const depth = path.filter((item: any) => typeof item === 'string').length;
+
+ let field: any;
+ let shouldEndSpan = false;
+ if (config.depth >= 0 && config.depth < depth) {
+ field = getParentField(contextValue, path);
+ } else {
+ const newField = createFieldIfNotExists(
+ tracer,
+ getConfig,
+ contextValue,
+ info,
+ path
+ );
+ field = newField.field;
+ shouldEndSpan = newField.spanAdded;
+ }
+
+ return tracer.withSpan(field.span, () => {
+ return safeExecuteInTheMiddle<
+ Maybe>
+ >(
+ () => {
+ return fieldResolver.call(this, source, args, contextValue, info);
+ },
+ err => {
+ if (shouldEndSpan) {
+ endSpan(field.span, err);
+ }
+ }
+ );
+ });
+ }
+
+ (wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] = true;
+
+ return wrappedFieldResolver;
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/version.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/version.ts
new file mode 100644
index 0000000000..714520138d
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/src/version.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.
+ */
+
+// this is autogenerated file, see scripts/version-update.js
+export const VERSION = '0.11.0';
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/test/graphql.test.ts b/plugins/node/opentelemetry-instrumentation-graphql/test/graphql.test.ts
new file mode 100644
index 0000000000..0b05712e63
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/test/graphql.test.ts
@@ -0,0 +1,1124 @@
+/*
+ * 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 {
+ BasicTracerProvider,
+ InMemorySpanExporter,
+ ReadableSpan,
+ SimpleSpanProcessor,
+} from '@opentelemetry/tracing';
+import * as assert from 'assert';
+import { GraphQLInstrumentation } from '../src';
+import { SpanAttributes, SpanNames } from '../src/enum';
+import { GraphQLInstrumentationConfig } from '../src/types';
+import { assertResolveSpan } from './helper';
+
+const defaultConfig: GraphQLInstrumentationConfig = {};
+const graphQLInstrumentation = new GraphQLInstrumentation(defaultConfig);
+graphQLInstrumentation.enable();
+graphQLInstrumentation.disable();
+
+// now graphql can be required
+
+import { buildSchema } from './schema';
+import { graphql } from 'graphql';
+// Construct a schema, using GraphQL schema language
+const schema = buildSchema();
+
+const sourceList1 = `
+ query {
+ books {
+ name
+ }
+ }
+`;
+
+const sourceBookById = `
+ query {
+ book(id: 0) {
+ name
+ }
+ }
+`;
+
+const sourceAddBook = `
+ mutation {
+ addBook(
+ name: "Fifth Book"
+ authorIds: "0,2"
+ ) {
+ id
+ }
+ }
+`;
+
+const sourceFindUsingVariable = `
+ query Query1 ($id: Int!) {
+ book(id: $id) {
+ name
+ }
+ }
+`;
+
+const badQuery = `
+ query foo bar
+`;
+
+const queryInvalid = `
+ query {
+ book(id: "a") {
+ name
+ }
+ }
+`;
+
+const exporter = new InMemorySpanExporter();
+const provider = new BasicTracerProvider();
+provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
+graphQLInstrumentation.setTracerProvider(provider);
+
+describe('graphql', () => {
+ function create(config: GraphQLInstrumentationConfig = {}) {
+ graphQLInstrumentation.setConfig(config);
+ graphQLInstrumentation.enable();
+ }
+
+ describe('when depth is not set', () => {
+ describe('AND source is query to get a list of books', () => {
+ let spans: ReadableSpan[];
+ beforeEach(async () => {
+ create({});
+ await graphql(schema, sourceList1);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 7 spans', () => {
+ assert.deepStrictEqual(spans.length, 7);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+ const span2 = spans[5];
+ const span3 = spans[6];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'books',
+ 'books',
+ '[Book]',
+ 'books {\n' + ' name\n' + ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(
+ span1,
+ 'name',
+ 'books.0.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ assertResolveSpan(
+ span2,
+ 'name',
+ 'books.1.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ assertResolveSpan(
+ span3,
+ 'name',
+ 'books.2.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ });
+ });
+ describe('AND source is query with param', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({});
+ await graphql(schema, sourceBookById);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: *) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: *) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'book',
+ 'book',
+ 'Book',
+ 'book(id: *) {\n' + ' name\n' + ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(
+ span1,
+ 'name',
+ 'book.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ });
+ });
+ describe('AND source is query with param and variables', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({});
+ await graphql(schema, sourceFindUsingVariable, null, null, {
+ id: 2,
+ });
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query Query1 ($id: Int!) {\n' +
+ ' book(id: $id) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query Query1 ($id: Int!) {\n' +
+ ' book(id: $id) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[`${SpanAttributes.VARIABLES}id`],
+ undefined
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'book',
+ 'book',
+ 'Book',
+ 'book(id: $id) {\n' + ' name\n' + ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(
+ span1,
+ 'name',
+ 'book.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ });
+ });
+ });
+
+ describe('when depth is set to 0', () => {
+ describe('AND source is query to get a list of books', () => {
+ let spans: ReadableSpan[];
+ beforeEach(async () => {
+ create({
+ depth: 0,
+ });
+ await graphql(schema, sourceList1);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 3 spans', () => {
+ assert.deepStrictEqual(spans.length, 3);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[2];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+ });
+ });
+
+ describe('when mergeItems is set to true', () => {
+ describe('AND source is query to get a list of books', () => {
+ let spans: ReadableSpan[];
+ beforeEach(async () => {
+ create({
+ mergeItems: true,
+ });
+ await graphql(schema, sourceList1);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' books {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+ });
+
+ describe('AND depth is set to 0', () => {
+ let spans: ReadableSpan[];
+ beforeEach(async () => {
+ create({
+ mergeItems: true,
+ depth: 0,
+ });
+ await graphql(schema, sourceList1);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 3 spans', () => {
+ assert.deepStrictEqual(spans.length, 3);
+ });
+ });
+ });
+
+ describe('when allowValues is set to true', () => {
+ describe('AND source is query with param', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({
+ allowValues: true,
+ });
+ await graphql(schema, sourceBookById);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: 0) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: 0) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'book',
+ 'book',
+ 'Book',
+ 'book(id: 0) {\n' + ' name\n' + ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(
+ span1,
+ 'name',
+ 'book.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ });
+ });
+ describe('AND mutation is called', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({
+ allowValues: true,
+ });
+ await graphql(schema, sourceAddBook);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' mutation {\n' +
+ ' addBook(\n' +
+ ' name: "Fifth Book"\n' +
+ ' authorIds: "0,2"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' mutation {\n' +
+ ' addBook(\n' +
+ ' name: "Fifth Book"\n' +
+ ' authorIds: "0,2"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'mutation'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'addBook',
+ 'addBook',
+ 'Book',
+ 'addBook(\n' +
+ ' name: "Fifth Book"\n' +
+ ' authorIds: "0,2"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(span1, 'id', 'addBook.id', 'Int', 'id', parentId);
+ });
+ });
+ describe('AND source is query with param and variables', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({
+ allowValues: true,
+ });
+ await graphql(schema, sourceFindUsingVariable, null, null, {
+ id: 2,
+ });
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query Query1 ($id: Int!) {\n' +
+ ' book(id: $id) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query Query1 ($id: Int!) {\n' +
+ ' book(id: $id) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'query'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[`${SpanAttributes.VARIABLES}id`],
+ 2
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'book',
+ 'book',
+ 'Book',
+ 'book(id: $id) {\n' + ' name\n' + ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(
+ span1,
+ 'name',
+ 'book.name',
+ 'String',
+ 'name',
+ parentId
+ );
+ });
+ });
+ });
+
+ describe('when mutation is called', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({
+ // allowValues: true
+ });
+ await graphql(schema, sourceAddBook);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 5 spans', () => {
+ assert.deepStrictEqual(spans.length, 5);
+ });
+
+ it('should instrument parse', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' mutation {\n' +
+ ' addBook(\n' +
+ ' name: "*"\n' +
+ ' authorIds: "*"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[3];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' mutation {\n' +
+ ' addBook(\n' +
+ ' name: "*"\n' +
+ ' authorIds: "*"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'mutation'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+
+ it('should instrument resolvers', () => {
+ const executeSpan = spans[3];
+ const resolveParentSpan = spans[2];
+ const span1 = spans[4];
+
+ assertResolveSpan(
+ resolveParentSpan,
+ 'addBook',
+ 'addBook',
+ 'Book',
+ 'addBook(\n' +
+ ' name: "*"\n' +
+ ' authorIds: "*"\n' +
+ ' ) {\n' +
+ ' id\n' +
+ ' }',
+ executeSpan.spanContext.spanId
+ );
+ const parentId = resolveParentSpan.spanContext.spanId;
+ assertResolveSpan(span1, 'id', 'addBook.id', 'Int', 'id', parentId);
+ });
+ });
+
+ describe('when query is not correct', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({});
+ await graphql(schema, badQuery);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 1 span', () => {
+ assert.deepStrictEqual(spans.length, 1);
+ });
+
+ it('should instrument parse with error', () => {
+ const parseSpan = spans[0];
+ const event = parseSpan.events[0];
+
+ assert.ok(event);
+
+ assert.deepStrictEqual(
+ event.attributes!['exception.type'],
+ 'GraphQLError'
+ );
+ assert.ok(event.attributes!['exception.message']);
+ assert.ok(event.attributes!['exception.stacktrace']);
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+ });
+
+ describe('when query is correct but cannot be validated', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({});
+ await graphql(schema, queryInvalid);
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 2 spans', () => {
+ assert.deepStrictEqual(spans.length, 2);
+ });
+
+ it('should instrument parse with error', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: "*") {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ const event = validateSpan.events[0];
+
+ assert.deepStrictEqual(event.name, 'exception');
+ assert.deepStrictEqual(
+ event.attributes!['exception.type'],
+ SpanAttributes.ERROR_VALIDATION_NAME
+ );
+ assert.ok(event.attributes!['exception.message']);
+ });
+ });
+
+ describe('when query operation is not supported', () => {
+ let spans: ReadableSpan[];
+
+ beforeEach(async () => {
+ create({});
+ await graphql({
+ schema,
+ source: sourceBookById,
+ operationName: 'foo',
+ });
+ spans = exporter.getFinishedSpans();
+ });
+
+ afterEach(() => {
+ exporter.reset();
+ graphQLInstrumentation.disable();
+ spans = [];
+ });
+
+ it('should have 3 spans', () => {
+ assert.deepStrictEqual(spans.length, 3);
+ });
+
+ it('should instrument parse with error', () => {
+ const parseSpan = spans[0];
+ assert.deepStrictEqual(
+ parseSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: *) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
+ });
+
+ it('should instrument validate', () => {
+ const parseSpan = spans[0];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
+ assert.deepStrictEqual(
+ validateSpan.parentSpanId,
+ parseSpan.spanContext.spanId
+ );
+ const event = validateSpan.events[0];
+
+ assert.ok(!event);
+ });
+
+ it('should instrument execute', () => {
+ const executeSpan = spans[2];
+ const validateSpan = spans[1];
+
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.SOURCE],
+ '\n' +
+ ' query {\n' +
+ ' book(id: *) {\n' +
+ ' name\n' +
+ ' }\n' +
+ ' }\n'
+ );
+ assert.deepStrictEqual(
+ executeSpan.attributes[SpanAttributes.OPERATION],
+ 'Operation "foo" not supported'
+ );
+ assert.deepStrictEqual(executeSpan.name, SpanNames.EXECUTE);
+ assert.deepStrictEqual(
+ executeSpan.parentSpanId,
+ validateSpan.spanContext.spanId
+ );
+ });
+ });
+});
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/test/helper.ts b/plugins/node/opentelemetry-instrumentation-graphql/test/helper.ts
new file mode 100644
index 0000000000..f633d6bd82
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/test/helper.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { ReadableSpan } from '@opentelemetry/tracing';
+import * as assert from 'assert';
+import { SpanAttributes, SpanNames } from '../src/enum';
+
+export function assertResolveSpan(
+ span: ReadableSpan,
+ fieldName: string,
+ fieldPath: string,
+ fieldType: string,
+ source: string,
+ parentSpanId?: string
+) {
+ const attrs = span.attributes;
+ assert.deepStrictEqual(span.name, SpanNames.RESOLVE);
+ assert.deepStrictEqual(attrs[SpanAttributes.FIELD_NAME], fieldName);
+ assert.deepStrictEqual(attrs[SpanAttributes.FIELD_PATH], fieldPath);
+ assert.deepStrictEqual(attrs[SpanAttributes.FIELD_TYPE], fieldType);
+ assert.deepStrictEqual(attrs[SpanAttributes.SOURCE], source);
+ if (parentSpanId) {
+ assert.deepStrictEqual(span.parentSpanId, parentSpanId);
+ }
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/test/schema.ts b/plugins/node/opentelemetry-instrumentation-graphql/test/schema.ts
new file mode 100644
index 0000000000..a55982c6c6
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/test/schema.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 https from 'https';
+import * as graphql from 'graphql';
+
+const url1 =
+ 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/package.json';
+
+function getData(url: string): any {
+ return new Promise((resolve, reject) => {
+ https
+ .get(url, response => {
+ let data = '';
+ response.on('data', chunk => {
+ data += chunk;
+ });
+ response.on('end', () => {
+ resolve(JSON.parse(data));
+ });
+ })
+ .on('error', err => {
+ reject(err);
+ });
+ });
+}
+
+const authors: Author[] = [];
+const books: Book[] = [];
+
+interface Book {
+ id: number;
+ name: string;
+ authorIds: number[];
+}
+
+interface Address {
+ country: string;
+ city: string;
+}
+
+interface Author {
+ id: number;
+ name: string;
+ address: Address;
+}
+
+function addBook(name: string, authorIds: string | number[] = []) {
+ if (typeof authorIds === 'string') {
+ authorIds = authorIds.split(',').map(id => parseInt(id, 10));
+ }
+ const id = books.length;
+ books.push({
+ id: id,
+ name: name,
+ authorIds: authorIds,
+ });
+ return books[books.length - 1];
+}
+
+function addAuthor(name: string, country: string, city: string) {
+ const id = authors.length;
+ authors.push({ id, name, address: { country, city } });
+ return authors[authors.length - 1];
+}
+
+function getBook(id: number) {
+ return books[id];
+}
+
+function getAuthor(id: number) {
+ return authors[id];
+}
+
+function prepareData() {
+ addAuthor('John', 'Poland', 'Szczecin');
+ addAuthor('Alice', 'Poland', 'Warsaw');
+ addAuthor('Bob', 'England', 'London');
+ addAuthor('Christine', 'France', 'Paris');
+ addBook('First Book', [0, 1]);
+ addBook('Second Book', [2]);
+ addBook('Third Book', [3]);
+}
+
+prepareData();
+
+export function buildSchema() {
+ const Author = new graphql.GraphQLObjectType({
+ name: 'Author',
+ fields: {
+ id: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return obj.id;
+ },
+ },
+ name: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return obj.name;
+ },
+ },
+ description: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return new Promise((resolve, reject) => {
+ getData(url1).then((response: { [key: string]: string }) => {
+ resolve(response.description);
+ }, reject);
+ });
+ },
+ },
+ address: {
+ type: new graphql.GraphQLObjectType({
+ name: 'Address',
+ fields: {
+ country: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return obj.country;
+ },
+ },
+ city: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return obj.city;
+ },
+ },
+ },
+ }),
+ resolve(obj, args) {
+ return obj.address;
+ },
+ },
+ },
+ });
+
+ const Book = new graphql.GraphQLObjectType({
+ name: 'Book',
+ fields: {
+ id: {
+ type: graphql.GraphQLInt,
+ resolve(obj, args) {
+ return obj.id;
+ },
+ },
+ name: {
+ type: graphql.GraphQLString,
+ resolve(obj, args) {
+ return obj.name;
+ },
+ },
+ authors: {
+ type: new graphql.GraphQLList(Author),
+ resolve(obj, args) {
+ return obj.authorIds.map((id: number) => {
+ return authors[id];
+ });
+ },
+ },
+ },
+ });
+
+ const query = new graphql.GraphQLObjectType({
+ name: 'Query',
+ fields: {
+ author: {
+ type: Author,
+ args: {
+ id: { type: graphql.GraphQLInt },
+ },
+ resolve(obj, args, context) {
+ return Promise.resolve(getAuthor(args.id));
+ },
+ },
+ authors: {
+ type: new graphql.GraphQLList(Author),
+ resolve(obj, args, context) {
+ return Promise.resolve(authors);
+ },
+ },
+ book: {
+ type: Book,
+ args: {
+ id: { type: graphql.GraphQLInt },
+ },
+ resolve(obj, args, context) {
+ return Promise.resolve(getBook(args.id));
+ },
+ },
+ books: {
+ type: new graphql.GraphQLList(Book),
+ resolve(obj, args, context) {
+ return Promise.resolve(books);
+ },
+ },
+ },
+ });
+
+ const mutation = new graphql.GraphQLObjectType({
+ name: 'Mutation',
+ fields: {
+ addBook: {
+ type: Book,
+ args: {
+ name: { type: new graphql.GraphQLNonNull(graphql.GraphQLString) },
+ authorIds: {
+ type: new graphql.GraphQLNonNull(graphql.GraphQLString),
+ },
+ },
+ resolve(obj, args, context) {
+ return Promise.resolve(addBook(args.name, args.authorIds));
+ },
+ },
+ },
+ });
+
+ const schema = new graphql.GraphQLSchema({ query, mutation });
+ return schema;
+}
diff --git a/plugins/node/opentelemetry-instrumentation-graphql/tsconfig.json b/plugins/node/opentelemetry-instrumentation-graphql/tsconfig.json
new file mode 100644
index 0000000000..28be80d266
--- /dev/null
+++ b/plugins/node/opentelemetry-instrumentation-graphql/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../../tsconfig.base",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build"
+ },
+ "include": [
+ "src/**/*.ts",
+ "test/**/*.ts"
+ ]
+}