Skip to content

Commit

Permalink
Release @apollo/[email protected] (#845)
Browse files Browse the repository at this point in the history
* Add OpenTelemetry support

This PR adds instrumentation to emit the following spans:

federation.request - total time to server a request.
federation.validate - time to validate the federated query.
federation.plan - time to plan the federated query.
federation.execute - the total time executing query.
federation.fetch - time fetching data from a subgraph.
federation.postprocessing - time to render the response from all the subgraph data.
The instrumentation looks a little messy due to the requirement to set error status on spans on exception, but also if errors are added to the response.

It is a reissue of the following PRs
#803
#828

* WIP on opentelemetry docs

* First run at OT docs ready for review

* Incorporate feedback from @BrynCooke

* Add note to update @apollo/gateway

* Release

 - [email protected]
 - @apollo/[email protected]
 - @apollo/[email protected]

* Update changelog for publish

Co-authored-by: bryncooke <[email protected]>
Co-authored-by: Stephen Barlow <[email protected]>
Co-authored-by: Stephen Barlow <[email protected]>
  • Loading branch information
4 people authored Jun 29, 2021
2 parents d3710a6 + ae9b3e3 commit b12a2d3
Show file tree
Hide file tree
Showing 15 changed files with 933 additions and 241 deletions.
6 changes: 5 additions & 1 deletion docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ module.exports = {
'enterprise-guide/federation-case-studies',
'enterprise-guide/additional-resources',
],
Debugging: ['errors', 'metrics'],
'Debugging & Metrics': [
'errors',
'metrics',
'opentelemetry',
],
'Third-Party Support': ['other-servers', 'federation-spec'],
'API Reference': ['api/apollo-federation', 'api/apollo-gateway'],
},
Expand Down
2 changes: 1 addition & 1 deletion docs/source/metrics.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Federated traces
title: Federated trace data
description: How federated tracing works
---

Expand Down
239 changes: 239 additions & 0 deletions docs/source/opentelemetry.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
---
title: OpenTelemetry in Apollo Server
sidebar_title: OpenTelemetry
---

import {
ExpansionPanel,
} from 'gatsby-theme-apollo-docs/src/components/expansion-panel';

