Skip to content

Commit

Permalink
health checks: configure path in apollo-server, doc better
Browse files Browse the repository at this point in the history
Our health check feature is kind of bizarre. It doesn't actually check
that your GraphQL server can, well, serve GraphQL: it just checks if it
can serve HTTP. And the way it matches path names doesn't work very well
in contexts like Lambda where your whole server is often mounted at a
sub-path. In many cases, it would be better to just run a trivial
GraphQL operation as a health check.

Additionally, it's strange that we let you configure the functionality
of the health check in framework integrations, when it would be just as
easy to define your own handler in your framework.

I suppose it's nice that the health check exists in the
batteries-included `apollo-server` package, since you do often want to
have a health check (though again, running a GraphQL operation is often
better) and that package doesn't let you tweak HTTP serving directly.
And it's a bit of a shame that you can't set the health check path in
that package.

So, this PR:

- Adds a `healthCheckPath` constructor option to the `apollo-server`
  package. You can also pass `null` to disable the health check, which
  wasn't previously possible with that package. This is based on work by
  @jutaz though the PR has been thoroughly reworked since then.
- Reorganizes the `ApolloServer` reference docs so that the
  `apollo-server`-specific options are all together, and documents both
  the new option and the accidentally-undocumented `onHealthCheck`.
- Rewrites the health check docs to encourage the use of GraphQL-level
  health checks, and to separate guidance around HTTP-level health
  checks in `apollo-server` (you can tweak it a bit if you want!) from
  HTTP-level health checks in framework integration packages (if you're
  doing something fancy just use the framework directly!)

Fixes #3577. Fixes #5731.
  • Loading branch information
glasser committed Oct 1, 2021
1 parent 0163a5f commit 877b19e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The version headers in this history reflect the versions of Apollo Server itself
## v3.3.0

