Skip to content

Commit

Permalink
apollo-server-azure-functions: Health checks implementation (#5003)
Browse files Browse the repository at this point in the history
Also make the apollo-server-azure-functions README mostly point to the docs
site rather than be another thing that needs to be kept up to date (most
other packages have this already).

Co-authored-by: David Glasser <glasser@davidglasser.net>
  • Loading branch information
vany0114 and glasser committed Oct 8, 2021
1 parent f11cd92 commit 0d31e0c
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 143 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The version headers in this history reflect the versions of Apollo Server itself
- `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)
- `apollo-server-azure-functions`: This package now supports health checks like all of the other supported Apollo Server packages; they are on by default and can be customized with `disableHealthCheck` and `onHealthCheck`. [PR #5003](https:// github.com/apollographql/apollo-server/pull/5003) [Issue #4925](https://github.com/apollographql/apollo-server/issues/4925)

## v3.3.0

Expand Down
5 changes: 5 additions & 0 deletions docs/source/deployment/azure-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This is the Azure Functions integration for the Apollo community GraphQL Server.

All examples below was created using Linux environments, if you are working with Windows-based platforms some commands couldn’t work fine.

Note that `apollo-server-azure-functions` does not provide a mechanism for adding arbitrary middleware to your web server (other that by manually wrapping the handler returned by `createHandler` in your own handler).

## Prerequisites

The following must be done before following this guide:
Expand Down Expand Up @@ -102,6 +104,7 @@ Make two changes to `graphql/function.json`: make the output name `$return`, and
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "{*segments}",
"methods": [
"get",
"post",
Expand All @@ -117,6 +120,8 @@ Make two changes to `graphql/function.json`: make the output name `$return`, and
}
```

The `route` line is required for the [health checks](../monitoring/health-checks) feature and is not otherwise required.

Finally, we need to return to the base folder and run the `func host start` command again after that, go back to your browser and refresh your page to see the Apollo Server running. You can then run operations against your graph with Apollo Sandbox.

```shell
Expand Down
10 changes: 9 additions & 1 deletion docs/source/integrations/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ Then run the web server with `npx micro`.

* `path`
* `onHealthCheck`
* `disableHealthCheck`.
* `disableHealthCheck`

Note that `apollo-server-micro` does _not_ have a built-in way of setting CORS headers.

Expand Down Expand Up @@ -508,6 +508,14 @@ const server = new ApolloServer({ typeDefs, resolvers });
exports.handler = server.createHandler();
```

`createHandler` accepts the following [common options](#common-options):

* `cors`
* `onHealthCheck`
* `disableHealthCheck`

Note that `apollo-server-azure-functions` does not provide a mechanism for adding arbitrary middleware to your web server (other that by manually wrapping the handler returned by `createHandler` in your own handler).

For more details on using `apollo-server-azure-functions`, see the [documentation on deploying to Azure Functions](../deployment/azure-functions/).


Expand Down
4 changes: 1 addition & 3 deletions docs/source/monitoring/health-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,4 @@ Like `apollo-server`, framework integration packages like `apollo-server-express

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).
These packages also support an `onHealthCheck` function like `apollo-server`; this function is passed to the framework-specific middleware function rather than to the `ApolloServer` constructor. If you're using `apollo-server-azure-functions` (which doesn't provide a general way to customize its HTTP serving behavior), this option may be helpful. For other integrations such as `apollo-server-express`, we don't recommend that you customize the health check via Apollo. Since you're already setting up your web framework, if you need to customize the behavior of the health check then it is probably more straightforward to simple define a health check handler yourself directly 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.
141 changes: 6 additions & 135 deletions packages/apollo-server-azure-functions/README.md
Original file line number Diff line number Diff line change
@@ -1,142 +1,13 @@
This is the Azure functions integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/v2). [Read the CHANGELOG](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md).
[![npm version](https://badge.fury.io/js/apollo-server-azure-functions.svg)](https://badge.fury.io/js/apollo-server-azure-functions)
[![Build Status](https://circleci.com/gh/apollographql/apollo-server/tree/main.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server)
[![Join the community forum](https://img.shields.io/badge/join%20the%20community-forum-blueviolet)](https://community.apollographql.com)
[![Read CHANGELOG](https://img.shields.io/badge/read-changelog-blue)](https://github.com/apollographql/apollo-server/blob/HEAD/CHANGELOG.md)

```shell
npm install apollo-server-azure-functions graphql
```

## Writing azure function
This is the Azure Functions integration of Apollo Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/). [Read the CHANGELOG.](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md)

Azure functions currently support two runtime versions. This package assumes that function is running under **runtime 2.0**.

Azure functions typically consist of at least 2 files - index.js (function handler definition) and function.json (function settings and bindings).
For more information about azure functions development model in general, refer to [official Azure functions docs](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node).

Example index.js:

```js
const { gql, ApolloServer } = require("apollo-server-azure-functions");

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;

// A map of functions which return data for the schema.
const resolvers = {
Query: {
hello: () => "world"
}
};

const server = new ApolloServer({ typeDefs, resolvers });

module.exports = server.createHandler();
```

Example function.json:
```json
{
"disabled": false,
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
```

It is important to set output binding name to '$return' for apollo-server-azure-function to work correctly.

## Modifying the Azure Function Response (Enable CORS)

To enable CORS the response HTTP headers need to be modified. To accomplish this use the `cors` option.

```js
const { ApolloServer, gql } = require('apollo-server-azure-functions');

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;

// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};

const server = new ApolloServer({
typeDefs,
resolvers,
});

module.exports = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});
```

To enable CORS response for requests with credentials (cookies, http authentication) the allow origin header must equal the request origin and the allow credential header must be set to true.

```js
const { ApolloServer, gql } = require('apollo-server-azure-functions');

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;

// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};

const server = new ApolloServer({
typeDefs,
resolvers,
});

module.exports = server.createHandler({
cors: {
origin: true,
credentials: true,
},
});
```

### Cors Options

The options correspond to the [express cors configuration](https://github.com/expressjs/cors#configuration-options) with the following fields(all are optional):

* `origin`: boolean | string | string[]
* `methods`: string | string[]
* `allowedHeaders`: string | string[]
* `exposedHeaders`: string | string[]
* `credentials`: boolean
* `maxAge`: number
A full example of how to use `apollo-server-azure-functions` can be found in [the docs](https://www.apollographql.com/docs/apollo-server/integrations/middleware/#apollo-server-azure-functions), including a [full tutorial](https://www.apollographql.com/docs/apollo-server/deployment/azure-functions/).

## Principles

Expand Down
41 changes: 40 additions & 1 deletion packages/apollo-server-azure-functions/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface CreateHandlerOptions {
credentials?: boolean;
maxAge?: number;
};
disableHealthCheck?: boolean;
onHealthCheck?: (req: HttpRequest) => Promise<any>;
}

export class ApolloServer extends ApolloServerBase {
Expand All @@ -31,7 +33,11 @@ export class ApolloServer extends ApolloServerBase {
return super.graphQLServerOptions({ request, context });
}

public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) {
public createHandler({
cors,
onHealthCheck,
disableHealthCheck,
}: CreateHandlerOptions = {}) {
const staticCorsHeaders: HttpResponse['headers'] = {};

if (cors) {
Expand Down Expand Up @@ -109,6 +115,39 @@ export class ApolloServer extends ApolloServerBase {
}
}

if (
!disableHealthCheck &&
req.url?.endsWith('/.well-known/apollo/server-health')
) {
const successfulResponse = {
body: JSON.stringify({ status: 'pass' }),
status: 200,
headers: {
'Content-Type': 'application/health+json',
...corsHeaders,
},
};
if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
return context.done(null, successfulResponse);
})
.catch(() => {
return context.done(null, {
body: JSON.stringify({ status: 'fail' }),
status: 503,
headers: {
'Content-Type': 'application/health+json',
...corsHeaders,
},
});
});
return;
} else {
return context.done(null, successfulResponse);
}
}

if (req.method === 'OPTIONS') {
if (
requestHeaders['access-control-request-headers'] &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import url from 'url';
import type { IncomingMessage, ServerResponse } from 'http';
import typeis from 'type-is';

const healthCheckRequest = {
method: 'GET',
body: null,
url: '/.well-known/apollo/server-health',
query: null,
headers: {},
};

const createAzureFunction = async (options: CreateAppOptions = {}) => {
const server = new ApolloServer(
(options.graphqlOptions as Config) || { schema: Schema },
Expand Down Expand Up @@ -94,7 +102,7 @@ describe('integration:AzureFunctions', () => {
const request = {
method: 'GET',
body: null,
path: '/graphql',
url: '/graphql',
query: query,
headers: {},
};
Expand Down Expand Up @@ -139,7 +147,7 @@ describe('integration:AzureFunctions', () => {
const request = {
method: 'OPTIONS',
body: null,
path: '/graphql',
url: '/graphql',
query: null,
headers: {},
};
Expand All @@ -161,7 +169,7 @@ describe('integration:AzureFunctions', () => {
const request = {
method: 'GET',
body: null,
path: '/',
url: '/',
query: null,
headers: {
Accept: 'text/html',
Expand All @@ -187,4 +195,80 @@ describe('integration:AzureFunctions', () => {
};
handler(context as any, request as any);
});

describe('healthchecks', () => {
it('creates a healthcheck endpoint', async () => {
const server = new ApolloServer({ schema: Schema });
const handler = server.createHandler({});

const context: any = {};
const p = new Promise((resolve, reject) => {
context.done = (error: Error, result: any) => {
if (error) {
reject(error);
} else {
resolve(result);
}
};
});

handler(context as any, healthCheckRequest as any);
const result: any = await p;
expect(result.status).toEqual(200);
expect(result.body).toEqual(JSON.stringify({ status: 'pass' }));
expect(result.headers['Content-Type']).toEqual('application/health+json');
});

it('provides a callback for the healthcheck', async () => {
const server = new ApolloServer({ schema: Schema });
const handler = server.createHandler({
onHealthCheck: async () => {
return new Promise((resolve) => {
return resolve('Success!');
});
},
});

const context: any = {};
const p = new Promise((resolve, reject) => {
context.done = (error: Error, result: any) => {
if (error) {
reject(error);
} else {
resolve(result);
}
};
});

handler(context as any, healthCheckRequest as any);
const result: any = await p;
expect(result.status).toEqual(200);
expect(result.body).toEqual(JSON.stringify({ status: 'pass' }));
expect(result.headers['Content-Type']).toEqual('application/health+json');
});

it('returns a 503 if healthcheck fails', async () => {
const server = new ApolloServer({ schema: Schema });
const handler = server.createHandler({
onHealthCheck: async () => {
return new Promise(() => {
throw new Error('Failed to connect!');
});
},
});

const context = {
done(error: any, result: any) {
if (error) throw error;
expect(result.status).toEqual(503);
expect(result.body).toEqual(JSON.stringify({ status: 'fail' }));
expect(result.headers['Content-Type']).toEqual(
'application/health+json',
);
},
};

handler(context as any, healthCheckRequest as any);
});
});
});

0 comments on commit 0d31e0c

Please sign in to comment.