optics-agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ require 'sucker_punch'
2
+ require 'optics-agent/reporting/report'
3
+
4
+ module OpticsAgent::Reporting
5
+ class ReportJob
6
+ include SuckerPunch::Job
7
+
8
+ def perform(agent)
9
+ report = OpticsAgent::Reporting::Report.new
10
+ agent.clear_query_queue.each do |item|
11
+ report.add_query(*item)
12
+
13
+ # XXX: don't send *every* trace
14
+ query_trace = QueryTrace.new(*item)
15
+ query_trace.send
16
+ end
17
+
18
+ report.decorate_from_schema(agent.schema)
19
+ report.send
20
+
21
+ self.class.perform_in(60, agent)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+ require 'graphql'
3
+
4
+ require 'apollo/optics/proto/reports_pb'
5
+ require 'optics-agent/reporting/helpers'
6
+ require 'optics-agent/reporting/send-message'
7
+ require 'optics-agent/instrumentation/query-schema'
8
+
9
+ module OpticsAgent::Reporting
10
+ # A report for a whole schema
11
+ class Schema
12
+ include Apollo::Optics::Proto
13
+ include OpticsAgent::Instrumentation
14
+ include OpticsAgent::Reporting
15
+
16
+ attr_accessor :message
17
+
18
+ def initialize(schema)
19
+ @message = SchemaReport.new({
20
+ header: generate_report_header(),
21
+ introspection_result: JSON.generate(introspect_schema(schema)),
22
+ type: get_types(schema)
23
+ })
24
+ end
25
+
26
+ # construct an array of Type (protobuf) objects
27
+ def get_types(schema)
28
+ types = []
29
+
30
+ schema.types.keys.each do |type_name|
31
+ next if type_name =~ /^__/
32
+ type = schema.types[type_name]
33
+ next unless type.is_a? GraphQL::ObjectType
34
+
35
+ fields = type.fields.values.map do |field|
36
+ Field.new({
37
+ name: field.name,
38
+ # XXX: does this actually work for all types?
39
+ returnType: field.type.to_s
40
+ })
41
+ end
42
+
43
+ types << Type.new({
44
+ name: type_name,
45
+ field: fields
46
+ })
47
+ end
48
+
49
+ types
50
+ end
51
+
52
+ def send
53
+ send_message('/api/ss/schema', @message)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ require 'sucker_punch'
2
+ require 'optics-agent/reporting/schema'
3
+
4
+ module OpticsAgent::Reporting
5
+ class SchemaJob
6
+ include SuckerPunch::Job
7
+
8
+ def perform(agent)
9
+ puts 'performing schema job'
10
+ schema = OpticsAgent::Reporting::Schema.new agent.schema
11
+ schema.send
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ require 'net/http'
2
+
3
+ module OpticsAgent
4
+ module Reporting
5
+ OPTICS_URL = 'https://optics-report.apollodata.com'
6
+ def send_message(path, message)
7
+
8
+ req = Net::HTTP::Post.new(path)
9
+ req['x-api-key'] = ENV['OPTICS_API_KEY']
10
+ req['user-agent'] = "optics-agent-rb"
11
+
12
+ req.body = message.class.encode(message)
13
+ puts message.class.encode_json(message)
14
+
15
+ uri = URI.parse(OPTICS_URL)
16
+ http = Net::HTTP.new(uri.host, uri.port)
17
+ http.use_ssl = true
18
+ res = http.request(req)
19
+ p res
20
+ p res.body
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ require 'optics-agent/graphql-middleware'
2
+ require 'graphql'
3
+
4
+ include OpticsAgent
5
+
6
+ describe GraphqlMiddleware do
7
+ it 'collects the correct query stats' do
8
+ person_type = GraphQL::ObjectType.define do
9
+ name "Person"
10
+ field :firstName do
11
+ type types.String
12
+ resolve -> (obj, args, ctx) { sleep(0.100); return 'Tom' }
13
+ end
14
+ field :lastName do
15
+ type types.String
16
+ resolve -> (obj, args, ctx) { sleep(0.100); return 'Coleman' }
17
+ end
18
+ end
19
+ query_type = GraphQL::ObjectType.define do
20
+ name 'Query'
21
+ field :person do
22
+ type person_type
23
+ resolve -> (obj, args, ctx) { sleep(0.050); return {} }
24
+ end
25
+ end
26
+
27
+ schema = GraphQL::Schema.define do
28
+ query query_type
29
+ end
30
+
31
+ schema.middleware << GraphqlMiddleware.new
32
+
33
+ query = spy("query")
34
+ schema.execute('{ person { firstName lastName } }', {
35
+ context: { optics_agent: { query: query } }
36
+ })
37
+
38
+ expect(query).to have_received(:report_field).exactly(3).times
39
+ expect(query).to have_received(:report_field)
40
+ .with('Query', 'person', be_instance_of(Time), be_instance_of(Time))
41
+ expect(query).to have_received(:report_field)
42
+ .with('Person', 'firstName', be_instance_of(Time), be_instance_of(Time))
43
+ expect(query).to have_received(:report_field)
44
+ .with('Person', 'lastName', be_instance_of(Time), be_instance_of(Time))
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ require 'optics-agent/normalization/latency'
2
+ include OpticsAgent::Normalization
3
+
4
+
5
+ describe 'latency helpers' do
6
+ describe 'empty_latency_count' do
7
+ it 'returns 256 zeros' do
8
+ zeros = empty_latency_count
9
+ expect(zeros.length).to equal(256)
10
+ zeros.each { |z| expect(z).to eq(0) }
11
+ end
12
+ end
13
+
14
+ describe 'latency_bucket' do
15
+ it 'returns the right values' do
16
+ tests = [
17
+ [0.1, 0],
18
+ [0.9, 0],
19
+ [1, 0],
20
+ [1.1, 1],
21
+ [1.21, 2],
22
+ [100, 49],
23
+ [1000, 73],
24
+ [1000 * 1000, 145],
25
+ [1.1**254, 255],
26
+ [1000 * 1000 * 1000 * 1000, 255]
27
+ ]
28
+
29
+ tests.each do |test|
30
+ expect(latency_bucket test.first).to eq(test.last)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,105 @@
1
+ require 'optics-agent/normalization/query'
2
+
3
+ TEST_QUERIES = [
4
+ [
5
+ 'basic test',
6
+ '{
7
+ user {
8
+ name
9
+ }
10
+ }',
11
+ '{user {name}}',
12
+ ],
13
+ [
14
+ 'basic test with query',
15
+ 'query {
16
+ user {
17
+ name
18
+ }
19
+ }',
20
+ '{user {name}}',
21
+ ],
22
+ [
23
+ 'basic with operation name',
24
+ 'query OpName {
25
+ user {
26
+ name
27
+ }
28
+ }',
29
+ 'query OpName {user {name}}',
30
+ ],
31
+ [
32
+ 'with various inline types',
33
+ 'query OpName {
34
+ user {
35
+ name(apple: [[10]], cat: ENUM_VALUE, bag: {input: "value"})
36
+ }
37
+ }',
38
+ 'query OpName {user {name(apple:[], bag:{}, cat:ENUM_VALUE)}}',
39
+ ],
40
+ [
41
+ 'with various argument types',
42
+ 'query OpName($c: Int!, $a: [[Boolean!]!], $b: EnumType) {
43
+ user {
44
+ name(apple: $a, cat: $c, bag: $b)
45
+ }
46
+ }',
47
+ 'query OpName($a:[[Boolean!]!],$b:EnumType,$c:Int!) {user {name(apple:$a, bag:$b, cat:$c)}}',
48
+ ],
49
+ [
50
+ 'fragment',
51
+ '{
52
+ user {
53
+ name
54
+ ...Bar
55
+ }
56
+ }
57
+ fragment Bar on User {
58
+ asd
59
+ }
60
+ fragment Baz on User {
61
+ jkl
62
+ }',
63
+ '{user {name ...Bar}} fragment Bar on User {asd}',
64
+ ],
65
+ [
66
+ 'full test',
67
+ 'query Foo ($b: Int, $a: Boolean){
68
+ user(name: "hello", age: 5) {
69
+ ... Bar
70
+ ... on User {
71
+ hello
72
+ bee
73
+ }
74
+ tz
75
+ aliased: name
76
+ }
77
+ }
78
+ fragment Baz on User {
79
+ asd
80
+ }
81
+ fragment Bar on User {
82
+ age @skip(if: $a)
83
+ ...Nested
84
+ }
85
+ fragment Nested on User {
86
+ blah
87
+ }',
88
+ 'query Foo($a:Boolean,$b:Int) {user(age:0, name:"") {name tz ...Bar ... on User {bee hello}}}' +
89
+ ' fragment Bar on User {age @skip(if:$a) ...Nested} fragment Nested on User {blah}',
90
+ ],
91
+ ]
92
+
93
+
94
+ describe OpticsAgent::Normalization::Query do
95
+ include OpticsAgent::Normalization::Query
96
+
97
+ TEST_QUERIES.each do |spec|
98
+ test_name, query, expected_signature = spec
99
+
100
+ it test_name do
101
+ signature = normalize(query)
102
+ expect(signature).to eq(expected_signature)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,43 @@
1
+ require 'optics-agent/reporting/query-trace'
2
+ require 'optics-agent/reporting/query'
3
+ require 'apollo/optics/proto/reports_pb'
4
+ require 'graphql'
5
+
6
+ include Apollo::Optics::Proto
7
+ include OpticsAgent::Reporting
8
+
9
+ class DocumentMock
10
+ def initialize(key)
11
+ @key = key
12
+ end
13
+
14
+ def [](name) # used for [:query]
15
+ @key
16
+ end
17
+ end
18
+
19
+
20
+ describe QueryTrace do
21
+ it "can represent a simple query" do
22
+ query = Query.new
23
+ query.report_field 'Person', 'firstName', 1, 1.1
24
+ query.report_field 'Person', 'lastName', 1, 1.1
25
+ query.report_field 'Query', 'person', 1, 1.22
26
+ query.document = DocumentMock.new('key')
27
+
28
+ trace = QueryTrace.new(query, {}, 1, 1.25)
29
+
30
+ expect(trace.report).to be_instance_of(TracesReport)
31
+ expect(trace.report.trace.length).to eq(1)
32
+
33
+ trace_obj = trace.report.trace.first
34
+ nodes = trace_obj.execute.child
35
+ expect(nodes.length).to eq(3)
36
+ expect(nodes.map(&:field_name)).to \
37
+ match_array(['Query.person', 'Person.firstName', 'Person.lastName'])
38
+
39
+ firstName_node = nodes.find { |n| n.field_name == 'Person.firstName' }
40
+ expect(firstName_node.start_time).to eq(0)
41
+ expect(firstName_node.end_time).to eq(0.1 * 1e9)
42
+ end
43
+ end
@@ -0,0 +1,194 @@
1
+ require 'optics-agent/reporting/report'
2
+ require 'optics-agent/reporting/query'
3
+ require 'apollo/optics/proto/reports_pb'
4
+ require 'graphql'
5
+
6
+ include OpticsAgent::Reporting
7
+ include Apollo::Optics::Proto
8
+
9
+ class DocumentMock
10
+ def initialize(key)
11
+ @key = key
12
+ end
13
+
14
+ def [](name) # used for [:query]
15
+ @key
16
+ end
17
+ end
18
+
19
+
20
+ describe Report do
21
+ it "can represent a simple query" do
22
+ query = Query.new
23
+ query.report_field 'Person', 'firstName', 1, 1.1
24
+ query.report_field 'Person', 'lastName', 1, 1.1
25
+ query.report_field 'Query', 'person', 1, 1.22
26
+ query.document = DocumentMock.new('key')
27
+
28
+ report = Report.new
29
+ report.add_query query, {}, 1, 1.25
30
+ report.finish!
31
+
32
+ expect(report.report).to be_an_instance_of(StatsReport)
33
+ stats_report = report.report
34
+ expect(stats_report.per_signature.keys).to match_array(['key'])
35
+
36
+ signature_stats = stats_report.per_signature.values.first
37
+ expect(signature_stats.per_type.length).to equal(2)
38
+ expect(signature_stats.per_type.map &:name).to match_array(['Person', 'Query'])
39
+
40
+ person_stats = signature_stats.per_type.find { |s| s.name === 'Person' }
41
+ expect(person_stats.field.length).to equal(2)
42
+ expect(person_stats.field.map &:name).to match_array(['firstName', 'lastName'])
43
+
44
+ firstName_stats = person_stats.field.find { |s| s.name === 'firstName' }
45
+ expect(firstName_stats.latency_count.length).to eq(256)
46
+ expect(firstName_stats.latency_count.reduce(&:+)).to eq(1)
47
+ expect(firstName_stats.latency_count[121]).to eq(1)
48
+ end
49
+
50
+ it "can aggregate the results of multiple queries with the same shape" do
51
+ queryOne = Query.new
52
+ queryOne.report_field 'Person', 'firstName', 1, 1.1
53
+ queryOne.report_field 'Person', 'lastName', 1, 1.1
54
+ queryOne.report_field 'Query', 'person', 1, 1.22
55
+ queryOne.document = DocumentMock.new('key')
56
+
57
+ queryTwo = Query.new
58
+ queryTwo.report_field 'Person', 'firstName', 1, 1.05
59
+ queryTwo.report_field 'Person', 'lastName', 1, 1.05
60
+ queryTwo.report_field 'Query', 'person', 1, 1.2
61
+ queryTwo.document = DocumentMock.new('key')
62
+
63
+ report = Report.new
64
+ report.add_query queryOne, {}, 1, 1.1
65
+ report.add_query queryTwo, {}, 1, 1.1
66
+ report.finish!
67
+
68
+ expect(report.report).to be_an_instance_of(StatsReport)
69
+ stats_report = report.report
70
+ expect(stats_report.per_signature.keys).to match_array(['key'])
71
+
72
+ signature_stats = stats_report.per_signature.values.first
73
+ expect(signature_stats.per_type.length).to equal(2)
74
+ expect(signature_stats.per_type.map &:name).to match_array(['Person', 'Query'])
75
+
76
+ person_stats = signature_stats.per_type.find { |s| s.name === 'Person' }
77
+ expect(person_stats.field.length).to equal(2)
78
+ expect(person_stats.field.map &:name).to match_array(['firstName', 'lastName'])
79
+
80
+ firstName_stats = person_stats.field.find { |s| s.name === 'firstName' }
81
+ expect(firstName_stats.latency_count.reduce(&:+)).to eq(2)
82
+ expect(firstName_stats.latency_count[114]).to eq(1)
83
+ expect(firstName_stats.latency_count[121]).to eq(1)
84
+ end
85
+
86
+ it "can aggregate the results of multiple queries with a different shape" do
87
+ queryOne = Query.new
88
+ queryOne.report_field 'Person', 'firstName', 1, 1.1
89
+ queryOne.report_field 'Person', 'lastName', 1, 1.1
90
+ queryOne.report_field 'Query', 'person', 1, 1.22
91
+ queryOne.document = DocumentMock.new('keyOne')
92
+
93
+ queryTwo = Query.new
94
+ queryTwo.report_field 'Person', 'firstName', 1, 1.05
95
+ queryTwo.report_field 'Person', 'lastName', 1, 1.05
96
+ queryTwo.report_field 'Query', 'person', 1, 1.02
97
+ queryTwo.document = DocumentMock.new('keyTwo')
98
+
99
+ report = Report.new
100
+ report.add_query queryOne, {}, 1, 1.1
101
+ report.add_query queryTwo, {}, 1, 1.1
102
+ report.finish!
103
+
104
+ expect(report.report).to be_an_instance_of(StatsReport)
105
+ stats_report = report.report
106
+ expect(stats_report.per_signature.keys).to match_array(['keyOne', 'keyTwo'])
107
+
108
+ signature_stats = stats_report.per_signature['keyOne']
109
+ expect(signature_stats.per_type.length).to equal(2)
110
+ expect(signature_stats.per_type.map &:name).to match_array(['Person', 'Query'])
111
+
112
+ person_stats = signature_stats.per_type.find { |s| s.name === 'Person' }
113
+ expect(person_stats.field.length).to equal(2)
114
+ expect(person_stats.field.map &:name).to match_array(['firstName', 'lastName'])
115
+
116
+ firstName_stats = person_stats.field.find { |s| s.name === 'firstName' }
117
+ expect(firstName_stats.latency_count.reduce(&:+)).to eq(1)
118
+ expect(firstName_stats.latency_count[121]).to eq(1)
119
+ end
120
+
121
+
122
+ it "can decorate it's fields with resultTypes from a schema" do
123
+ query = Query.new
124
+ query.report_field 'Person', 'firstName', 1, 1.1
125
+ query.report_field 'Person', 'age', 1, 1.1
126
+ query.document = DocumentMock.new('key')
127
+
128
+ report = Report.new
129
+ report.add_query query, {}, 1, 1.25
130
+ report.finish!
131
+
132
+ person_type = GraphQL::ObjectType.define do
133
+ name 'Person'
134
+ field :firstName, types.String
135
+ field :age, !types.Int
136
+ end
137
+ query_type = GraphQL::ObjectType.define do
138
+ name 'Query'
139
+ field :person, person_type
140
+ end
141
+
142
+ schema = GraphQL::Schema.define do
143
+ query query_type
144
+ end
145
+
146
+ report.decorate_from_schema(schema)
147
+
148
+ stats_report = report.report
149
+ signature_stats = stats_report.per_signature.values.first
150
+ person_stats = signature_stats.per_type.find { |s| s.name === 'Person' }
151
+
152
+ firstName_stats = person_stats.field.find { |s| s.name === 'firstName' }
153
+ expect(firstName_stats.returnType).to eq('String')
154
+
155
+ age_stats = person_stats.field.find { |s| s.name === 'age' }
156
+ expect(age_stats.returnType).to eq('Int!')
157
+ end
158
+
159
+ it "can handle introspection fields" do
160
+ query = Query.new
161
+ query.report_field 'Query', '__schema', 1, 1.1
162
+ query.report_field 'Query', '__typename', 1, 1.1
163
+ query.report_field 'Query', '__type', 1, 1.1
164
+ query.document = DocumentMock.new('key')
165
+
166
+ report = Report.new
167
+ report.add_query query, {}, 1, 1.25
168
+ report.finish!
169
+
170
+ query_type = GraphQL::ObjectType.define do
171
+ name 'Query'
172
+ end
173
+
174
+ schema = GraphQL::Schema.define do
175
+ query query_type
176
+ end
177
+
178
+ report.decorate_from_schema(schema)
179
+
180
+ stats_report = report.report
181
+ signature_stats = stats_report.per_signature.values.first
182
+ query_stats = signature_stats.per_type.find { |s| s.name === 'Query' }
183
+
184
+ schema_stats = query_stats.field.find { |s| s.name === '__schema' }
185
+ expect(schema_stats.returnType).to eq('__Schema')
186
+
187
+ type_stats = query_stats.field.find { |s| s.name === '__type' }
188
+ expect(type_stats.returnType).to eq('__Type')
189
+
190
+ typename_stats = query_stats.field.find { |s| s.name === '__typename' }
191
+ expect(typename_stats.returnType).to eq('Query')
192
+ end
193
+
194
+ end