optics-agent 0.1.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.
@@ -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