Skip to content

Commit

Permalink
Add Apollo Engine Proxy
Browse files Browse the repository at this point in the history
exAspArk committed Oct 25, 2017
1 parent 3b8ac14 commit ca9b02f
Showing 11 changed files with 271 additions and 83 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
/pkg/
/spec/reports/
/tmp/
/bin/engineproxy*

# rspec failure tracking
.rspec_status
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@ sudo: false
language: ruby
rvm:
- 2.3.4
script: bundle exec rspec
script: make download_binaries && bundle exec rspec
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,10 +8,14 @@ one of the following labels: `Added`, `Changed`, `Deprecated`,
to manage the versions of this gem so
that you can set version constraints properly.

#### [Unreleased](https://github.com/uniiverse/apollo-tracing-ruby/compare/v1.0.0...HEAD)
#### [Unreleased](https://github.com/uniiverse/apollo-tracing-ruby/compare/v1.1.0...HEAD)

* WIP

#### [v1.1.0](https://github.com/uniiverse/apollo-tracing-ruby/compare/v1.0.0..v1.1.0) – 2017-10-25

* `Added`: Apollo Engine Proxy version [2017.10-408-g497e1410](https://www.apollographql.com/docs/engine/proxy-release-notes.html). ([#2](https://github.com/uniiverse/apollo-tracing-ruby/pull/2))

#### [v1.0.0](https://github.com/uniiverse/apollo-tracing-ruby/compare/v0.1.1...v1.0.0) – 2017-10-17

* `Changed`: the gem name from `graphql-tracing` to `apollo-tracing`.
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
download_binaries:
curl -O https://registry.npmjs.org/apollo-engine-binary-darwin/-/apollo-engine-binary-darwin-0.2017.10-408-g497e1410.tgz
curl -O https://registry.npmjs.org/apollo-engine-binary-darwin/-/apollo-engine-binary-linux-0.2017.10-408-g497e1410.tgz
curl -O https://registry.npmjs.org/apollo-engine-binary-darwin/-/apollo-engine-binary-windows-0.2017.10-408-g497e1410.tgz
tar -xzf apollo-engine-binary-darwin-0.2017.10-408-g497e1410.tgz
tar -xzf apollo-engine-binary-linux-0.2017.10-408-g497e1410.tgz
tar -xzf apollo-engine-binary-windows-0.2017.10-408-g497e1410.tgz
mv package/engineproxy_darwin_amd64 bin/
mv package/engineproxy_linux_amd64 bin/
mv package/engineproxy_windows_amd64.exe bin/
rm -r package/
rm apollo-engine-binary-darwin-0.2017.10-408-g497e1410.tgz
rm apollo-engine-binary-linux-0.2017.10-408-g497e1410.tgz
rm apollo-engine-binary-windows-0.2017.10-408-g497e1410.tgz

release: download_binaries
bundle exec rake release
111 changes: 103 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,17 @@

Ruby implementation of [GraphQL](https://github.com/rmosolgo/graphql-ruby) trace data in the [Apollo Tracing](https://github.com/apollographql/apollo-tracing) format.


## Contents

* [Installation](#installation)
* [Usage](#usage)
* [Tracing](#tracing)
* [Engine Proxy](#engine-proxy)
* [Development](#development)
* [Contributing](#contributing)
* [License](#license)

## Installation

Add this line to your application's Gemfile:
@@ -49,16 +60,18 @@ Schema = GraphQL::Schema.define do
end

# Execute query
query = "query($user_id: ID!) {
posts(user_id: $user_id) {
id
title
}
}"
query = "
query($user_id: ID!) {
posts(user_id: $user_id) {
id
title
}
}
"
Schema.execute(query, variables: { user_id: 1 })
```

### Setup Tracing
### Tracing

Add 'ApolloTracing' to your schema:

@@ -141,6 +154,89 @@ Now your response should look something like:
}
```

### Engine Proxy

Now you can start using the [Apollo Engine](https://www.apollographql.com/engine/) service.
Here is the general architecture overview of a sidecar mode – Proxy runs next to your application server:

```
----------------- request ----------------- request -----------------
| | -----------> | | -----------> | |
| Client | | Engine Proxy | | Application |
| | <----------- | | <----------- | |
----------------- response ----------------- response -----------------
|
|
GraphQL tracing data |
|
˅
-----------------
| |
| Apollo Engine |
| |
-----------------
```

`ApolloTracing` gem comes with the [Apollo Engine Proxy](https://www.apollographql.com/docs/engine/index.html#engine-proxy) binary written in Go.
To configure the Proxy create a Proxy config file:

```
# config/apollo-engine-proxy.json
{
"apiKey": "service:YOUR_ENGINE_API_KEY",
"logging": { "level": "INFO" },
"origins": [{
"http": { "url": "http://localhost:3000/graphql" }
}],
"frontends": [{
"host": "127.0.0.1", "port": 3001, "endpoint": "/graphql"
}]
}
```

* `apiKey` – get this on your [Apollo Engine](https://engine.apollographql.com/) home page.
* `logging.level` – a log level for the Proxy ("INFO", "DEBUG" or "ERROR").
* `origins` – a list of URLs with your GraphQL endpoints in the Application.
* `frontends` – an address on which the Proxy will be listening.

To run the Proxy as a child process, which will be automatically terminated if the Application proccess stoped, add the following line to the `config.ru` file:

<pre>
# config.ru – this file is used by Rack-based servers to start the application.
require File.expand_path('../config/environment', __FILE__)

<b>ApolloTracing.start_proxy('config/apollo-engine-proxy.json')</b>
run Your::Application
</pre>

For example, if you use [rails](https://github.com/rails/rails) with [puma](https://github.com/puma/puma) application server and run it like:

```
bundle exec puma -w 2 -t 16 -p 3001
```

The proccess tree may look like:

```
---------------
| Puma Master |
| Port 3000 |
---------------
| |
---------- ----------
| | ----------------
˅ -> | Puma Worker1 |
---------------- | -----------------
| Engine Proxy | | ----------------
| Port 3001 | -> | Puma Worker2 |
---------------- ----------------
```

Now you can send requests to the reverse Proxy `http://localhost:3001`.
It'll proxy any (GraphQL and non-GraphQL) requests to the Application `http://localhost:3000`.
If the request matches the endpoints described in `origins`, it'll strip the `tracing` data from the response and will send it to the Apollo Engine service.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -154,4 +250,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/uniive
## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

11 changes: 8 additions & 3 deletions apollo-tracing.gemspec
Original file line number Diff line number Diff line change
@@ -14,9 +14,14 @@ Gem::Specification.new do |spec|
spec.homepage = "https://github.com/uniiverse/apollo-tracing-ruby"
spec.license = "MIT"

spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
spec.files =
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^test/}) } +
%w[
bin/engineproxy_darwin_amd64
bin/engineproxy_linux_amd64
bin/engineproxy_windows_amd64.exe
]

spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
1 change: 1 addition & 0 deletions bin/setup
Original file line number Diff line number Diff line change
@@ -4,5 +4,6 @@ IFS=$'\n\t'
set -vx

bundle install
make download_binaries

# Do any other automated setup that you need to do here
37 changes: 37 additions & 0 deletions lib/apollo_tracing.rb
Original file line number Diff line number Diff line change
@@ -4,6 +4,43 @@
require "apollo_tracing/version"

class ApolloTracing
def self.start_proxy(config_filepath = 'config/apollo-engine.json')
config_json = File.read(config_filepath)
binary_path =
if RUBY_PLATFORM.include?('darwin')
File.expand_path('../../bin/engineproxy_darwin_amd64', __FILE__)
elsif /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
File.expand_path('../../bin/engineproxy_windows_amd64.exe', __FILE__)
else
File.expand_path('../../bin/engineproxy_linux_amd64', __FILE__)
end

@@proxy_pid = spawn(
{"ENGINE_CONFIG" => config_json},
"#{binary_path} -config=env -restart=true",
{out: STDOUT, err: STDERR}
)
at_exit { stop_proxy }
Process.detach(@@proxy_pid)
@@proxy_pid
end

def self.stop_proxy
Process.getpgid(@@proxy_pid)
Process.kill('TERM', @@proxy_pid)

3.times do
Process.getpgid(@@proxy_pid)
sleep 1
end

Process.getpgid(@@proxy_pid)
puts "Couldn't cleanly terminate the Apollo Engine Proxy in 3 seconds!"
Process.kill('KILL', @@proxy_pid)
rescue Errno::ESRCH
# process does not exist
end

def use(schema_definition)
schema_definition.instrument(:query, self)
schema_definition.instrument(:field, self)
2 changes: 1 addition & 1 deletion lib/apollo_tracing/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class ApolloTracing
VERSION = "1.0.0"
VERSION = "1.1.0"
end
156 changes: 87 additions & 69 deletions spec/apollo_tracing_spec.rb
Original file line number Diff line number Diff line change
@@ -5,83 +5,101 @@
require 'fixtures/schema'

RSpec.describe ApolloTracing do
it 'returns time in RFC 3339 format' do
query = "query($user_id: ID!) { posts(user_id: $user_id) { id title user_id } }"
now = Time.new(2017, 8, 25, 0, 0, 0, '+00:00')
allow(Time).to receive(:now).and_return(now)

result = Schema.execute(query, variables: {'user_id' => "1"})

expect(result.dig("extensions", 'tracing', 'startTime')).to eq('2017-08-25T00:00:00.000Z')
expect(result.dig("extensions", 'tracing', 'endTime')).to eq('2017-08-25T00:00:00.000Z')
describe '.start_proxy' do
it 'runs a proxy' do
pid = ApolloTracing.start_proxy('spec/fixtures/apollo-engine-proxy.json')
expect { Process.getpgid(pid) }.not_to raise_error
ApolloTracing.stop_proxy
end
end

it "resolves graphql query with tracing extension" do
query = "query($user_id: ID!) { posts(user_id: $user_id) { id title user_id } }"

result = Schema.execute(query, variables: {'user_id' => "1"})

expect(result["data"]).to eq(
"posts" => [{
"id" => "1",
"title" => "Post Title",
"user_id" => "1"
}]
)
tracing = result.dig("extensions", 'tracing')

expect(tracing['version']).to eq(1)
expect(tracing['startTime']).to be_a(String)
expect(tracing['endTime']).to be_a(String)
expect(tracing['duration']).to be >= 0

resolvers = tracing.dig('execution', 'resolvers')

expect(resolvers.dig(0, 'path')).to eq(["posts"])
expect(resolvers.dig(0, 'parentType')).to eq("Query")
expect(resolvers.dig(0, 'fieldName')).to eq("posts")
expect(resolvers.dig(0, 'returnType')).to eq("[Post!]!")
expect(resolvers.dig(0, 'startOffset')).to be >= 0
expect(resolvers.dig(0, 'duration')).to be >= 0

expect(resolvers.dig(1, 'path')).to eq(["posts", 0, "id"])
expect(resolvers.dig(1, 'parentType')).to eq("Post")
expect(resolvers.dig(1, 'fieldName')).to eq("id")
expect(resolvers.dig(1, 'returnType')).to eq("ID!")
expect(resolvers.dig(1, 'startOffset')).to be >= 0
expect(resolvers.dig(1, 'duration')).to be >= 0

expect(resolvers.dig(2, 'path')).to eq(["posts", 0, "title"])
expect(resolvers.dig(2, 'parentType')).to eq("Post")
expect(resolvers.dig(2, 'fieldName')).to eq("title")
expect(resolvers.dig(2, 'returnType')).to eq("String!")
expect(resolvers.dig(2, 'startOffset')).to be >= 0
expect(resolvers.dig(2, 'duration')).to be >= 0

expect(resolvers.dig(3, 'path')).to eq(["posts", 0, "user_id"])
expect(resolvers.dig(3, 'parentType')).to eq("Post")
expect(resolvers.dig(3, 'fieldName')).to eq("user_id")
expect(resolvers.dig(3, 'returnType')).to eq("ID!")
expect(resolvers.dig(3, 'startOffset')).to be >= 0
expect(resolvers.dig(3, 'duration')).to be >= 0
describe '.stop_proxy' do
it 'stops a proxy' do
pid = ApolloTracing.start_proxy('spec/fixtures/apollo-engine-proxy.json')
ApolloTracing.stop_proxy
expect { Process.getpgid(pid) }.to raise_error(Errno::ESRCH, 'No such process')
end
end

it "resolves without race conditions and multiple threads by sharing vars in the context" do
thread1 = Thread.new do
query1 = "query($user_id: ID!) { posts(user_id: $user_id) { id slow_id } }"
@result1 = Schema.execute(query1, variables: {'user_id' => "1"})
context 'introspection' do
it 'returns time in RFC 3339 format' do
query = "query($user_id: ID!) { posts(user_id: $user_id) { id title user_id } }"
now = Time.new(2017, 8, 25, 0, 0, 0, '+00:00')
allow(Time).to receive(:now).and_return(now)

result = Schema.execute(query, variables: {'user_id' => "1"})

expect(result.dig("extensions", 'tracing', 'startTime')).to eq('2017-08-25T00:00:00.000Z')
expect(result.dig("extensions", 'tracing', 'endTime')).to eq('2017-08-25T00:00:00.000Z')
end

thread2 = Thread.new do
sleep 1
query2 = "query($user_id: ID!) { posts(user_id: $user_id) { title } }"
@result2 = Schema.execute(query2, variables: {'user_id' => "1"})
it "resolves graphql query with tracing extension" do
query = "query($user_id: ID!) { posts(user_id: $user_id) { id title user_id } }"

result = Schema.execute(query, variables: {'user_id' => "1"})

expect(result["data"]).to eq(
"posts" => [{
"id" => "1",
"title" => "Post Title",
"user_id" => "1"
}]
)
tracing = result.dig("extensions", 'tracing')

expect(tracing['version']).to eq(1)
expect(tracing['startTime']).to be_a(String)
expect(tracing['endTime']).to be_a(String)
expect(tracing['duration']).to be >= 0

resolvers = tracing.dig('execution', 'resolvers')

expect(resolvers.dig(0, 'path')).to eq(["posts"])
expect(resolvers.dig(0, 'parentType')).to eq("Query")
expect(resolvers.dig(0, 'fieldName')).to eq("posts")
expect(resolvers.dig(0, 'returnType')).to eq("[Post!]!")
expect(resolvers.dig(0, 'startOffset')).to be >= 0
expect(resolvers.dig(0, 'duration')).to be >= 0

expect(resolvers.dig(1, 'path')).to eq(["posts", 0, "id"])
expect(resolvers.dig(1, 'parentType')).to eq("Post")
expect(resolvers.dig(1, 'fieldName')).to eq("id")
expect(resolvers.dig(1, 'returnType')).to eq("ID!")
expect(resolvers.dig(1, 'startOffset')).to be >= 0
expect(resolvers.dig(1, 'duration')).to be >= 0

expect(resolvers.dig(2, 'path')).to eq(["posts", 0, "title"])
expect(resolvers.dig(2, 'parentType')).to eq("Post")
expect(resolvers.dig(2, 'fieldName')).to eq("title")
expect(resolvers.dig(2, 'returnType')).to eq("String!")
expect(resolvers.dig(2, 'startOffset')).to be >= 0
expect(resolvers.dig(2, 'duration')).to be >= 0

expect(resolvers.dig(3, 'path')).to eq(["posts", 0, "user_id"])
expect(resolvers.dig(3, 'parentType')).to eq("Post")
expect(resolvers.dig(3, 'fieldName')).to eq("user_id")
expect(resolvers.dig(3, 'returnType')).to eq("ID!")
expect(resolvers.dig(3, 'startOffset')).to be >= 0
expect(resolvers.dig(3, 'duration')).to be >= 0
end

[thread1, thread2].map(&:join)
it "resolves without race conditions and multiple threads by sharing vars in the context" do
thread1 = Thread.new do
query1 = "query($user_id: ID!) { posts(user_id: $user_id) { id slow_id } }"
@result1 = Schema.execute(query1, variables: {'user_id' => "1"})
end

thread2 = Thread.new do
sleep 1
query2 = "query($user_id: ID!) { posts(user_id: $user_id) { title } }"
@result2 = Schema.execute(query2, variables: {'user_id' => "1"})
end

expect(@result1.dig('extensions', 'tracing', 'execution', 'resolvers', 1, 'path')).to eq(['posts', 0, 'id'])
expect(@result1.dig('extensions', 'tracing', 'execution', 'resolvers', 2, 'path')).to eq(['posts', 0, 'slow_id'])
expect(@result2.dig('extensions', 'tracing', 'execution', 'resolvers', 1, 'path')).to eq(['posts', 0, 'title'])
[thread1, thread2].map(&:join)

expect(@result1.dig('extensions', 'tracing', 'execution', 'resolvers', 1, 'path')).to eq(['posts', 0, 'id'])
expect(@result1.dig('extensions', 'tracing', 'execution', 'resolvers', 2, 'path')).to eq(['posts', 0, 'slow_id'])
expect(@result2.dig('extensions', 'tracing', 'execution', 'resolvers', 1, 'path')).to eq(['posts', 0, 'title'])
end
end
end
10 changes: 10 additions & 0 deletions spec/fixtures/apollo-engine-proxy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"apiKey": "service:YOUR_ENGINE_API_KEY",
"logging": { "level": "DEBUG" },
"origins": [{
"http": { "url": "http://localhost:3000/graphql" }
}],
"frontends": [{
"host": "127.0.0.1", "port": 3001, "endpoint": "/graphql"
}]
}

0 comments on commit ca9b02f

Please sign in to comment.