Skip to content

Commit

Permalink
health checks: configure path in apollo-server, doc better (#5270)
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.

Co-authored-by: David Glasser <[email protected]>
  • Loading branch information
jutaz and glasser authored Oct 8, 2021
1 parent ab8e853 commit a0ccf23
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The version headers in this history reflect the versions of Apollo Server itself
- `apollo-server-fastify`: Export `ApolloServerFastifyConfig` and `FastifyContext` TypeScript types. [PR #5743](https://github.com/apollographql/apollo-server/pull/5743)
- `apollo-server-core`: Only generate the schema hash once on startup rather than twice. [PR #5757](https://github.com/apollographql/apollo-server/pull/5757)
- `[email protected]`: When choosing whether or not to parse a response as JSON, treat any `content-type` ending in `+json` as JSON rather than just `application/hal+json` (in addition to `application/json`). [PR #5737](https://github.com/apollographql/apollo-server/pull/5737)
- `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.3.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 @@ -240,21 +240,6 @@ Available in Apollo Server v3.4.0 and later.
<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 @@ -341,24 +326,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 @@ -444,6 +411,82 @@ If this is set to any string value, use that value instead of the environment va
</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).
50 changes: 32 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,17 @@ 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 by adding a handler that
// returns 200 to the URL path of your choice. 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 +82,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 a0ccf23

Please sign in to comment.