[OpenTelemetry](https://opentelemetry.io/) is a collection of open-source tools for generating and processing telemetry data (such as logs and metrics) from different systems in a generic, consistent way.

You can configure your gateway, your individual subgraphs, or even a monolothic Apollo Server instance to emit telemetry related to processing GraphQL operations.

Additionally, the `@apollo/gateway` library provides built-in OpenTelemetry instrumentation to emit [gateway-specific spans](#gateway-specific-spans) for operation traces.

> Apollo Studio does _not_ currently consume OpenTelemetry-formatted data. To push trace data to Studio, see [Federated trace data](./metrics/).
>
> You should configure OpenTelemetry if you want to push trace data to an OpenTelemetry-compatible system, such as [Zipkin](https://zipkin.io/) or [Jaeger](https://www.jaegertracing.io/).
## Setup

### 1. Install required libraries

To use OpenTelemetry in your application, you need to install a baseline set of `@opentelemetry` Node.js libraries. This set differs slightly depending on whether you're setting up your federated gateway or a subgraph/monolith.

<ExpansionPanel title="Gateway libraries">

```bash
npm install \
@opentelemetry/api \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected]
```

</ExpansionPanel>

<ExpansionPanel title="Subgraph/monolith libraries">

```bash
npm install \
@opentelemetry/api \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected]
```

</ExpansionPanel>

**Most importantly, subgraphs and monoliths _must_ install `@opentelemetry/instrumentation-graphql`, and gateways _must not_ install it.**

As shown, most `@opentelemetry` libraries should maintain a consistent version number to prevent dependency conflicts. As of this article's most recent update, the latest version is `0.22`.

#### Update `@apollo/gateway`

If you're using OpenTelemetry in your federated gateway, also update the `@apollo/gateway` library to version `0.31.0` or later to add support for [gateway-specific spans](#gateway-specific-spans).

### 2. Configure instrumentation

Next, update your application to configure your OpenTelemetry instrumentation _as early as possible in your app's execution_. This _must_ occur before you even import `apollo-server`, `express`, or `http`. Otherwise, your trace data will be incomplete.

**We recommend putting this configuration in its own file**, which you import at the very top of `index.js`. A sample file is provided below (note the lines that should either be deleted or uncommented).

```js:title=open-telemetry.js
// Import required symbols
const { HttpInstrumentation } = require ('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require ('@opentelemetry/instrumentation-express');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { NodeTracerProvider } = require("@opentelemetry/node");
const { SimpleSpanProcessor, ConsoleSpanExporter } = require ("@opentelemetry/tracing");
const { Resource } = require('@opentelemetry/resources');
// **DELETE IF SETTING UP A GATEWAY, UNCOMMENT OTHERWISE**
// const { GraphQLInstrumentation } = require ('@opentelemetry/instrumentation-graphql');

// Register server-related instrumentation
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
// **DELETE IF SETTING UP A GATEWAY, UNCOMMENT OTHERWISE**
//new GraphQLInstrumentation()
]
});

// Initialize provider and identify this particular service
// (in this case, we're implementing a federated gateway)
const provider = new NodeTracerProvider({
resource: Resource.default().merge(new Resource({
// Replace with any string to identify this service in your system
"service.name": "gateway",
})),
});

// Configure a test exporter to print all traces to the console
const consoleExporter = new ConsoleSpanExporter();
provider.addSpanProcessor(
new SimpleSpanProcessor(consoleExporter)
);

// Register the provider to begin tracing
provider.register();
```

For now, this code does _not_ push trace data to an external system. Instead, it prints that data to the console for debugging purposes.


After you make these changes to your app, start it up locally. It should begin printing trace data similar to the following:

<ExpansionPanel title="Click to expand">

```js
{
traceId: '0ed36c42718622cc726a661a3328aa61',
parentId: undefined,
name: 'HTTP POST',
id: '36c6a3ae19563ec3',
kind: 1,
timestamp: 1624650903925787,
duration: 26793,
attributes: {
'http.url': 'http://localhost:4000/',
'http.host': 'localhost:4000',
'net.host.name': 'localhost',
'http.method': 'POST',
'http.route': '',
'http.target': '/',
'http.user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
'http.request_content_length_uncompressed': 1468,
'http.flavor': '1.1',
'net.transport': 'ip_tcp',
'net.host.ip': '::1',
'net.host.port': 4000,
'net.peer.ip': '::1',
'net.peer.port': 39722,
'http.status_code': 200,
'http.status_text': 'OK'
},
status: { code: 1 },
events: []
}

{
traceId: '0ed36c42718622cc726a661a3328aa61',
parentId: '36c6a3ae19563ec3',
name: 'middleware - <anonymous>',
id: '3776786d86f24124',
kind: 0,
timestamp: 1624650903934147,
duration: 63,
attributes: {
'http.route': '/',
'express.name': '<anonymous>',
'express.type': 'middleware'
},
status: { code: 0 },
events: []
}
```

</ExpansionPanel>

Nice! Next, we can modify this code to begin pushing trace data to an external service, such as Zipkin or Jaeger.

### 3. Push trace data to a tracing system

Next, let's modify the code in the [previous step](#2-configure-instrumentation) to instead push traces to a locally running instance of [Zipkin](https://zipkin.io/).

> To run Zipkin locally, [see the quickstart](https://zipkin.io/pages/quickstart.html). If you want to use a different tracing system, consult the documentation for that system.
First, we need to replace our `ConsoleSpanExporter` (which prints traces to the terminal) with a `ZipkinExporter`, which specifically pushes trace data to a running Zipkin instance.

Install the following additional library:

```bash
npm install @opentelemetry/[email protected]
```

Then, import the `ZipkinExporter` in your dedicated OpenTelemetry file:

```js:title=open-telemetry.js
const { ZipkinExporter } = require("@opentelemetry/exporter-zipkin");
```

Now we can replace our `ConsoleSpanExporter` with a `ZipkinExporter`. Replace lines 27-21 of the code in [the previous step](#2-configure-instrumentation) with the following:

```js
// Configure an exporter that pushes all traces to Zipkin
// (This assumes Zipkin is running on localhost at the
// default port of 9411)
const zipkinExporter = new ZipkinExporter({
// url: set_this_if_not_running_zipkin_locally
});
provider.addSpanProcessor(
new SimpleSpanProcessor(zipkinExporter)
);
```

Now, open Zipkin in your browser at `http://localhost:9411`. You should now be able to query recent trace data in the UI!

You can show the details of any operation and see a breakdown of its processing timeline by span.

### 4. Update for production readiness

Our example telemetry configuration assumes that Zipkin is running locally, and that we want to process every span individually as it's emitted.

To prepare for production, we probably at least want to conditionally set the `url` option for the `ZipkinExporter` and replace our `SimpleSpanProcessor` with a `BatchSpanProcessor`.

You can learn more about the settings available to these and other OpenTelemetry objects in the [getting started guide](https://github.com/open-telemetry/opentelemetry-js/tree/main/getting-started).

## GraphQL-specific spans

The `@opentelemetry/instrumentation-graphql` library enables subgraphs and monoliths to emit the following spans as part of [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/data-sources/#traces):

> **Reminder:** Federated gateways _must not_ install the `@opentelemetry/instrumentation-graphql` library, so these spans are _not_ included in its traces.
| Name | Description |
|------|-------------|
| `graphql.parse` | The amount of time the server spent parsing an operation string. |
| `graphql.validate` | The amount of time the server spent validating an operation string. |
| `graphql.execute` | The total amount of time the server spent executing an operation. |
| `graphql.resolve` | The amount of time the gateway spent resolving a particular field. |

Note that not every GraphQL span appears in every operation trace. This is because Apollo server can _skip_ parsing or validating an operation string if that string is available in the operation cache.

## Gateway-specific spans

The `@apollo/gateway` library emits the following spans as part of [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/data-sources/#traces):

| Name | Description |
|------|-------------|
| `gateway.request` | The total amount of time the gateway spent serving a request. |
| `gateway.validate` | The amount of time the gateway spent validating a GraphQL operation string. |
| `gateway.plan` | The amount of time the gateway spent generating a query plan for a validated operation. |
| `gateway.execute` | The amount of time the gateway spent executing operations on subgraphs. |
| `gateway.fetch` | The amount of time the gateway spent fetching data from a particular subgraph. |
| `gateway.postprocessing` | The amount of time the gateway spent composing a complete response from individual subgraph responses. |
2 changes: 1 addition & 1 deletion federation-integration-testsuite-js/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "apollo-federation-integration-testsuite",
"private": true,
"version": "0.25.1",
"version": "0.25.2",
"description": "Apollo Federation Integrations / Test Fixtures",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { queryPlanSerializer } from '@apollo/query-planner';
export { default as selectionSetSerializer } from './selectionSetSerializer';
export { default as typeSerializer } from './typeSerializer';
export { default as graphqlErrorSerializer } from './graphqlErrorSerializer';
export { default as spanSerializer } from './readableSpanArraySerializer';

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Plugin } from 'pretty-format';
import { ReadableSpan } from '@opentelemetry/tracing'

export default {
test(value: any) {
return value && Array.isArray(value) && value.length > 0 && isReadableSpan(value[0]);
},

print(spans: ReadableSpan[]) {
// This method takes an array of spans and builds up a set of trees containing only the information we are interested in.
// Spans contain the ID of it's parent, unless it is a root span with no parent.
//
// The simplified data model is called a span mirror.
//
// The algorithm is:
// 1. create span mirrors for every span and place it in a map.
// For each span:
// 1. find the corresponding mirrors for itself and parent.
// 2. push the parent in to the parent's children array.

const root = {children:[]};
const spanMirrors = new Map<ReadableSpan | undefined, any>();
spanMirrors.set(undefined, root);
spans.forEach((s) => spanMirrors.set(s, {name: s.name, attributes:s.attributes, children: [], status: s.status}));
spans.forEach((s) => {
const spanMirror = spanMirrors.get(s);
const parentSpan = spans.find((s2) => s2.spanContext().spanId === s.parentSpanId);
const parentSpanMirror = spanMirrors.get(parentSpan);
parentSpanMirror.children.push(spanMirror);
});

return JSON.stringify(root.children, undefined, 2);
},
} as Plugin;

function isReadableSpan(arg: any): arg is ReadableSpan {
let isSpan = arg && 'kind' in arg && 'startTime' in arg && 'parentSpanId' in arg;
return isSpan;
}
4 changes: 3 additions & 1 deletion gateway-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section.
- _Nothing yet! Stay tuned!_
## v0.31.0

- OpenTelemetry intrumentation. [836](https://github.com/apollographql/federation/pull/836)

## v0.30.0

Expand Down
3 changes: 2 additions & 1 deletion gateway-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apollo/gateway",
"version": "0.30.0",
"version": "0.31.0",
"description": "Apollo Gateway",
"author": "Apollo <[email protected]>",
"main": "dist/index.js",
Expand Down Expand Up @@ -40,6 +40,7 @@
"pretty-format": "^26.0.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0",
"graphql": "^14.5.0 || ^15.0.0"
}
}
Loading

0 comments on commit b12a2d3

Please sign in to comment.