Apollo ServerとNowで作るGraphQLモックサーバ #4 モニタリング編
前回の続き。
作成したGraphQLサーバをモニタリング可能な状態にする。
Apollo Engine
Apollo Engine | The GraphQL Gateway with essential features like caching and performance tracing.
- GraphQL caching
- Query execution tracing
- Error tracking
- Trends
などの機能を備える
- モニタリングしたいだけなのでcachingは今回使わない
- アラートは有償版でのみ有効
- 有償版は$99/mo~(割と高い)
導入
最初にGraphQLのスキーマをアップロードする
npm i -g apollo apollo schema:publish --endpoint=<your graphql endpoint here> --key="<your key>"
次に、App Engineを利用するためのセットアップを行う
API Reference: apollo-server | Apollo Server
Provided the ENGINE_API_KEY environment variable is set, the engine reporting agent will be started automatically.
apollo-serverは環境変数 ENGINE_API_KEY
がセットされていれば、
その値を使ってapollo-engineを有効化するようになっている。
環境変数は .env
に記述したものを
nowの --dotenv
オプションに渡してセットする。
ENGINE_API_KEY=service:foo-bar-baz:xxx...
$ now --dotenv=.env.production $ now alias
確認
Usage Metrics
デプロイされた環境にアクセスし、Playgroundでいくつかクエリを実行した後、 Apollo Engineの "Metrics" の項目を見ると実行したクエリのログが確認できる。
- リクエストの頻度
- パフォーマンスの概要
- エラー率
クエリ名ごとにさらに詳細な情報を確認することもできる。
Daily Report
Slack integrationを設定しておくとdaily reportでモニタリングのサマリを受け取ることもできる。
Schema Explorer
Schema Explorerではschemaに含まれるtypeの一覧と、それぞれの使用状況を確認することができる。
例えば、これを見るとPerson.addressは全く使われていないことがわかるので @deprecated
にする、といった判断に使える。
Schema History
schemaの変遷を確認できる。
例えば、Person.addressを @deprecated
に変更した場合、以下のように表示される。
まとめ
- 小一時間くらいあればここまで準備できる
- あとはSchema足していってResolverをひたすら書く
- とりあえずクライアントの動作を確認するためだけのモックサーバならmock書いていくだけでもOK
- GraphQLのサーバ運用するのはそれなりに労力かかる(と聞く)が、モニタリングあればただ闇雲にやるよりはずっと安心
Apollo ServerとNowで作るGraphQLモックサーバ #3 デプロイ編
前回の続き
ZEIT Nowにデプロイする
Deploying Node.js Apps - ZEIT Documentation
詳しい方法は公式ドキュメントに譲るとして...
一般的なnode appは特に設定をしなくてもプロジェクトディレクトリで now
コマンドを実行すればデプロイされ、利用可能な状態になる。
npm scriptsの "start" がエントリーポイントになるので、以下のように設定しておく。
"scripts": { "start": "ts-node src/index.ts" },
now.json
また、設定なしでデプロイした場合URLにプロジェクトディレクトリ名が勝手に入ってしまうため、 now.jsonの "name" で明示的にプロジェクト名を設定しておくとよい。
{ "name": "graphql-server", "env": { "NODE_ENV": "production" } }
これにより https://graphql-server-foobarbaz.now.sh
といったURLにデプロイされるようになる。
GitHubへのpushにフックしてデプロイさせる
手順は以下の通り ZEIT – Now + GitHub
Pull Requestをopenしてブランチをpushするとnow.jsonの設定にしたがってデプロイが始まる。
デプロイが完了すると下記のようなメッセージが表示され、 commit messageの横にある✓のアイコンからデプロイされた環境にアクセスできるようになる。
URLを固定する
通常 now
でデプロイするとURLが毎回変わってしまう。
デプロイする度にAPIのURLが変わってしまうのは困るので、URLを固定するためにaliasを使う。
Aliases and Domains - ZEIT Documentation
{ "name": "graphql-server", "alias": "graphql-server-foobar.now.sh", "env": { "NODE_ENV": "production" } }
now.jsonに "alias" として、[任意の文字列].now.sh
を追加し、
now alias
を実行すると、現在のdeploymentが指定したaliasでアクセスできるようになる。
モニタリング編に続く
Apollo ServerとNowで作るGraphQLモックサーバ #2 実装編
モックサーバの実装
前回
1. 型の定義を行う
import { ApolloServer, gql } from 'apollo-server' const typeDefs = gql` type Person { id: ID name: String phone: String } type Query { people: [Person] } ` const mocks = { Query: () => ({ people: () => new MockList([2, 5]), // 2~5件の[Person]が返る }), } const server = new ApolloServer({ typeDefs, mocks, playground: true, }) const port = 4004 server.listen(port).then(({ url }: { url: string }) => { console.log(`Server ready at ${url}`) })
型定義をしただけでQueryの中身(resolver)を書いていないのだから このままでは動かないんじゃないか...と思うが動く。
Apollo ServerはMockingの仕組みによってresolverが設定されていなくてもそれらしい値を返すようになっている。
2. devサーバを起動
package.jsonに以下のscriptを追加して yarn dev
で起動する
"scripts": { "dev": "ts-node-dev --respawn src/index.ts" },
ts-node-devの --respawn
オプションにより、ファイルが更新されると実行されているサーバが自動的に再起動する
3. Playgroundで確認
起動したサーバの /
にアクセスするとPlaygroundが表示される。
こういうのが最初から用意されているのもapollo-serverのいいところ。
一応値は返ってきているが、nameやphoneが全て "Hello World" なのが気になる
4. Mockを調整
nameは名前っぽい、phoneは電話番号っぽい値が返ってほしいので、 mockの設定をもう少し弄ってみる
「っぽい」値を得るためにfakerを使う
import * as faker from 'faker' const typeDefs = gql` scalar Name scalar Phone type Person { id: ID name: Name phone: Phone } type Query { people: [Person] } ` const mocks = { Name: () => faker.name.findName(), Phone: () => faker.phone.phoneNumber(), Query: () => ({ people: () => new MockList([2, 5]), // 2~5件の[Person]が返る }), }
それっぽい値が返ってくるようになった
デプロイ編に続く
Apollo ServerとNowで作るGraphQLモックサーバ #1 準備編
GraphQLのモックサーバを作る際に使うものをメモ
使うもの
TypeScript
- 型があったほうが書きやすい
- 最近はクライアント(React)も全てTypeScriptで書いてるし...
- apollo-serverのパッケージには型情報が含まれている
ts-nodeとts-node-dev
- 本番はともかく、パフォーマンスを必要としないdevでいちいちコンパイルしてから実行するのはめんどくさい
- ts-nodeを使えば.tsをそのまま実行できる
- とある案件のdev環境でしばらく使っているが安定稼働している
- ローカルで開発を行う際にはts-node-devを使う
--respawn
オプション付きで起動するとファイルの更新を検知して再起動する
Apollo Server
apollographql/apollo-server: GraphQL server for Express, Connect, Hapi, Koa and more
- GraphQL APIサーバを立てるのに必要なものがだいたい揃ってる全部入りパッケージ
- とある案件ではgraphql-yogaというものを使っているが、これもapollo-serverとだいたい同じもの
- apollo-serverのほうがメジャーなので近々乗り換えようと思っている
Apollo Engine
Apollo Engine | The GraphQL Gateway with essential features like caching and performance tracing.
- apollo-serverに追加することでクエリのキャッシュやモニタリングが可能になる
- ユニークなのはフィールドごとにリクエスト回数が可視化されるところ
- People.bloodTypeは1回もリクエストされてないからdeprecatedにしよう...といった判断に使える
- Free版はカスタムアラートが使えない。有償版は$99/mo~ とややお高め
- 既存サーバへの組み込みも容易
ZEIT Now
続く
TypeScriptにおける、既存クラスの拡張
TypeScriptでAudioParam.cancelAndHoldAtTime()
のコンパイルを通したかったが、本家の型定義には追加されていないので、自分で拡張を行ったときのメモ。
declare global { interface AudioParam { cancelAndHoldAtTime(cancelTime: number): void } }
Declaration Merging · TypeScript
compiler merges two separate declarations declared with the same name into a single definition.
自分でAudioParam interfaceを追加すると、元々存在するAudioParam interfaceにマージされる。
You can also add declarations to the global scope from inside a module:
AuidoParamがglobal scopeにいるのでGlobal augmentationを使う
参考
create-react-appで絶対パスによるimportをする
src/Components/App.jsx
を src/index.js
からimportする場合、create-react-appでは
import App from './Components/App';
と書くが、これを、
import App from 'Components/App';
こう書きたい。
が、デフォルトではパスの解決ができず、 Module not found: Can't resolve 'Components/App’
エラーとなる。
NODE_PATHを設定する
プロジェクトの .env
に NODE_PATH=src
を設定することで解決する。
※ dev serverの再起動が必要
NODE_PATH=src
Same as NODE_PATH in Node.js, but only relative folders are allowed. Can be handy for emulating a monorepo setup by setting NODE_PATH=src.
余談だが、create-react-appのカスタム環境変数は、上記のAdvanced Configurationに含まれるものと、REACT_APP_
プレフィクスを持つもの、それに NODE_ENV
のみが有効となる。
Adding Custom Environment Variables
You must create custom environment variables beginning with REACT_APP_. Any other variables except NODE_ENV will be ignored to avoid accidentally exposing a private key on the machine that could have the same name.
TypeScriptによるStateless Functional Componentの定義
環境構築は create-react-app
とMicrosoft/TypeScript-React-Starterでサクッと行う
$ npx create-react-app my-app --scripts-version=react-scripts-ts $ cd my-app $ npm run start
以前、TypeScriptでReact app作ろうとしてセットアップに手間取ったときとは隔世の感がある。
type React.SFCを使う
@types/react
に React.SFC
という型が用意されているので、これを使う
DefinitelyTyped/index.d.ts at master · DefinitelyTyped/DefinitelyTyped
type SFC<P = {}> = StatelessComponent<P>; interface StatelessComponent<P = {}> { (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null; propTypes?: ValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; }
type SFC<P = {}>
とあるので、全てのpropsはoptionalにする必要がある。
そのため、requiredで定義すると以下のコンパイルエラーが発生する。
error TS2322: Type '{}' is not assignable to type 'IntrinsicAttributes & AppProps & { children?: ReactNode; }'. Type '{}' is not assignable to type 'AppProps'. Property 'name' is missing in type '{}'.
コンポーネントの書き換え
App.tsxをStateless Functional Componentに書き換えるとこのようになる
import * as React from 'react'; import './App.css'; const logo = require('./logo.svg'); interface AppProps { name?: string; } const App: React.SFC<AppProps> = ({ name }) => { return ( <div className="App"> <div className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h2>Welcome to {name}</h2> </div> <p className="App-intro"> To get started, edit <code>src/App.tsx</code> and save to reload. </p> </div> ); }; App.defaultProps = { name: 'React.SFC', }; export default App;
defaultPropsの定義をコンポーネント定義と分けたくない場合は、
const App: React.SFC<AppProps> = ({ name = 'React.SFC' }) => { return ( <div className="App"> ...
とも書ける