optics-agent 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +48 -89
- data/lib/optics-agent.rb +1 -0
- data/lib/optics-agent/agent.rb +113 -54
- data/lib/optics-agent/instrumentation/query-schema.rb +5 -1
- data/lib/optics-agent/instrumenters/field.rb +51 -0
- data/lib/optics-agent/instrumenters/patch-graphql-schema.rb +29 -0
- data/lib/optics-agent/normalization/query.rb +2 -2
- data/lib/optics-agent/rack-middleware.rb +13 -8
- data/lib/optics-agent/reporting/helpers.rb +13 -8
- data/lib/optics-agent/reporting/query-trace.rb +7 -7
- data/lib/optics-agent/reporting/query.rb +19 -5
- data/lib/optics-agent/reporting/report.rb +35 -12
- data/lib/optics-agent/reporting/report_job.rb +10 -9
- data/lib/optics-agent/reporting/schema_job.rb +8 -2
- data/spec/{graphql-middleware_spec.rb → field_instrumenter_spec.rb} +10 -7
- data/spec/query_trace_spec.rb +7 -6
- data/spec/report_spec.rb +84 -27
- metadata +49 -6
- data/lib/optics-agent/graphql-middleware.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6c80fb6905bbb97a8edee4ae2fb1d57d9cca76c
|
4
|
+
data.tar.gz: 969d07c49e74405731ba62ab699982f86c41daa4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b6dbc2a2f0a7f259ba2b4994be4c054e05bdc8ec8fe6d8a4da484830bcdbc5c5afa739dea52a62a4765750b73947ebe9c2a096a7b345383ae0e7c7beb0e92fa
|
7
|
+
data.tar.gz: af83946a5b4e0990de46b802865a95fecd8612a2adb085fafe5c0b4b6d8caa69b86909c0cdb29077e9989ec0238ae55a56048dc83c90946fe91d1f50f4231063
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# optics-agent-ruby
|
2
2
|
Optics Agent for GraphQL Monitoring in Ruby.
|
3
3
|
|
4
|
-
This is an alpha release, suitable for use in development contexts. There are still some outstanding improvements to make it ready for production contexts; see the [known limitations](#known-limitations) section below.
|
5
|
-
|
6
4
|
[![Gem Version](https://badge.fury.io/rb/optics-agent.svg)](https://badge.fury.io/rb/optics-agent) [![Build Status](https://travis-ci.org/apollostack/optics-agent-ruby.svg?branch=master)](https://travis-ci.org/apollostack/optics-agent-ruby)
|
7
5
|
|
8
6
|
|
@@ -16,96 +14,23 @@ gem 'optics-agent'
|
|
16
14
|
|
17
15
|
To your `Gemfile`
|
18
16
|
|
19
|
-
## Setup
|
20
|
-
|
21
17
|
### API key
|
22
18
|
|
23
|
-
You'll need to run your app with the `OPTICS_API_KEY` environment variable set (or set via
|
24
|
-
|
25
|
-
### Configuration
|
26
|
-
|
27
|
-
After creating an agent (see below), you can configure it with
|
28
|
-
|
29
|
-
```rb
|
30
|
-
agent.set_options(option: value)
|
31
|
-
```
|
32
|
-
|
33
|
-
Possible options are:
|
34
|
-
|
35
|
-
- `api_key` - Your API key for the Optics service. This defaults to the OPTICS_API_KEY environment variable, but can be overridden here.
|
36
|
-
- `endpoint_url ['https://optics-report.apollodata.com']` - Where to send the reports. Defaults to the production Optics endpoint, or the `OPTICS_ENDPOINT_URL` environment variable if it is set. You shouldn't need to set this unless you are debugging
|
37
|
-
- `debug [false]` - Log detailed debugging messages
|
38
|
-
- `disable_reporting [false]` - Don't report anything to Optics (useful for testing)
|
39
|
-
- `print_reports [false]` - Print JSON versions of the data sent to Optics to the log
|
40
|
-
- `schema_report_delay_ms [10000]` - How long to wait before sending a schema report after startup, in, milliseconds
|
41
|
-
- `report_interval_ms [60000]` - How often to send reports in milliseconds. Defaults to 1 minute. Minimum 10 seconds. You shouldn't need to set this unless you are debugging.
|
42
|
-
|
43
|
-
### Basic Rack/Sinatra
|
44
|
-
|
45
|
-
Create an agent
|
46
|
-
|
47
|
-
```ruby
|
48
|
-
# we expect one day there'll be some options
|
49
|
-
agent = OpticsAgent::Agent.instance
|
50
|
-
```
|
51
|
-
|
52
|
-
Register the Rack middleware (say in a `config.ru`):
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
use agent.rack_middleware
|
56
|
-
```
|
57
|
-
|
58
|
-
Register the GraphQL middleware:
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
agent.instrument_schema(YourSchema)
|
62
|
-
```
|
63
|
-
|
64
|
-
Add something like this to your route:
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
post '/graphql' do
|
68
|
-
request.body.rewind
|
69
|
-
params = JSON.parse request.body.read
|
70
|
-
document = params['query']
|
71
|
-
variables = params['variables']
|
72
|
-
|
73
|
-
result = Schema.execute(
|
74
|
-
document,
|
75
|
-
variables: variables,
|
76
|
-
context: { optics_agent: env[:optics_agent].with_document(document) }
|
77
|
-
)
|
78
|
-
|
79
|
-
JSON.generate(result)
|
80
|
-
end
|
81
|
-
```
|
19
|
+
You'll need to run your app with the `OPTICS_API_KEY` environment variable set (or set via `agent.configure`) to the API key of your Apollo Optics service; you can get an API key by setting up a service at https://optics.apollodata.com.
|
82
20
|
|
83
|
-
## Rails
|
84
|
-
|
85
|
-
The equivalent of the above for Rails is:
|
86
|
-
|
87
|
-
Create an agent in `config/application.rb`, and register the rack middleware:
|
21
|
+
## Rails Setup
|
88
22
|
|
23
|
+
Create an agent in `config/initializers/optics_agent.rb`, and register the rack middleware:
|
89
24
|
```ruby
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
config.optics_agent = OpticsAgent::Agent.instance
|
95
|
-
config.middleware.use config.optics_agent.rack_middleware
|
96
|
-
end
|
25
|
+
optics_agent = OpticsAgent::Agent.new
|
26
|
+
optics_agent.configure do
|
27
|
+
schema YourSchema
|
28
|
+
# See other configuration options below
|
97
29
|
end
|
98
|
-
|
99
|
-
```
|
100
|
-
|
101
|
-
Register the GraphQL middleware when you create your schema:
|
102
|
-
|
103
|
-
```ruby
|
104
|
-
Rails.application.config.optics_agent.instrument_schema(YourSchema)
|
30
|
+
Rails.application.config.middleware.use optics_agent.rack_middleware
|
105
31
|
```
|
106
32
|
|
107
33
|
Register Optics Agent on the GraphQL context within your `graphql` action as below:
|
108
|
-
|
109
34
|
```ruby
|
110
35
|
def create
|
111
36
|
query_string = params[:query]
|
@@ -125,16 +50,50 @@ end
|
|
125
50
|
|
126
51
|
You can check out the GitHunt Rails API server example here: https://github.com/apollostack/githunt-api-rails
|
127
52
|
|
128
|
-
##
|
53
|
+
## General Setup
|
129
54
|
|
130
|
-
|
55
|
+
You must:
|
131
56
|
|
132
|
-
|
57
|
+
1. Create an agent with `OpticsAgent::Agent.new`
|
58
|
+
2. Register your schema with the `agent.configure` block
|
59
|
+
3. Attach the `agent.rack_middleware` to your Rack router
|
60
|
+
4. Ensure you pass the `optics_agent` context from the rack environment to your schema execution.
|
133
61
|
|
134
|
-
|
135
|
-
|
62
|
+
### Configuration
|
63
|
+
|
64
|
+
After creating an agent, you can configure it with:
|
65
|
+
|
66
|
+
```rb
|
67
|
+
# defaults are show below
|
68
|
+
agent.configure do
|
69
|
+
# The schema you wish to instrument
|
70
|
+
schema YourSchema
|
71
|
+
|
72
|
+
# Your API key for the Optics service. This defaults to the OPTICS_API_KEY environment variable, but can be overridden here.
|
73
|
+
api_key ENV['OPTICS_API_KEY']
|
74
|
+
|
75
|
+
# Log detailed debugging messages
|
76
|
+
debug false
|
77
|
+
|
78
|
+
# Don't report anything to Optics (useful for testing)
|
79
|
+
disable_reporting false
|
136
80
|
|
137
|
-
|
81
|
+
# Print JSON versions of the data sent to Optics to the log
|
82
|
+
print_reports false
|
83
|
+
|
84
|
+
# Send detailed traces along with usage reports
|
85
|
+
report_traces true
|
86
|
+
|
87
|
+
# How long to wait before sending a schema report after startup, in, milliseconds
|
88
|
+
schema_report_delay_ms 10 * 1000
|
89
|
+
|
90
|
+
# How often to send reports in milliseconds. Defaults to 1 minute. Minimum 10 seconds. You shouldn't need to set this unless you are debugging.
|
91
|
+
report_interval_ms 60 * 1000
|
92
|
+
|
93
|
+
# Where to send the reports. Defaults to the production Optics endpoint, or the `OPTICS_ENDPOINT_URL` environment variable if it is set. You shouldn't need to set this unless you are debugging
|
94
|
+
endpoint_url 'https://optics-report.apollodata.com'
|
95
|
+
end
|
96
|
+
```
|
138
97
|
|
139
98
|
## Development
|
140
99
|
|
data/lib/optics-agent.rb
CHANGED
data/lib/optics-agent/agent.rb
CHANGED
@@ -1,89 +1,112 @@
|
|
1
|
-
require 'singleton'
|
2
1
|
require 'optics-agent/rack-middleware'
|
3
|
-
require 'optics-agent/
|
2
|
+
require 'optics-agent/instrumenters/field'
|
4
3
|
require 'optics-agent/reporting/report_job'
|
5
4
|
require 'optics-agent/reporting/schema_job'
|
6
5
|
require 'optics-agent/reporting/query-trace'
|
7
6
|
require 'net/http'
|
7
|
+
require 'faraday'
|
8
8
|
|
9
9
|
module OpticsAgent
|
10
|
-
# XXX: this is a class but acts as a singleton right now.
|
11
|
-
# Need to figure out how to pass the agent into the middleware
|
12
|
-
# (for instance we could dynamically generate a middleware class,
|
13
|
-
# or ask the user to pass the agent as an option) to avoid it
|
14
10
|
class Agent
|
15
|
-
include Singleton
|
16
11
|
include OpticsAgent::Reporting
|
17
12
|
|
18
|
-
attr_reader :schema
|
13
|
+
attr_reader :schema, :report_traces
|
19
14
|
|
20
15
|
def initialize
|
21
16
|
@query_queue = []
|
22
17
|
@semaphone = Mutex.new
|
23
18
|
|
24
19
|
# set defaults
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@
|
42
|
-
@api_key = api_key
|
43
|
-
@endpoint_url = endpoint_url
|
20
|
+
@configuration = Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure(&block)
|
24
|
+
@configuration.instance_eval(&block)
|
25
|
+
|
26
|
+
if @configuration.schema && @schema != @configuration.schema
|
27
|
+
instrument_schema(@configuration.schema)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def disabled?
|
32
|
+
@configuration.disable_reporting || !@configuration.api_key || !@schema
|
33
|
+
end
|
34
|
+
|
35
|
+
def report_traces?
|
36
|
+
@configuration.report_traces
|
44
37
|
end
|
45
38
|
|
46
39
|
def instrument_schema(schema)
|
40
|
+
unless @configuration.api_key
|
41
|
+
warn """No api_key set.
|
42
|
+
Either configure it or use the OPTICS_API_KEY environment variable.
|
43
|
+
"""
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
if @schema
|
48
|
+
warn """Agent has already instrumented a schema.
|
49
|
+
Perhaps you are calling both `agent.configure { schema YourSchema }` and
|
50
|
+
`agent.instrument_schema YourSchema`?
|
51
|
+
"""
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
47
55
|
@schema = schema
|
48
|
-
|
49
|
-
schema.middleware << graphql_middleware
|
56
|
+
@schema._attach_optics_agent(self)
|
50
57
|
|
51
|
-
unless
|
58
|
+
unless disabled?
|
52
59
|
debug "spawning schema thread"
|
53
60
|
Thread.new do
|
54
61
|
debug "schema thread spawned"
|
55
|
-
sleep @schema_report_delay_ms / 1000
|
62
|
+
sleep @configuration.schema_report_delay_ms / 1000.0
|
56
63
|
debug "running schema job"
|
57
64
|
SchemaJob.new.perform(self)
|
58
65
|
end
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
#
|
69
|
+
# We call this method on every request to ensure that the reporting thread
|
63
70
|
# is active in the correct process for pre-forking webservers like unicorn
|
64
71
|
def ensure_reporting!
|
65
|
-
unless @
|
72
|
+
unless @schema
|
73
|
+
warn """No schema instrumented.
|
74
|
+
Use the `schema` configuration setting, or call `agent.instrument_schema`
|
75
|
+
"""
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
unless @reporting_thread_active || disabled?
|
66
80
|
schedule_report
|
67
81
|
@reporting_thread_active = true
|
68
82
|
end
|
69
83
|
end
|
70
84
|
|
85
|
+
def reporting_connection
|
86
|
+
@reporting_connection ||=
|
87
|
+
Faraday.new(:url => @configuration.endpoint_url) do |faraday|
|
88
|
+
# XXX: allow setting adaptor in config
|
89
|
+
faraday.adapter :net_http_persistent
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
71
93
|
def schedule_report
|
72
94
|
debug "spawning reporting thread"
|
73
95
|
Thread.new do
|
74
96
|
debug "reporting thread spawned"
|
75
97
|
while true
|
76
|
-
sleep @report_interval_ms / 1000
|
98
|
+
sleep @configuration.report_interval_ms / 1000.0
|
77
99
|
debug "running reporting job"
|
78
100
|
ReportJob.new.perform(self)
|
101
|
+
debug "finished running reporting job"
|
79
102
|
end
|
80
103
|
end
|
81
104
|
end
|
82
105
|
|
83
|
-
def add_query(
|
106
|
+
def add_query(*args)
|
84
107
|
@semaphone.synchronize {
|
85
108
|
debug { "adding query to queue, queue length was #{@query_queue.length}" }
|
86
|
-
@query_queue <<
|
109
|
+
@query_queue << args
|
87
110
|
}
|
88
111
|
end
|
89
112
|
|
@@ -97,32 +120,31 @@ module OpticsAgent
|
|
97
120
|
end
|
98
121
|
|
99
122
|
def rack_middleware
|
123
|
+
# We need to pass ourselves to the class we return here because
|
124
|
+
# rack will instanciate it. (See comment at the top of RackMiddleware)
|
125
|
+
OpticsAgent::RackMiddleware.agent = self
|
100
126
|
OpticsAgent::RackMiddleware
|
101
127
|
end
|
102
128
|
|
103
129
|
def graphql_middleware
|
104
|
-
|
105
|
-
OpticsAgent::GraphqlMiddleware.new
|
130
|
+
warn "You no longer need to pass the optics agent middleware, it now attaches itself"
|
106
131
|
end
|
107
132
|
|
108
133
|
def send_message(path, message)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
134
|
+
response = reporting_connection.post do |request|
|
135
|
+
request.url path
|
136
|
+
request.headers['x-api-key'] = @configuration.api_key
|
137
|
+
request.headers['user-agent'] = "optics-agent-rb"
|
138
|
+
|
139
|
+
request.body = message.class.encode(message)
|
140
|
+
if @configuration.debug || @configuration.print_reports
|
141
|
+
log "sending message: #{message.class.encode_json(message)}"
|
142
|
+
end
|
116
143
|
end
|
117
144
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
res = http.request(req)
|
122
|
-
|
123
|
-
if @debug || @print_reports
|
124
|
-
log "got response: #{res.inspect}"
|
125
|
-
log "response body: #{res.body.inspect}"
|
145
|
+
if @configuration.debug || @configuration.print_reports
|
146
|
+
log "got response: #{response}"
|
147
|
+
log "response body: #{response.body}"
|
126
148
|
end
|
127
149
|
end
|
128
150
|
|
@@ -131,11 +153,48 @@ module OpticsAgent
|
|
131
153
|
puts "optics-agent: #{message}"
|
132
154
|
end
|
133
155
|
|
156
|
+
def warn(message = nil)
|
157
|
+
log "WARNING: #{message}"
|
158
|
+
end
|
159
|
+
|
134
160
|
def debug(message = nil)
|
135
|
-
if @debug
|
161
|
+
if @configuration.debug
|
136
162
|
message = yield unless message
|
137
163
|
log "DEBUG: #{message} <#{Process.pid} | #{Thread.current.object_id}>"
|
138
164
|
end
|
139
165
|
end
|
140
166
|
end
|
167
|
+
|
168
|
+
class Configuration
|
169
|
+
def self.defaults
|
170
|
+
{
|
171
|
+
schema: nil,
|
172
|
+
debug: false,
|
173
|
+
disable_reporting: false,
|
174
|
+
print_reports: false,
|
175
|
+
report_traces: true,
|
176
|
+
schema_report_delay_ms: 10 * 1000,
|
177
|
+
report_interval_ms: 60 * 1000,
|
178
|
+
api_key: ENV['OPTICS_API_KEY'],
|
179
|
+
endpoint_url: ENV['OPTICS_ENDPOINT_URL'] || 'https://optics-report.apollodata.com'
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
# Allow e.g. `debug false` == `debug = false` in configuration blocks
|
184
|
+
defaults.each_key do |key|
|
185
|
+
define_method key, ->(*maybe_value) do
|
186
|
+
if (maybe_value.length === 1)
|
187
|
+
self.instance_variable_set("@#{key}", maybe_value[0])
|
188
|
+
elsif (maybe_value.length === 0)
|
189
|
+
self.instance_variable_get("@#{key}")
|
190
|
+
else
|
191
|
+
throw new ArgumentError("0 or 1 argument expected")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def initialize
|
197
|
+
self.class.defaults.each { |key, value| self.send(key, value) }
|
198
|
+
end
|
199
|
+
end
|
141
200
|
end
|
@@ -3,7 +3,11 @@ module OpticsAgent
|
|
3
3
|
INTROSPECTION_QUERY ||= IO.read("#{File.dirname(__FILE__)}/introspection-query.graphql")
|
4
4
|
|
5
5
|
def introspect_schema(schema)
|
6
|
-
schema.execute(INTROSPECTION_QUERY
|
6
|
+
result = schema.execute(INTROSPECTION_QUERY,
|
7
|
+
context: {optics_agent: :skip}
|
8
|
+
)
|
9
|
+
|
10
|
+
result['data']['__schema']
|
7
11
|
end
|
8
12
|
end
|
9
13
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module OpticsAgent
|
2
|
+
module Instrumenters
|
3
|
+
class Field
|
4
|
+
attr_accessor :agent
|
5
|
+
|
6
|
+
def instrument(type, field)
|
7
|
+
old_resolve_proc = field.resolve_proc
|
8
|
+
new_resolve_proc = ->(obj, args, ctx) {
|
9
|
+
if @agent
|
10
|
+
middleware(@agent, type, obj, field, args, ctx, ->() { old_resolve_proc.call(obj, args, ctx) })
|
11
|
+
else
|
12
|
+
old_resolve_proc.call(obj, args, ctx)
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
field.redefine do
|
17
|
+
resolve(new_resolve_proc)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def middleware(agent, parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
|
22
|
+
agent_context = query_context[:optics_agent]
|
23
|
+
|
24
|
+
unless agent_context
|
25
|
+
agent.warn """No agent passed in graphql context.
|
26
|
+
Ensure you set `context: {optics_agent: env[:optics_agent].with_document(document) }``
|
27
|
+
when executing your graphql query.
|
28
|
+
If you don't want to instrument this query, pass `context: {optics_agent: :skip}`.
|
29
|
+
"""
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
# This happens when an introspection query occurs (reporting schema)
|
34
|
+
# Also, people could potentially use it to skip reporting
|
35
|
+
if agent_context == :skip
|
36
|
+
return next_middleware.call
|
37
|
+
end
|
38
|
+
|
39
|
+
query = agent_context.query
|
40
|
+
|
41
|
+
start_offset = query.duration_so_far
|
42
|
+
result = next_middleware.call
|
43
|
+
duration = query.duration_so_far - start_offset
|
44
|
+
|
45
|
+
query.report_field(parent_type.to_s, field_definition.name, start_offset, duration)
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Monkey patch GraphQL::Schema.define so that it automatically attaches our
|
2
|
+
# field instrumenter. Our instrumenter will do nothing unless we later attach
|
3
|
+
# an agent to it, which this will also allow us to do.
|
4
|
+
|
5
|
+
require 'graphql'
|
6
|
+
require 'optics-agent/instrumenters/field'
|
7
|
+
|
8
|
+
module OpticsAgent::GraphQLSchemaExtensions
|
9
|
+
def define(**kwargs, &block)
|
10
|
+
@instrumenter = OpticsAgent::Instrumenters::Field.new
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def _attach_optics_agent(agent)
|
14
|
+
agent.debug "Attaching agent to field instrumenter"
|
15
|
+
@instrumenter.agent = agent
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
instrumenter = @instrumenter
|
20
|
+
super **kwargs do
|
21
|
+
instance_eval(&block) if block
|
22
|
+
instrument :field, instrumenter
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class GraphQL::Schema
|
28
|
+
prepend OpticsAgent::GraphQLSchemaExtensions
|
29
|
+
end
|
@@ -58,7 +58,7 @@ module OpticsAgent
|
|
58
58
|
|
59
59
|
visitor[Nodes::InlineFragment].leave << -> (node, parent) do
|
60
60
|
selections = current[:selections]
|
61
|
-
stack[0][:selections] << "... on #{node.type} #{block(selections)}"
|
61
|
+
stack[0][:selections] << "... on #{node.type.name} #{block(selections)}"
|
62
62
|
end
|
63
63
|
|
64
64
|
visitor[Nodes::FragmentSpread].leave << -> (node, parent) do
|
@@ -90,7 +90,7 @@ module OpticsAgent
|
|
90
90
|
visitor[Nodes::FragmentDefinition].leave << -> (node, parent) do
|
91
91
|
selections = current[:selections]
|
92
92
|
if (used_fragment_names.include?(node.name))
|
93
|
-
output << " fragment #{node.name} on #{node.type} " \
|
93
|
+
output << " fragment #{node.name} on #{node.type.name} " \
|
94
94
|
+ block(selections)
|
95
95
|
end
|
96
96
|
end
|
@@ -1,19 +1,25 @@
|
|
1
1
|
require 'optics-agent/agent'
|
2
2
|
require 'optics-agent/reporting/query'
|
3
|
-
|
4
3
|
module OpticsAgent
|
5
4
|
class RackMiddleware
|
5
|
+
# Right now we assume there'll only be a single rack middleware in an
|
6
|
+
# app, and set the agent via a class attribute. This means that in theory
|
7
|
+
# you could use more than one agent, but at most one middleware.
|
8
|
+
# In the future, if we see a need for more than one middleware, we could
|
9
|
+
# probably just copy the class when calling `agent.rack_middleware`
|
10
|
+
class << self
|
11
|
+
attr_accessor :agent
|
12
|
+
end
|
13
|
+
|
6
14
|
def initialize(app, options={})
|
7
15
|
@app = app
|
8
16
|
end
|
9
17
|
|
10
18
|
def call(env)
|
11
|
-
|
12
|
-
|
13
|
-
# XXX: figure out a way to pass this in here
|
14
|
-
agent = OpticsAgent::Agent.instance
|
19
|
+
agent = self.class.agent
|
15
20
|
agent.ensure_reporting!
|
16
21
|
agent.debug { "rack-middleware: request started" }
|
22
|
+
|
17
23
|
query = OpticsAgent::Reporting::Query.new
|
18
24
|
|
19
25
|
# Attach so resolver middleware can access
|
@@ -21,12 +27,11 @@ module OpticsAgent
|
|
21
27
|
|
22
28
|
result = @app.call(env)
|
23
29
|
|
24
|
-
# XXX: this approach means if the user forgets to call with_document
|
25
|
-
# we just never log queries. Can we detect if the request is a graphql one?
|
26
30
|
agent.debug { "rack-middleware: request finished" }
|
27
31
|
if (query.document)
|
28
32
|
agent.debug { "rack-middleware: adding query to agent" }
|
29
|
-
|
33
|
+
query.finish!
|
34
|
+
agent.add_query(query, env)
|
30
35
|
end
|
31
36
|
|
32
37
|
result
|
@@ -11,13 +11,16 @@ module OpticsAgent::Reporting
|
|
11
11
|
def generate_timestamp(time)
|
12
12
|
Apollo::Optics::Proto::Timestamp.new({
|
13
13
|
seconds: time.to_i,
|
14
|
-
nanos: time.to_i % 1
|
14
|
+
nanos: duration_nanos(time.to_i % 1)
|
15
15
|
});
|
16
16
|
end
|
17
17
|
|
18
|
-
def duration_nanos(
|
19
|
-
|
20
|
-
|
18
|
+
def duration_nanos(duration_in_seconds)
|
19
|
+
(duration_in_seconds * 1e9).to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
def duration_micros(duration_in_seconds)
|
23
|
+
(duration_in_seconds * 1e6).to_i
|
21
24
|
end
|
22
25
|
|
23
26
|
# XXX: implement
|
@@ -29,9 +32,11 @@ module OpticsAgent::Reporting
|
|
29
32
|
}
|
30
33
|
end
|
31
34
|
|
32
|
-
def add_latency(counts,
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
def add_latency(counts, duration_in_seconds)
|
36
|
+
counts[latency_bucket_for_duration(duration_in_seconds)] += 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def latency_bucket_for_duration(duration_in_seconds)
|
40
|
+
latency_bucket(duration_micros(duration_in_seconds))
|
36
41
|
end
|
37
42
|
end
|
@@ -10,11 +10,11 @@ module OpticsAgent::Reporting
|
|
10
10
|
|
11
11
|
attr_accessor :report
|
12
12
|
|
13
|
-
def initialize(query, rack_env
|
13
|
+
def initialize(query, rack_env)
|
14
14
|
trace = Trace.new({
|
15
|
-
start_time: generate_timestamp(start_time),
|
16
|
-
end_time: generate_timestamp(end_time),
|
17
|
-
duration_ns: duration_nanos(
|
15
|
+
start_time: generate_timestamp(query.start_time),
|
16
|
+
end_time: generate_timestamp(query.end_time),
|
17
|
+
duration_ns: duration_nanos(query.duration),
|
18
18
|
signature: query.signature
|
19
19
|
})
|
20
20
|
|
@@ -31,11 +31,11 @@ module OpticsAgent::Reporting
|
|
31
31
|
})
|
32
32
|
|
33
33
|
nodes = []
|
34
|
-
query.each_report do |type_name, field_name,
|
34
|
+
query.each_report do |type_name, field_name, start_offset, duration|
|
35
35
|
nodes << Trace::Node.new({
|
36
36
|
field_name: "#{type_name}.#{field_name}",
|
37
|
-
start_time: duration_nanos(
|
38
|
-
end_time: duration_nanos(
|
37
|
+
start_time: duration_nanos(start_offset),
|
38
|
+
end_time: duration_nanos(start_offset + duration)
|
39
39
|
})
|
40
40
|
end
|
41
41
|
trace.execute = Trace::Node.new({
|
@@ -2,6 +2,8 @@ require 'apollo/optics/proto/reports_pb'
|
|
2
2
|
require 'optics-agent/reporting/helpers'
|
3
3
|
require 'optics-agent/normalization/latency'
|
4
4
|
require 'optics-agent/normalization/query'
|
5
|
+
require 'hitimes'
|
6
|
+
require 'forwardable'
|
5
7
|
|
6
8
|
module OpticsAgent::Reporting
|
7
9
|
# This is a convenience class that enables us to fairly blindly
|
@@ -12,13 +14,25 @@ module OpticsAgent::Reporting
|
|
12
14
|
include OpticsAgent::Normalization
|
13
15
|
include OpticsAgent::Normalization::Query
|
14
16
|
|
17
|
+
extend Forwardable
|
18
|
+
|
15
19
|
attr_accessor :document
|
20
|
+
attr_reader :start_time, :end_time
|
21
|
+
def_delegators :@interval, :duration, :duration_so_far
|
16
22
|
|
17
23
|
def initialize
|
18
24
|
@reports = []
|
19
25
|
|
20
26
|
@document = nil
|
21
|
-
@signature
|
27
|
+
@signature = nil
|
28
|
+
|
29
|
+
@start_time = Time.now
|
30
|
+
@interval = Hitimes::Interval.now
|
31
|
+
end
|
32
|
+
|
33
|
+
def finish!
|
34
|
+
@end_time = Time.now
|
35
|
+
@interval.stop
|
22
36
|
end
|
23
37
|
|
24
38
|
def signature
|
@@ -32,8 +46,8 @@ module OpticsAgent::Reporting
|
|
32
46
|
end
|
33
47
|
|
34
48
|
# we do nothing when reporting to minimize impact
|
35
|
-
def report_field(type_name, field_name,
|
36
|
-
@reports << [type_name, field_name,
|
49
|
+
def report_field(type_name, field_name, start_offset, duration)
|
50
|
+
@reports << [type_name, field_name, start_offset, duration]
|
37
51
|
end
|
38
52
|
|
39
53
|
def each_report
|
@@ -44,7 +58,7 @@ module OpticsAgent::Reporting
|
|
44
58
|
|
45
59
|
# add our results to an existing StatsPerSignature
|
46
60
|
def add_to_stats(stats_per_signature)
|
47
|
-
each_report do |type_name, field_name,
|
61
|
+
each_report do |type_name, field_name, start_offset, duration|
|
48
62
|
type_stat = stats_per_signature.per_type.find { |ts| ts.name == type_name }
|
49
63
|
unless type_stat
|
50
64
|
type_stat = TypeStat.new({ name: type_name })
|
@@ -60,7 +74,7 @@ module OpticsAgent::Reporting
|
|
60
74
|
type_stat.field << field_stat
|
61
75
|
end
|
62
76
|
|
63
|
-
add_latency(field_stat.latency_count,
|
77
|
+
add_latency(field_stat.latency_count, duration)
|
64
78
|
end
|
65
79
|
end
|
66
80
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'apollo/optics/proto/reports_pb'
|
2
2
|
require 'optics-agent/reporting/helpers'
|
3
3
|
require 'optics-agent/normalization/latency'
|
4
|
+
require 'hitimes'
|
4
5
|
|
5
6
|
module OpticsAgent::Reporting
|
6
7
|
# This class represents a complete report that we send to the optics server
|
@@ -11,9 +12,10 @@ module OpticsAgent::Reporting
|
|
11
12
|
include OpticsAgent::Reporting
|
12
13
|
include OpticsAgent::Normalization
|
13
14
|
|
14
|
-
|
15
|
+
attr_reader :report
|
16
|
+
attr_reader :traces_to_report
|
15
17
|
|
16
|
-
def initialize
|
18
|
+
def initialize(report_traces: true)
|
17
19
|
# internal report that we encapsulate
|
18
20
|
@report = StatsReport.new({
|
19
21
|
header: ReportHeader.new({
|
@@ -21,28 +23,39 @@ module OpticsAgent::Reporting
|
|
21
23
|
}),
|
22
24
|
start_time: generate_timestamp(Time.now)
|
23
25
|
})
|
26
|
+
|
27
|
+
@interval = Hitimes::Interval.now
|
28
|
+
|
29
|
+
@report_traces = report_traces
|
30
|
+
@traces_to_report = []
|
24
31
|
end
|
25
32
|
|
26
33
|
def finish!
|
27
34
|
@report.end_time ||= generate_timestamp(Time.now)
|
28
|
-
@report.realtime_duration || duration_nanos(@
|
35
|
+
@report.realtime_duration || duration_nanos(@interval.stop)
|
29
36
|
end
|
30
37
|
|
31
38
|
def send_with(agent)
|
39
|
+
agent.debug do
|
40
|
+
n_queries = 0
|
41
|
+
@report.per_signature.values.each do |signature_stats|
|
42
|
+
signature_stats.per_client_name.values.each do |client_stats|
|
43
|
+
n_queries += client_stats.count_per_version.values.reduce(&:+)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
"Sending #{n_queries} queries and #{@traces_to_report.length} traces"
|
47
|
+
end
|
32
48
|
self.finish!
|
49
|
+
@traces_to_report.each do |trace|
|
50
|
+
trace.send_with(agent)
|
51
|
+
end
|
33
52
|
agent.send_message('/api/ss/stats', @report)
|
34
53
|
end
|
35
54
|
|
36
|
-
|
37
|
-
def add_query(query, rack_env, start_time, end_time)
|
55
|
+
def add_query(query, rack_env)
|
38
56
|
@report.per_signature[query.signature] ||= StatsPerSignature.new
|
39
57
|
signature_stats = @report.per_signature[query.signature]
|
40
58
|
|
41
|
-
add_client_stats(signature_stats, rack_env, start_time, end_time)
|
42
|
-
query.add_to_stats(signature_stats)
|
43
|
-
end
|
44
|
-
|
45
|
-
def add_client_stats(signature_stats, rack_env, start_time, end_time)
|
46
59
|
info = client_info(rack_env)
|
47
60
|
signature_stats.per_client_name[info[:client_name]] ||= StatsPerClientName.new({
|
48
61
|
latency_count: empty_latency_count,
|
@@ -51,10 +64,20 @@ module OpticsAgent::Reporting
|
|
51
64
|
client_stats = signature_stats.per_client_name[info[:client_name]]
|
52
65
|
|
53
66
|
# XXX: handle errors
|
54
|
-
add_latency(client_stats.latency_count,
|
55
|
-
|
67
|
+
add_latency(client_stats.latency_count, query.duration)
|
56
68
|
client_stats.count_per_version[info[:client_version]] ||= 0
|
57
69
|
client_stats.count_per_version[info[:client_version]] += 1
|
70
|
+
|
71
|
+
query.add_to_stats(signature_stats)
|
72
|
+
|
73
|
+
if @report_traces
|
74
|
+
# Is this the first query we've seen in this reporting period and
|
75
|
+
# latency bucket? In which case we want to send a trace
|
76
|
+
bucket = latency_bucket_for_duration(query.duration)
|
77
|
+
if (client_stats.latency_count[bucket] == 1)
|
78
|
+
@traces_to_report << QueryTrace.new(query, rack_env)
|
79
|
+
end
|
80
|
+
end
|
58
81
|
end
|
59
82
|
|
60
83
|
# take a graphql schema and add returnTypes to all the fields on our report
|
@@ -3,17 +3,18 @@ require 'optics-agent/reporting/report'
|
|
3
3
|
module OpticsAgent::Reporting
|
4
4
|
class ReportJob
|
5
5
|
def perform(agent)
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
begin
|
7
|
+
report = OpticsAgent::Reporting::Report.new(report_traces: agent.report_traces?)
|
8
|
+
agent.clear_query_queue.each do |item|
|
9
|
+
report.add_query(*item)
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
report.decorate_from_schema(agent.schema)
|
13
|
+
report.send_with(agent)
|
14
|
+
rescue StandardError => e
|
15
|
+
agent.debug "report failed #{e}"
|
16
|
+
raise
|
13
17
|
end
|
14
|
-
|
15
|
-
report.decorate_from_schema(agent.schema)
|
16
|
-
report.send_with(agent)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
@@ -3,8 +3,14 @@ require 'optics-agent/reporting/schema'
|
|
3
3
|
module OpticsAgent::Reporting
|
4
4
|
class SchemaJob
|
5
5
|
def perform(agent)
|
6
|
-
|
7
|
-
|
6
|
+
begin
|
7
|
+
schema = OpticsAgent::Reporting::Schema.new agent.schema
|
8
|
+
schema.send_with(agent)
|
9
|
+
rescue StandardError => e
|
10
|
+
agent.debug "schema report failed #{e}"
|
11
|
+
agent.debug e.backtrace
|
12
|
+
raise
|
13
|
+
end
|
8
14
|
end
|
9
15
|
end
|
10
16
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'ostruct'
|
2
|
-
require 'optics-agent/
|
2
|
+
require 'optics-agent/instrumenters/field'
|
3
3
|
require 'graphql'
|
4
4
|
|
5
5
|
include OpticsAgent
|
6
6
|
|
7
|
-
describe
|
7
|
+
describe Instrumenters::Field do
|
8
8
|
it 'collects the correct query stats' do
|
9
9
|
person_type = GraphQL::ObjectType.define do
|
10
10
|
name "Person"
|
@@ -25,23 +25,26 @@ describe GraphqlMiddleware do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
instrumenter = Instrumenters::Field.new
|
29
|
+
instrumenter.agent = true
|
28
30
|
schema = GraphQL::Schema.define do
|
29
31
|
query query_type
|
32
|
+
instrument :field, instrumenter
|
30
33
|
end
|
31
34
|
|
32
|
-
schema.middleware << GraphqlMiddleware.new
|
33
|
-
|
34
35
|
query = spy("query")
|
36
|
+
allow(query).to receive(:duration_so_far).and_return(1.0)
|
37
|
+
|
35
38
|
schema.execute('{ person { firstName lastName } }', {
|
36
39
|
context: { optics_agent: OpenStruct.new(query: query) }
|
37
40
|
})
|
38
41
|
|
39
42
|
expect(query).to have_received(:report_field).exactly(3).times
|
40
43
|
expect(query).to have_received(:report_field)
|
41
|
-
.with('Query', 'person', be_instance_of(
|
44
|
+
.with('Query', 'person', be_instance_of(Float), be_instance_of(Float))
|
42
45
|
expect(query).to have_received(:report_field)
|
43
|
-
.with('Person', 'firstName', be_instance_of(
|
46
|
+
.with('Person', 'firstName', be_instance_of(Float), be_instance_of(Float))
|
44
47
|
expect(query).to have_received(:report_field)
|
45
|
-
.with('Person', 'lastName', be_instance_of(
|
48
|
+
.with('Person', 'lastName', be_instance_of(Float), be_instance_of(Float))
|
46
49
|
end
|
47
50
|
end
|
data/spec/query_trace_spec.rb
CHANGED
@@ -9,12 +9,13 @@ include OpticsAgent::Reporting
|
|
9
9
|
describe QueryTrace do
|
10
10
|
it "can represent a simple query" do
|
11
11
|
query = Query.new
|
12
|
-
query.report_field 'Person', 'firstName', 1,
|
13
|
-
query.report_field 'Person', 'lastName', 1,
|
14
|
-
query.report_field 'Query', 'person', 1,
|
12
|
+
query.report_field 'Person', 'firstName', 1, 0.1
|
13
|
+
query.report_field 'Person', 'lastName', 1.1, 0.1
|
14
|
+
query.report_field 'Query', 'person', 1.2, 0.22
|
15
15
|
query.document = '{field}'
|
16
|
+
query.finish!
|
16
17
|
|
17
|
-
trace = QueryTrace.new(query, {}
|
18
|
+
trace = QueryTrace.new(query, {})
|
18
19
|
|
19
20
|
expect(trace.report).to be_instance_of(TracesReport)
|
20
21
|
expect(trace.report.trace.length).to eq(1)
|
@@ -26,7 +27,7 @@ describe QueryTrace do
|
|
26
27
|
match_array(['Query.person', 'Person.firstName', 'Person.lastName'])
|
27
28
|
|
28
29
|
firstName_node = nodes.find { |n| n.field_name == 'Person.firstName' }
|
29
|
-
expect(firstName_node.start_time).to eq(
|
30
|
-
expect(firstName_node.end_time).to eq(
|
30
|
+
expect(firstName_node.start_time).to eq(1 * 1e9)
|
31
|
+
expect(firstName_node.end_time).to eq(1.1 * 1e9)
|
31
32
|
end
|
32
33
|
end
|
data/spec/report_spec.rb
CHANGED
@@ -9,13 +9,14 @@ include Apollo::Optics::Proto
|
|
9
9
|
describe Report do
|
10
10
|
it "can represent a simple query" do
|
11
11
|
query = Query.new
|
12
|
-
query.report_field 'Person', 'firstName', 1,
|
13
|
-
query.report_field 'Person', 'lastName', 1,
|
14
|
-
query.report_field 'Query', 'person', 1,
|
12
|
+
query.report_field 'Person', 'firstName', 1, 0.1
|
13
|
+
query.report_field 'Person', 'lastName', 1.1, 0.1
|
14
|
+
query.report_field 'Query', 'person', 1.2, 0.22
|
15
|
+
query.finish!
|
15
16
|
query.document = '{field}'
|
16
17
|
|
17
18
|
report = Report.new
|
18
|
-
report.add_query query, {}
|
19
|
+
report.add_query query, {}
|
19
20
|
report.finish!
|
20
21
|
|
21
22
|
expect(report.report).to be_an_instance_of(StatsReport)
|
@@ -38,20 +39,22 @@ describe Report do
|
|
38
39
|
|
39
40
|
it "can aggregate the results of multiple queries with the same shape" do
|
40
41
|
queryOne = Query.new
|
41
|
-
queryOne.report_field 'Person', 'firstName', 1,
|
42
|
-
queryOne.report_field 'Person', 'lastName', 1,
|
43
|
-
queryOne.report_field 'Query', 'person', 1,
|
42
|
+
queryOne.report_field 'Person', 'firstName', 1, 0.1
|
43
|
+
queryOne.report_field 'Person', 'lastName', 1.1, 0.1
|
44
|
+
queryOne.report_field 'Query', 'person', 1.2, 0.22
|
45
|
+
queryOne.finish!
|
44
46
|
queryOne.document = '{field}'
|
45
47
|
|
46
48
|
queryTwo = Query.new
|
47
|
-
queryTwo.report_field 'Person', 'firstName', 1,
|
48
|
-
queryTwo.report_field 'Person', 'lastName', 1,
|
49
|
-
queryTwo.report_field 'Query', 'person', 1,
|
49
|
+
queryTwo.report_field 'Person', 'firstName', 1, 0.05
|
50
|
+
queryTwo.report_field 'Person', 'lastName', 1.05, 0.05
|
51
|
+
queryTwo.report_field 'Query', 'person', 1.1, 0.20
|
52
|
+
queryTwo.finish!
|
50
53
|
queryTwo.document = '{field}'
|
51
54
|
|
52
55
|
report = Report.new
|
53
|
-
report.add_query queryOne, {}
|
54
|
-
report.add_query queryTwo, {}
|
56
|
+
report.add_query queryOne, {}
|
57
|
+
report.add_query queryTwo, {}
|
55
58
|
report.finish!
|
56
59
|
|
57
60
|
expect(report.report).to be_an_instance_of(StatsReport)
|
@@ -74,20 +77,22 @@ describe Report do
|
|
74
77
|
|
75
78
|
it "can aggregate the results of multiple queries with a different shape" do
|
76
79
|
queryOne = Query.new
|
77
|
-
queryOne.report_field 'Person', 'firstName', 1,
|
78
|
-
queryOne.report_field 'Person', 'lastName', 1,
|
79
|
-
queryOne.report_field 'Query', 'person', 1,
|
80
|
+
queryOne.report_field 'Person', 'firstName', 1, 0.1
|
81
|
+
queryOne.report_field 'Person', 'lastName', 1.1, 0.1
|
82
|
+
queryOne.report_field 'Query', 'person', 1.2, 0.22
|
83
|
+
queryOne.finish!
|
80
84
|
queryOne.document = '{fieldOne}'
|
81
85
|
|
82
86
|
queryTwo = Query.new
|
83
|
-
queryTwo.report_field 'Person', 'firstName', 1,
|
84
|
-
queryTwo.report_field 'Person', 'lastName', 1,
|
85
|
-
queryTwo.report_field 'Query', 'person', 1,
|
87
|
+
queryTwo.report_field 'Person', 'firstName', 1, 0.05
|
88
|
+
queryTwo.report_field 'Person', 'lastName', 1.05, 0.05
|
89
|
+
queryTwo.report_field 'Query', 'person', 1.1, 0.20
|
90
|
+
queryTwo.finish!
|
86
91
|
queryTwo.document = '{fieldTwo}'
|
87
92
|
|
88
93
|
report = Report.new
|
89
|
-
report.add_query queryOne, {}
|
90
|
-
report.add_query queryTwo, {}
|
94
|
+
report.add_query queryOne, {}
|
95
|
+
report.add_query queryTwo, {}
|
91
96
|
report.finish!
|
92
97
|
|
93
98
|
expect(report.report).to be_an_instance_of(StatsReport)
|
@@ -110,12 +115,13 @@ describe Report do
|
|
110
115
|
|
111
116
|
it "can decorate it's fields with resultTypes from a schema" do
|
112
117
|
query = Query.new
|
113
|
-
query.report_field 'Person', 'firstName', 1,
|
114
|
-
query.report_field 'Person', 'age', 1,
|
118
|
+
query.report_field 'Person', 'firstName', 1, 0.1
|
119
|
+
query.report_field 'Person', 'age', 1.1, 0.1
|
120
|
+
query.finish!
|
115
121
|
query.document = '{field}'
|
116
122
|
|
117
123
|
report = Report.new
|
118
|
-
report.add_query query, {}
|
124
|
+
report.add_query query, {}
|
119
125
|
report.finish!
|
120
126
|
|
121
127
|
person_type = GraphQL::ObjectType.define do
|
@@ -147,13 +153,14 @@ describe Report do
|
|
147
153
|
|
148
154
|
it "can handle introspection fields" do
|
149
155
|
query = Query.new
|
150
|
-
query.report_field 'Query', '__schema', 1,
|
151
|
-
query.report_field 'Query', '__typename', 1,
|
152
|
-
query.report_field 'Query', '__type', 1, 1.1
|
156
|
+
query.report_field 'Query', '__schema', 1, 0.1
|
157
|
+
query.report_field 'Query', '__typename', 1.1, 0.1
|
158
|
+
query.report_field 'Query', '__type', 1.2, 1.1
|
159
|
+
query.finish!
|
153
160
|
query.document = '{field}'
|
154
161
|
|
155
162
|
report = Report.new
|
156
|
-
report.add_query query, {}
|
163
|
+
report.add_query query, {}
|
157
164
|
report.finish!
|
158
165
|
|
159
166
|
query_type = GraphQL::ObjectType.define do
|
@@ -180,4 +187,54 @@ describe Report do
|
|
180
187
|
expect(typename_stats.returnType).to eq('Query')
|
181
188
|
end
|
182
189
|
|
190
|
+
describe "trace reporting" do
|
191
|
+
class QueryMock
|
192
|
+
attr_reader :signature, :duration, :start_time, :end_time
|
193
|
+
def initialize(signature, duration)
|
194
|
+
@signature = signature
|
195
|
+
@duration = duration
|
196
|
+
@start_time = Time.now
|
197
|
+
@end_time = Time.now
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_to_stats(_); end
|
201
|
+
def each_report(); end
|
202
|
+
end
|
203
|
+
|
204
|
+
it "only sends one trace for two queries of the same shape and latency" do
|
205
|
+
queryOne = QueryMock.new '{field}', 1
|
206
|
+
queryTwo = QueryMock.new '{field}', 1
|
207
|
+
|
208
|
+
report = Report.new
|
209
|
+
report.add_query queryOne, {}
|
210
|
+
report.add_query queryTwo, {}
|
211
|
+
report.finish!
|
212
|
+
|
213
|
+
expect(report.traces_to_report.length).to be(1)
|
214
|
+
end
|
215
|
+
|
216
|
+
it "sends two traces for two queries of the same shape and different latencies" do
|
217
|
+
queryOne = QueryMock.new '{field}', 1
|
218
|
+
queryTwo = QueryMock.new '{field}', 1.1
|
219
|
+
|
220
|
+
report = Report.new
|
221
|
+
report.add_query queryOne, {}
|
222
|
+
report.add_query queryTwo, {}
|
223
|
+
report.finish!
|
224
|
+
|
225
|
+
expect(report.traces_to_report.length).to be(2)
|
226
|
+
end
|
227
|
+
|
228
|
+
it "sends two traces for two queries of different shapes and the same latency" do
|
229
|
+
queryOne = QueryMock.new '{fieldOne}', 1
|
230
|
+
queryTwo = QueryMock.new '{fieldTwo}', 1
|
231
|
+
|
232
|
+
report = Report.new
|
233
|
+
report.add_query queryOne, {}
|
234
|
+
report.add_query queryTwo, {}
|
235
|
+
report.finish!
|
236
|
+
|
237
|
+
expect(report.traces_to_report.length).to be(2)
|
238
|
+
end
|
239
|
+
end
|
183
240
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: optics-agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 'Tom Coleman '
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 1.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 1.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: google-protobuf
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +38,48 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 3.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-http-persistent
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.0.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: hitimes
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.2.4
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.2.4
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
84
|
name: rake
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,9 +119,10 @@ files:
|
|
77
119
|
- lib/apollo/optics/proto/reports_pb.rb
|
78
120
|
- lib/optics-agent.rb
|
79
121
|
- lib/optics-agent/agent.rb
|
80
|
-
- lib/optics-agent/graphql-middleware.rb
|
81
122
|
- lib/optics-agent/instrumentation/introspection-query.graphql
|
82
123
|
- lib/optics-agent/instrumentation/query-schema.rb
|
124
|
+
- lib/optics-agent/instrumenters/field.rb
|
125
|
+
- lib/optics-agent/instrumenters/patch-graphql-schema.rb
|
83
126
|
- lib/optics-agent/normalization/latency.rb
|
84
127
|
- lib/optics-agent/normalization/query.rb
|
85
128
|
- lib/optics-agent/rack-middleware.rb
|
@@ -91,7 +134,7 @@ files:
|
|
91
134
|
- lib/optics-agent/reporting/schema.rb
|
92
135
|
- lib/optics-agent/reporting/schema_job.rb
|
93
136
|
- spec/benchmark/benchmark.rb
|
94
|
-
- spec/
|
137
|
+
- spec/field_instrumenter_spec.rb
|
95
138
|
- spec/latency_spec.rb
|
96
139
|
- spec/query-normalization_spec.rb
|
97
140
|
- spec/query_trace_spec.rb
|
@@ -126,7 +169,7 @@ specification_version: 4
|
|
126
169
|
summary: An Agent for Apollo Optics
|
127
170
|
test_files:
|
128
171
|
- spec/benchmark/benchmark.rb
|
129
|
-
- spec/
|
172
|
+
- spec/field_instrumenter_spec.rb
|
130
173
|
- spec/latency_spec.rb
|
131
174
|
- spec/query-normalization_spec.rb
|
132
175
|
- spec/query_trace_spec.rb
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module OpticsAgent
|
2
|
-
class GraphqlMiddleware
|
3
|
-
def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
|
4
|
-
# This happens when an introspection query occurs (reporting schema)
|
5
|
-
# However, we could also use it to tell people if they've set things up wrong.
|
6
|
-
return next_middleware.call unless query_context[:optics_agent]
|
7
|
-
|
8
|
-
start_time = Time.now
|
9
|
-
result = next_middleware.call
|
10
|
-
end_time = Time.now
|
11
|
-
|
12
|
-
query = query_context[:optics_agent].query
|
13
|
-
query.report_field(parent_type.to_s, field_definition.name, start_time, end_time)
|
14
|
-
|
15
|
-
result
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|