diff --git a/examples/graphql/README.md b/examples/graphql/README.md index cc25573219..b4a9ba2826 100644 --- a/examples/graphql/README.md +++ b/examples/graphql/README.md @@ -46,6 +46,12 @@ npm install ``` 5. You can also write your own queries, open page `http://localhost:4000/graphql` +6. You can also test a `graphql-transform-federation` + ```shell script + # from this directory + npm run server:federation + npm run client:federation + ``` ## Useful links diff --git a/examples/graphql/client-federation.js b/examples/graphql/client-federation.js new file mode 100644 index 0000000000..7342697a6a --- /dev/null +++ b/examples/graphql/client-federation.js @@ -0,0 +1,44 @@ +'use strict'; + +const url = require('url'); +const http = require('http'); +// Construct a schema, using GraphQL schema language + +const source = ` +{ + continents { + code + name + } +} +`; + +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/countries-service.js b/examples/graphql/countries-service.js new file mode 100644 index 0000000000..5e41e86533 --- /dev/null +++ b/examples/graphql/countries-service.js @@ -0,0 +1,31 @@ +'use strict'; + +const { fetch } = require('cross-fetch'); +const { print } = require('graphql'); +const { wrapSchema, introspectSchema } = require('@graphql-tools/wrap'); +const { transformSchemaFederation } = require('graphql-transform-federation'); + +const executor = async ({ document, variables }) => { + const query = print(document); + const fetchResult = await fetch('https://countries.trevorblades.com/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query, variables }), + }); + return fetchResult.json(); +}; + +module.exports = async () => { + const schema = wrapSchema({ + schema: await introspectSchema(executor), + executor, + }); + + return transformSchemaFederation(schema, { + Query: { + extend: true, + }, + }); +}; diff --git a/examples/graphql/package.json b/examples/graphql/package.json index c9213ba302..273d253b4d 100644 --- a/examples/graphql/package.json +++ b/examples/graphql/package.json @@ -6,11 +6,13 @@ "main": "index.js", "scripts": { "client": "node ./client.js", + "client:federation": "node ./client-federation.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:apollo": "node ./server-apollo.js", "server:express": "node ./server-express.js", - "server:apollo": "node ./server-apollo.js" + "server:federation": "node ./server-federation.js" }, "repository": { "type": "git", @@ -31,18 +33,22 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { + "@apollo/gateway": "^0.19.1", + "@graphql-tools/wrap": "^6.0.18", "@opentelemetry/api": "^1.0.2", "@opentelemetry/exporter-collector": "^0.25.0", "@opentelemetry/instrumentation": "^0.25.0", - "@opentelemetry/instrumentation-express": "^0.23.0", - "@opentelemetry/instrumentation-graphql": "^0.23.0", + "@opentelemetry/instrumentation-express": "^0.24.0", + "@opentelemetry/instrumentation-graphql": "^0.24.0", "@opentelemetry/instrumentation-http": "^0.25.0", - "@opentelemetry/sdk-trace-node": "^0.25.0", - "@opentelemetry/sdk-trace-base": "^0.25.0", + "@opentelemetry/node": "^0.25.0", + "@opentelemetry/tracing": "^0.25.0", "apollo-server": "^2.18.1", + "cross-fetch": "^3.0.5", "express": "^4.17.1", "express-graphql": "^0.11.0", - "graphql": "^15.3.0" + "graphql": "^15.3.0", + "graphql-transform-federation": "^2.1.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", "devDependencies": { diff --git a/examples/graphql/server-federation.js b/examples/graphql/server-federation.js new file mode 100644 index 0000000000..2c85bce722 --- /dev/null +++ b/examples/graphql/server-federation.js @@ -0,0 +1,54 @@ +'use strict'; + +require('./tracer'); + +const { ApolloServer } = require('apollo-server'); +const { + ApolloGateway, + RemoteGraphQLDataSource, + LocalGraphQLDataSource, +} = require('@apollo/gateway'); + +const getCountriesSchema = require('./countries-service'); + +const setupGateway = async () => { + const countriesSchema = await getCountriesSchema(); + + const gateway = new ApolloGateway({ + serviceList: [{ name: 'countries', url: 'http://countries' }], + + // Experimental: Enabling this enables the query plan view in Playground. + __exposeQueryPlanExperimental: false, + + buildService: ({ url }) => { + if (url === 'http://countries') { + return new LocalGraphQLDataSource(countriesSchema); + } + return new RemoteGraphQLDataSource({ + url, + }); + }, + }); + + return gateway; +}; + +(async () => { + const gateway = await setupGateway(); + + const server = new ApolloServer({ + gateway, + + // Apollo Graph Manager (previously known as Apollo Engine) + // When enabled and an `ENGINE_API_KEY` is set in the environment, + // provides metrics, schema management and trace reporting. + engine: false, + + // Subscriptions are unsupported but planned for a future Gateway version. + subscriptions: false, + }); + + server.listen().then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); +})(); diff --git a/package.json b/package.json index 69587ea34e..635e7006c7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "lint": "lerna run lint", "lint:fix": "lerna run lint:fix", "lint:examples": "eslint ./examples/**/*.js", - "lint:examples:fix": "eslint ./examples/**/*.js --fix" + "lint:examples:fix": "eslint ./examples/**/*.js --fix", + "lerna:scope": "lerna bootstrap --include-dependents --include-dependencies --scope" }, "keywords": [ "opentelemetry", diff --git a/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts b/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts index eba55bfd1f..930dd11dbb 100644 --- a/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-graphql/src/utils.ts @@ -34,7 +34,7 @@ const OPERATION_VALUES = Object.values(AllowedOperationTypes); export function addSpanSource( span: api.Span, - loc: graphqlTypes.Location, + loc?: graphqlTypes.Location, allowValues?: boolean, start?: number, end?: number @@ -212,14 +212,17 @@ const KindsToBeRemoved: string[] = [ ]; export function getSourceFromLocation( - loc: graphqlTypes.Location, + loc?: graphqlTypes.Location, allowValues = false, - start: number = loc.start, - end: number = loc.end + inputStart?: number, + inputEnd?: number ): string { let source = ''; - if (loc.startToken) { + if (loc?.startToken) { + const start = typeof inputStart === 'number' ? inputStart : loc.start; + const end = typeof inputEnd === 'number' ? inputEnd : loc.end; + let next: graphqlTypes.Token | null = loc.startToken.next; let previousLine: number | undefined = 1; while (next) {