From a0ccf23f50ae51e00a4f2fb260ce44d4b191eb5f Mon Sep 17 00:00:00 2001 From: Justas Brazauskas Date: Fri, 8 Oct 2021 03:08:05 +0300 Subject: [PATCH] health checks: configure path in apollo-server, doc better (#5270) 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 --- CHANGELOG.md | 1 + docs/source/api/apollo-server.md | 109 ++++++++++++------ docs/source/monitoring/health-checks.md | 41 ++++++- .../apollo-server-express/src/ApolloServer.ts | 50 +++++--- packages/apollo-server/src/index.ts | 8 +- 5 files changed, 152 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eab7b5852e..c78e4ea8aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - `apollo-datasource-rest@3.2.1`: 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 diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index e8a9498ae95..3501ee8c573 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -240,21 +240,6 @@ Available in Apollo Server v3.4.0 and later. -##### `cors` - -`Object` or `Boolean` - - - -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). - - - - - - ##### `formatError` `Function` @@ -341,24 +326,6 @@ You can also manually call `stop()` in other contexts. Note that `stop()` is asy - - - -##### `stopGracePeriodMillis` - -`number` - - - -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). - - - - @@ -444,6 +411,82 @@ If this is set to any string value, use that value instead of the environment va + + + +### `apollo-server`-specific options + +**These options are only part of the batteries-included `apollo-server` package.** They do not exist in framework integrations. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ +##### `healthCheckPath` + +`string` or `null` + + +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`. + +
+ +##### `onHealthCheck` + +`Function` + + +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. + +
+ +##### `cors` + +`Object` or `Boolean` + + +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. + +
+ +##### `stopGracePeriodMillis` + +`number` + + +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). + +
diff --git a/docs/source/monitoring/health-checks.md b/docs/source/monitoring/health-checks.md index 8f783ac29c3..eef88bb226d 100644 --- a/docs/source/monitoring/health-checks.md +++ b/docs/source/monitoring/health-checks.md @@ -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'; @@ -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('...'); @@ -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). diff --git a/packages/apollo-server-express/src/ApolloServer.ts b/packages/apollo-server-express/src/ApolloServer.ts index 16fd000fdca..77b2eae9287 100644 --- a/packages/apollo-server-express/src/ApolloServer.ts +++ b/packages/apollo-server-express/src/ApolloServer.ts @@ -22,6 +22,17 @@ export interface GetMiddlewareOptions { bodyParserConfig?: OptionsJson | boolean; onHealthCheck?: (req: express.Request) => Promise; 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 { @@ -71,9 +82,9 @@ 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 @@ -81,23 +92,26 @@ export class ApolloServer< // 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? diff --git a/packages/apollo-server/src/index.ts b/packages/apollo-server/src/index.ts index f5717ec1769..92b2d54cbfd 100644 --- a/packages/apollo-server/src/index.ts +++ b/packages/apollo-server/src/index.ts @@ -25,14 +25,16 @@ export interface ServerInfo { } export class ApolloServer extends ApolloServerExpress { - private cors?: CorsOptions | boolean; - private onHealthCheck?: (req: express.Request) => Promise; + private cors: CorsOptions | boolean | undefined; + private onHealthCheck: ((req: express.Request) => Promise) | undefined; + private healthCheckPath: string | null | undefined; private httpServer: http.Server; constructor( config: ApolloServerExpressConfig & { cors?: CorsOptions | boolean; onHealthCheck?: (req: express.Request) => Promise; + healthCheckPath?: string | null; stopGracePeriodMillis?: number; }, ) { @@ -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 { @@ -116,6 +119,7 @@ export class ApolloServer extends ApolloServerExpress { : { origin: '*', }, + __internal_healthCheckPath: this.healthCheckPath, }); await new Promise((resolve) => {