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,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