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,9 @@
1
+ module OpticsAgent
2
+ module Instrumentation
3
+ INTROSPECTION_QUERY ||= IO.read("#{File.dirname(__FILE__)}/introspection-query.graphql")
4
+
5
+ def introspect_schema(schema)
6
+ schema.execute(INTROSPECTION_QUERY)['data']['__schema']
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ module OpticsAgent
2
+ module Normalization
3
+ def empty_latency_count
4
+ Array.new(256) { 0 }
5
+ end
6
+
7
+ # see https://github.com/apollostack/optics-agent/blob/master/docs/histograms.md
8
+ def latency_bucket(micros)
9
+ bucket = Math.log(micros) / Math.log(1.1)
10
+
11
+ [255, [0, bucket].max].min.ceil.to_i
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,156 @@
1
+ require 'graphql'
2
+
3
+ module OpticsAgent
4
+ module Normalization
5
+ module Query
6
+ include GraphQL::Language
7
+
8
+ # query is a query string
9
+ def normalize(query_string)
10
+ document = GraphQL.parse(query_string)
11
+
12
+ # to store results
13
+ output = ''
14
+ used_fragment_names = []
15
+ current = {}
16
+ visitor = Visitor.new(document)
17
+
18
+ stack = []
19
+ visitor.enter << -> (_, _) do
20
+ stack.unshift(
21
+ arguments: [],
22
+ directives: [],
23
+ selections: []
24
+ )
25
+ end
26
+ visitor.leave << -> (_, _) { current = stack.shift }
27
+
28
+ visitor[Nodes::Argument].leave << -> (node, parent) do
29
+ stack[0][:arguments] << "#{node.name}:#{genericize_type(node.value)}"
30
+ end
31
+
32
+ visitor[Nodes::Directive].leave << -> (node, parent) do
33
+ id = "@#{node.name}"
34
+ arguments = current[:arguments]
35
+ unless arguments.empty?
36
+ id << "(#{arguments.sort.join(', ')})"
37
+ end
38
+ stack[0][:directives] << id
39
+ end
40
+
41
+ visitor[Nodes::Field].leave << -> (node, parent) do
42
+ id = node.name
43
+ arguments = current[:arguments]
44
+ unless arguments.empty?
45
+ id << "(#{arguments.sort.join(', ')})"
46
+ end
47
+ directives = current[:directives]
48
+ unless directives.empty?
49
+ id << " #{directives.sort.join(' ')}"
50
+ end
51
+ selections = current[:selections]
52
+ unless selections.empty?
53
+ id << ' ' + block(selections)
54
+ end
55
+
56
+ stack[0][:selections] << id
57
+ end
58
+
59
+ visitor[Nodes::InlineFragment].leave << -> (node, parent) do
60
+ selections = current[:selections]
61
+ stack[0][:selections] << "... on #{node.type} #{block(selections)}"
62
+ end
63
+
64
+ visitor[Nodes::FragmentSpread].leave << -> (node, parent) do
65
+ used_fragment_names << node.name
66
+ stack[0][:selections] << "...#{node.name}"
67
+ end
68
+
69
+ visitor[Nodes::OperationDefinition].leave << -> (node, parent) do
70
+ # no need to walk this, I don't think anything else can have vars
71
+ vars = nil
72
+ unless node.variables.empty?
73
+ variable_strs = node.variables.sort_by(&:name).map do |variable|
74
+ "$#{variable.name}:#{format_argument_type(variable.type)}"
75
+ end
76
+ vars = "(#{variable_strs.join(',')})"
77
+ end
78
+
79
+ query_content = block(current[:selections])
80
+ if (node.name || vars || node.operation_type != 'query')
81
+ parts = [node.operation_type]
82
+ parts << "#{node.name}#{vars}" if (node.name || vars)
83
+ parts << query_content
84
+ output << parts.join(' ')
85
+ else
86
+ output << query_content
87
+ end
88
+ end
89
+
90
+ visitor[Nodes::FragmentDefinition].leave << -> (node, parent) do
91
+ selections = current[:selections]
92
+ if (used_fragment_names.include?(node.name))
93
+ output << " fragment #{node.name} on #{node.type} " \
94
+ + block(selections)
95
+ end
96
+ end
97
+
98
+ visitor.visit
99
+ output
100
+ end
101
+
102
+ private
103
+
104
+ # See https://github.com/apollostack/optics-agent/blob/master/docs/signatures.md
105
+ def sort_value(a)
106
+ type_value = if a[0..3] == '... '
107
+ 2
108
+ elsif a[0..2] == '...'
109
+ 1
110
+ else
111
+ 0
112
+ end
113
+
114
+ [type_value, a]
115
+ end
116
+
117
+ def block(array)
118
+ if array.empty?
119
+ '{}'
120
+ else
121
+ "{#{array.sort_by{ |x| sort_value(x) }.join(' ')}}"
122
+ end
123
+ end
124
+
125
+ def format_argument_type(type)
126
+ case type
127
+ when Nodes::ListType
128
+ "[#{format_argument_type(type.of_type)}]"
129
+ when Nodes::NonNullType
130
+ "#{format_argument_type(type.of_type)}!"
131
+ else
132
+ type.name
133
+ end
134
+ end
135
+
136
+ def genericize_type(value)
137
+ case value
138
+ when Nodes::VariableIdentifier
139
+ "$#{value.name}"
140
+ when String
141
+ "\"\""
142
+ when Numeric
143
+ "0"
144
+ when TrueClass, FalseClass
145
+ value.to_s
146
+ when Array
147
+ "[]"
148
+ when Nodes::Enum
149
+ value.name
150
+ when Nodes::InputObject
151
+ "{}"
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,43 @@
1
+ require 'optics-agent/agent'
2
+ require 'optics-agent/reporting/query'
3
+
4
+ module OpticsAgent
5
+ class RackMiddleware
6
+ def initialize(app, options={})
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ begin
12
+ start_time = Time.now
13
+
14
+ # XXX: figure out a way to pass this in here
15
+ agent = OpticsAgent::Agent.instance
16
+ query = OpticsAgent::Reporting::Query.new
17
+
18
+ # Attach so resolver middleware can access
19
+ env[:optics_agent] = {
20
+ agent: agent,
21
+ query: query
22
+ }
23
+ env[:optics_agent].define_singleton_method(:with_document) do |document|
24
+ self[:query].document = document
25
+ self
26
+ end
27
+
28
+ result = @app.call(env)
29
+
30
+ # XXX: this approach means if the user forgets to call with_document
31
+ # we just never log queries. Can we detect if the request is a graphql one?
32
+ if (query.document)
33
+ agent.add_query(query, env, start_time, Time.now)
34
+ end
35
+
36
+ result
37
+ rescue Exception => e
38
+ puts "Rack Middleware Error: #{e}"
39
+ puts e.backtrace
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ require 'apollo/optics/proto/reports_pb'
2
+
3
+ module OpticsAgent::Reporting
4
+ def generate_report_header
5
+ # XXX: fill out
6
+ Apollo::Optics::Proto::ReportHeader.new({
7
+ agent_version: '0'
8
+ })
9
+ end
10
+
11
+ def generate_timestamp(time)
12
+ Apollo::Optics::Proto::Timestamp.new({
13
+ seconds: time.to_i,
14
+ nanos: time.to_i % 1 * 1e9
15
+ });
16
+ end
17
+
18
+ # XXX: implement
19
+ def client_info(rack_env)
20
+ {
21
+ client_name: 'none',
22
+ client_version: 'none',
23
+ client_address: '::1'
24
+ }
25
+ end
26
+
27
+ def add_latency(counts, start_time, end_time)
28
+ micros = (end_time - start_time) * 1e6
29
+ bucket = latency_bucket(micros)
30
+ counts[bucket] += 1
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ require 'apollo/optics/proto/reports_pb'
2
+ require 'optics-agent/reporting/helpers'
3
+ require 'optics-agent/reporting/send-message'
4
+
5
+ module OpticsAgent::Reporting
6
+ # A trace is just a different view of a single query report, with full
7
+ # information about start and end times
8
+ class QueryTrace
9
+ include OpticsAgent::Reporting
10
+ include Apollo::Optics::Proto
11
+
12
+ attr_accessor :report
13
+
14
+ def initialize(query, rack_env, start_time, end_time)
15
+ trace = Trace.new({
16
+ start_time: generate_timestamp(start_time),
17
+ signature: query.signature
18
+ })
19
+
20
+ # XXX: report trace details (not totally clear yet from the JS agent what should be here)
21
+ trace.details = Trace::Details.new({})
22
+
23
+ info = client_info(rack_env)
24
+ trace.client_name = info[:client_name]
25
+ trace.client_version = info[:client_version]
26
+ trace.client_address = info[:client_address]
27
+ trace.http = Trace::HTTPInfo.new({
28
+ host: "localhost:8080",
29
+ path: "/graphql"
30
+ })
31
+
32
+ nodes = []
33
+ query.each_report do |type_name, field_name, field_start_time, field_end_time|
34
+ nodes << Trace::Node.new({
35
+ field_name: "#{type_name}.#{field_name}",
36
+ start_time: ((field_start_time - start_time) * 1e9).to_i,
37
+ end_time: ((field_end_time - start_time) * 1e9).to_i
38
+ })
39
+ end
40
+ trace.execute = Trace::Node.new({
41
+ child: nodes
42
+ })
43
+
44
+ @report = TracesReport.new({
45
+ header: generate_report_header,
46
+ trace: [trace]
47
+ })
48
+ end
49
+
50
+ def send
51
+ send_message('/api/ss/traces', @report)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ require 'apollo/optics/proto/reports_pb'
2
+ require 'optics-agent/reporting/helpers'
3
+ require 'optics-agent/normalization/latency'
4
+
5
+ module OpticsAgent::Reporting
6
+ # This is a convenience class that enables us to fairly blindly
7
+ # pass in data as we resolve a query
8
+ class Query
9
+ include Apollo::Optics::Proto
10
+ include OpticsAgent::Reporting
11
+ include OpticsAgent::Normalization
12
+
13
+ attr_accessor :document
14
+
15
+ def initialize
16
+ @reports = []
17
+
18
+ @document = nil
19
+ end
20
+
21
+ def signature
22
+ # Note this isn't actually possible but would be a sensible spot to throw
23
+ # if the user forgets to call `.with_document`
24
+ unless @document
25
+ throw "You must call .with_document on the optics context"
26
+ end
27
+
28
+ # TODO: query normalization here
29
+ return document["query"].to_s
30
+ end
31
+
32
+ # we do nothing when reporting to minimize impact
33
+ def report_field(type_name, field_name, start_time, end_time)
34
+ @reports << [type_name, field_name, start_time, end_time]
35
+ end
36
+
37
+ def each_report
38
+ @reports.each do |report|
39
+ yield *report
40
+ end
41
+ end
42
+
43
+ # add our results to an existing StatsPerSignature
44
+ def add_to_stats(stats_per_signature)
45
+ each_report do |type_name, field_name, start_time, end_time|
46
+ type_stat = stats_per_signature.per_type.find { |ts| ts.name == type_name }
47
+ unless type_stat
48
+ type_stat = TypeStat.new({ name: type_name })
49
+ stats_per_signature.per_type << type_stat
50
+ end
51
+
52
+ field_stat = type_stat.field.find { |fs| fs.name == field_name }
53
+ unless field_stat
54
+ field_stat = FieldStat.new({
55
+ name: field_name,
56
+ latency_count: empty_latency_count
57
+ })
58
+ type_stat.field << field_stat
59
+ end
60
+
61
+ add_latency(field_stat.latency_count, start_time, end_time)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,102 @@
1
+ require 'apollo/optics/proto/reports_pb'
2
+ require 'optics-agent/reporting/send-message'
3
+ require 'optics-agent/reporting/helpers'
4
+ require 'optics-agent/normalization/latency'
5
+
6
+ module OpticsAgent::Reporting
7
+ # This class represents a complete report that we send to the optics server
8
+ # It pretty closely wraps the StatsReport protobuf message with a few
9
+ # convenience methods
10
+ class Report
11
+ include Apollo::Optics::Proto
12
+ include OpticsAgent::Reporting
13
+ include OpticsAgent::Normalization
14
+
15
+ attr_accessor :report
16
+
17
+ def initialize
18
+ # internal report that we encapsulate
19
+ @report = StatsReport.new({
20
+ header: ReportHeader.new({
21
+ agent_version: '1'
22
+ }),
23
+ start_time: Timestamp.new({
24
+ # XXX pass this in?
25
+ seconds: Time.now.to_i,
26
+ nanos: 0
27
+ })
28
+ })
29
+ end
30
+
31
+ def finish!
32
+ @report.end_time ||= Timestamp.new({
33
+ # XXX pass this in?
34
+ seconds: Time.now.to_i,
35
+ nanos: 0
36
+ })
37
+ end
38
+
39
+ def send
40
+ self.finish!
41
+ send_message('/api/ss/stats', @report)
42
+ end
43
+
44
+ # XXX: record timing / client
45
+ def add_query(query, rack_env, start_time, end_time)
46
+ @report.per_signature[query.signature] ||= StatsPerSignature.new
47
+ signature_stats = @report.per_signature[query.signature]
48
+
49
+ add_client_stats(signature_stats, rack_env, start_time, end_time)
50
+ query.add_to_stats(signature_stats)
51
+ end
52
+
53
+ def add_client_stats(signature_stats, rack_env, start_time, end_time)
54
+ info = client_info(rack_env)
55
+ signature_stats.per_client_name[info[:client_name]] ||= StatsPerClientName.new({
56
+ latency_count: empty_latency_count,
57
+ error_count: empty_latency_count
58
+ })
59
+ client_stats = signature_stats.per_client_name[info[:client_name]]
60
+
61
+ # XXX: handle errors
62
+ add_latency(client_stats.latency_count, start_time, end_time)
63
+
64
+ client_stats.count_per_version[info[:client_version]] ||= 0
65
+ client_stats.count_per_version[info[:client_version]] += 1
66
+ end
67
+
68
+ # take a graphql schema and add returnTypes to all the fields on our report
69
+ def decorate_from_schema(schema)
70
+ each_field do |type_stat, field_stat|
71
+ # short circuit for special fields
72
+ field_stat.returnType = type_stat.name if field_stat.name == '__typename'
73
+
74
+ if type_stat.name == 'Query'
75
+ field_stat.returnType = '__Type' if field_stat.name == '__type'
76
+ field_stat.returnType = '__Schema' if field_stat.name == '__schema'
77
+ end
78
+
79
+ if field_stat.returnType.empty?
80
+ type = schema.types[type_stat.name]
81
+ throw "Type #{type_stat.name} not found!" unless type
82
+
83
+ field = type.fields[field_stat.name]
84
+ throw "Field #{type_stat.name}.#{field_stat.name} not found!" unless field
85
+
86
+ field_stat.returnType = field.type.to_s
87
+ end
88
+ end
89
+ end
90
+
91
+ # do something once per field we've collected
92
+ def each_field
93
+ @report.per_signature.values.each do |sps|
94
+ sps.per_type.each do |type|
95
+ type.field.each do |field|
96
+ yield type, field
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end