- `apollo-server-core`: Error handling when a `serverWillStop` callback invoked by `server.stop()` (or `gateway.stop()`) throws is now consistent: the original call to `server.stop()` throws the error, and any concurrent and subsequent calls to `server.stop()` throw the same error. Prior to Apollo Server v2.22.0, the original call threw the error and the behavior of concurrent and subsequent calls was undefined (in practice, it would call shutdown handlers a second time). Apollo Server v2.22.0 intended to put these semantics into place where all three kinds of calls would throw, but due to bugs, the original call would return without error and concurrent calls would hang. (Subsequent calls would correctly throw the error.) In addition, errors thrown by the `drainServer` hook introduced in Apollo Server v3.2.0 are now handled in the same way. [Issue #5649](https://github.com/apollographql/apollo-server/issues/5649) [PR #5653](https://github.com/apollographql/apollo-server/pull/5653)
- `apollo-server`: You can now configure the health check URL path with the `healthCheckPath` constructor option, or disable serving health checks by passing `null` for this option. (This option is specific to the batteries-included `apollo-server` package; if you're using a framework integration package and want to serve a health check at a different path, just use your web framework directly.) [PR #5270](https://github.com/apollographql/apollo-server/pull/5270) [Issue #3577](https://github.com/apollographql/apollo-server/issues/3577)

## v3.2.0

Expand Down
109 changes: 76 additions & 33 deletions docs/source/api/apollo-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,21 +199,6 @@ An array containing custom functions to use as additional [validation rules](htt
<tr>
<td>

##### `cors`

`Object` or `Boolean`
</td>
<td>

An `Object` containing [configuration options](https://github.com/expressjs/cors#configuration-options) for the server's CORS behavior. Provide `false` to remove CORS middleware entirely.

This option is used only by the `apollo-server` package. If you're integrating with Node.js middleware via a different package, instead see the [framework-specific middleware function](#framework-specific-middleware-function).
</td>
</tr>

<tr>
<td>

##### `formatError`

`Function`
Expand Down Expand Up @@ -300,24 +285,6 @@ You can also manually call `stop()` in other contexts. Note that `stop()` is asy
</td>
</tr>

<tr>
<td>

##### `stopGracePeriodMillis`

`number`
</td>
<td>

The amount of time to wait after [`ApolloServer.stop()`](#stop) is called (including via a [termination signal](#stoponterminationsignals)) before forcefully closing all active connections. If you pass `Infinity`, Apollo Server waits indefinitely for all active connections to go idle.

**This option is used only by the `apollo-server` package.** If you're integrating with [Node.js middleware](../integrations/middleware/) via a different package, it's your responsibility to stop your HTTP server in whatever way is appropriate.

The default value is `10_000` (10 seconds).

</td>
</tr>


<tr>
<td colspan="2">
Expand Down Expand Up @@ -390,6 +357,82 @@ The default value is `true`. Set this to `false` to use mocked resolvers only fo
</td>
</tr>

</tbody>
</table>

### `apollo-server`-specific options

**These options are only part of the batteries-included `apollo-server` package.** They do not exist in framework integrations.

<table class="field-table">
<thead>
<tr>
<th>Name /<br/>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>

<tr>
<td>

##### `healthCheckPath`

`string` or `null`
</td>
<td>

Disable [HTTP-level health checks](../monitoring/health-checks/#http-level-health-checks) by passing `null`, or change the path on which it is served from the default of `/.well-known/apollo/server-health`.

</td>
</tr>

<tr>
<td>

##### `onHealthCheck`

`Function`
</td>
<td>

A custom function to execute when Apollo Server receives a request at the [HTTP-level health check](../monitoring/health-checks/#http-level-health-checks) endpoint.

</td>
</tr>


<tr>
<td>

##### `cors`

`Object` or `Boolean`
</td>
<td>

An `Object` containing [configuration options](https://github.com/expressjs/cors#configuration-options) for the server's CORS behavior. Provide `false` to remove CORS middleware entirely.

</td>
</tr>

<tr>
<td>

##### `stopGracePeriodMillis`

`number`
</td>
<td>

The amount of time to wait after [`ApolloServer.stop()`](#stop) is called (including via a [termination signal](#stoponterminationsignals)) before forcefully closing all active connections. If you pass `Infinity`, Apollo Server waits indefinitely for all active connections to go idle.

The default value is `10_000` (10 seconds).

</td>
</tr>


</tbody>
</table>

Expand Down
41 changes: 37 additions & 4 deletions docs/source/monitoring/health-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@ title: Health checks
description: Determining the health status of the Apollo Server
---

Health checks are often used by load balancers to determine if a server is available and ready to start serving traffic. By default, Apollo Server provides a health check endpoint at `/.well-known/apollo/server-health` which returns a 200 status code if the server has started.
Health checks are often used by load balancers to determine if a server is available and ready to start serving traffic.

This basic health check may not be comprehensive enough for some applications and depending on individual circumstances, it may be beneficial to provide a more thorough implementation by defining an `onHealthCheck` function to the `ApolloServer` constructor options. If defined, this `onHealthCheck` async function should return if the server is deemed _ready_ or `throw` if there is an error. Returning (resolving the `Promise`) will result in an HTTP status code of 200, which is generally desired by most health-check tooling (e.g. Kubernetes, AWS, etc.), while `throw`ing (rejecting the `Promise`) will result in an HTTP status code of 503.
## GraphQL-level health checks

The most natural way to determine if your GraphQL server is healthy is to run a GraphQL operation!

Every GraphQL server supports the trival query that asks for the "type name" of the top-level query. So you can run a health check as a GET against an URL like:

```
https://your.server/graphql?query=%7B__typename%7D
```

This health check will run a real GraphQL operation, so if your server requires special headers or cookies in order to run any query, you'll need to provide that as well.

## HTTP-level health checks

Apollo Server also supports a simple HTTP-level health check. This is enabled by default and is served at the URL path `/.well-known/apollo/server-health`. This returns the 200 status code if the HTTP server is successfully serving. It does not invoke any of the GraphQL execution machinery. This means it may be better for your needs if your server's [context function](../data/resolvers/#the-context-argument) requires you to be logged in, but it may be worse for your needs if you want your health check to test that your server's context function runs successfully.

### In the batteries-included `apollo-server` package

The batteries-included `apollo-server` package doesn't provide you with direct access to your HTTP server, so we provide a few knobs to let you adjust the HTTP-level health check.

You can pass a string `healthCheckPath` to the `ApolloServer` constructor to change the URL where the health check is served. You can also pass `healthCheckPath: null` to disable the health check.

If you'd like the health check to do more than just "always return success", you can pass an async function `onHealthCheck` function to the `ApolloServer` constructor. If defined, this `onHealthCheck` async function should return if the server is deemed _ready_ or `throw` if there is an error. Returning (resolving the `Promise`) will result in an HTTP status code of 200, which is generally desired by most health-check tooling (e.g. Kubernetes, AWS, etc.), while `throw`ing (rejecting the `Promise`) will result in an HTTP status code of 503.

```js{10-17}
import { ApolloServer, gql } from 'apollo-server';
Expand All @@ -18,8 +40,7 @@ const server = new ApolloServer({
typeDefs,
resolvers,
async onHealthCheck() {
// Replace the `true` in this conditional with more specific checks!
if (true) {
if (everythingLooksHealthy()) {
return;
} else {
throw new Error('...');
Expand All @@ -34,3 +55,15 @@ server.listen().then(({ url }) => {
);
});
```

Note that the `healthCheckPath` and `onHealthCheck` options are *only* accepted by the batteries-include `apollo-server` constructor, not by other framework integration packages.

### In framework integration packages

Like `apollo-server`, framework integration packages like `apollo-server-express` and `apollo-server-lambda` also serve a health check at the URL path `/.well-known/apollo/server-health` by default. Customizing this health check is done in the [framework-specific middleware function](../api/apollo-server/#framework-specific-middleware-function) such as `applyMiddleware` or `createHandler` rather than in the `ApolloServer` constructor.

To disable serving the health check endpoint, pass `disableHealthCheck: true` to the framework-specific middleware function.

These packages technically support an `onHealthCheck` function like `apollo-server`; this function is passed to the framework-specific middleware function rather than to the `ApolloServer` constructor. That said, since you're already setting up your web framework, if you need to customize the behavior of the health check then it may be simpler to just define a health check handler yourself in your web framework (and pass `disableHealthCheck: true` to disable Apollo Server's health check). (A future major version of Apollo Server may change the health check feature to be specific to the "batteries-included" `apollo-server` package rather than part of all framework integration packages.) For similar reasons, you can't customize the health check path in framework integration packages; just disable the built-in health check and add your own at your preferred path.

> `apollo-server-azure-functions` [does not currently support health checks](https://github.com/apollographql/apollo-server/issues/4925).
49 changes: 31 additions & 18 deletions packages/apollo-server-express/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export interface GetMiddlewareOptions {
bodyParserConfig?: OptionsJson | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
// There's no real point to allowing you to customize the health check path in
// an Apollo Server web framework integration package. You're already using
// Express --- just define a health check yourself! This option only exists to
// provide a small amount of configuration for `apollo-server`, which doesn't
// otherwise give you direct access to the web server. (Honestly, the health
// check feature really should *only* exist in `apollo-server`; that it exists
// elsewhere (and doesn't even check to see if GraphQL operations can
// execute!) is a mistake we're stuck with due to backwards compatibility.)
// Passing `null` here implies disableHealthCheck:true.
__internal_healthCheckPath?: string | null;
}

export interface ServerRegistration extends GetMiddlewareOptions {
Expand Down Expand Up @@ -71,33 +81,36 @@ export class ApolloServer<
bodyParserConfig,
disableHealthCheck,
onHealthCheck,
__internal_healthCheckPath,
}: GetMiddlewareOptions = {}): express.Router {
if (!path) path = '/graphql';

this.assertStarted('getMiddleware');

// Note that even though we use Express's router here, we still manage to be
// Connect-compatible because express.Router just implements the same
// middleware interface that Connect and Express share!
const router = express.Router();

if (!disableHealthCheck) {
router.use('/.well-known/apollo/server-health', (req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');

if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
});
if (!disableHealthCheck && __internal_healthCheckPath !== null) {
router.use(
__internal_healthCheckPath ?? '/.well-known/apollo/server-health',
(req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');

if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
},
);
}

// XXX multiple paths?
Expand Down
8 changes: 6 additions & 2 deletions packages/apollo-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ export interface ServerInfo {
}

export class ApolloServer extends ApolloServerExpress {
private cors?: CorsOptions | boolean;
private onHealthCheck?: (req: express.Request) => Promise<any>;
private cors: CorsOptions | boolean | undefined;
private onHealthCheck: ((req: express.Request) => Promise<any>) | undefined;
private healthCheckPath: string | null | undefined;
private httpServer: http.Server;

constructor(
config: ApolloServerExpressConfig & {
cors?: CorsOptions | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
healthCheckPath?: string | null;
stopGracePeriodMillis?: number;
},
) {
Expand All @@ -51,6 +53,7 @@ export class ApolloServer extends ApolloServerExpress {
this.httpServer = httpServer;
this.cors = config.cors;
this.onHealthCheck = config.onHealthCheck;
this.healthCheckPath = config?.healthCheckPath;
}

private createServerInfo(): ServerInfo {
Expand Down Expand Up @@ -116,6 +119,7 @@ export class ApolloServer extends ApolloServerExpress {
: {
origin: '*',
},
__internal_healthCheckPath: this.healthCheckPath,
});

await new Promise((resolve) => {
Expand Down

0 comments on commit 877b19e

Please sign in to comment.