optics-agent 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](https://badge.fury.io/rb/optics-agent) [](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
|