gql_metrics_tracer 0.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/gql_metrics_tracer.rb +192 -0
  3. metadata +87 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b1b248815920370432ba4165e3b544f5baa430d8805be210af0c590dde54db2
4
+ data.tar.gz: ed0898b557cd27f7a1286ede425b410e590b09aaa96c5526338a54d8e43cffd4
5
+ SHA512:
6
+ metadata.gz: a787ae6298818afc34946b44285474e9d590fa987d169f47aa17d5f61cf11ee8ed25d17302d2031688a8d3b38e21ba2affb23dc2c6c4db83a2f14498d4bf30fa
7
+ data.tar.gz: 9950c53fe9f6681e39b9a2f4e94897100ded43f209fd308e96789e08399b40c1e9fc0187228bf6150e6f437e58ef43df20917d30ce65f86ac16091e3057feef5
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "typhoeus"
4
+ require "json"
5
+
6
+ class GQLMetricsTracer
7
+ def self.use(schema_definition, _options = {})
8
+ tracer = new(schema_definition)
9
+ schema_definition.instrument(:query, tracer)
10
+ schema_definition.tracer(tracer)
11
+ end
12
+
13
+ def initialize(schema_definition)
14
+ @schema_id = nil
15
+ @schema_definition = schema_definition
16
+
17
+ publish_schema!
18
+ end
19
+
20
+ def before_query(query)
21
+ query.context["gqlmetrics-trace"] = {
22
+ "starts-ns" => precise_time,
23
+ "starts-timestamp" => Time.current,
24
+ "traces" => [],
25
+ }
26
+ end
27
+
28
+ def after_query(query)
29
+ ended_at_ns = precise_time
30
+ ended_timestamp = Time.current
31
+ started_at = query.context.dig("gqlmetrics-trace", "starts-timestamp")
32
+ started_at_ns = query.context.dig("gqlmetrics-trace", "starts-ns")
33
+ traces = query.context.dig("gqlmetrics-trace", "traces")
34
+
35
+ send_trace!(
36
+ query: query.document.to_query_string,
37
+ duration_ns: ended_at_ns - started_at_ns,
38
+ started_at: started_at,
39
+ ended_at: ended_timestamp,
40
+ traces: traces,
41
+ )
42
+ end
43
+
44
+ def trace(key, data, &block)
45
+ if key == "execute_field"
46
+ field = data[:context].present? ? data[:context].field : data[:field]
47
+ path = data[:context].present? ? data[:context].path : data[:path]
48
+ parent_type = data[:context].present? ? data[:context].parent_type : data[:owner].graphql_name
49
+ context = data[:context] || data[:object].context
50
+
51
+ trace_field!(field: field, path: path, parent_type: parent_type, context: context, &block)
52
+ else
53
+ yield
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :schema_id, :schema_definition
60
+
61
+ def send_trace!(query:, duration_ns:, started_at:, ended_at:, traces:)
62
+ return unless graph_token_configured?
63
+ return if @schema_id.nil?
64
+
65
+ Thread.new do
66
+ response = Typhoeus.post(
67
+ "#{gqlmetrics_http_host}/api/traces",
68
+ headers: {
69
+ 'Content-Type': "application/json",
70
+ 'Authorization': "Bearer #{ENV['GQLMETRICS_GRAPH_ID']}",
71
+ },
72
+ timeout: 30,
73
+ body: JSON.dump(
74
+ {
75
+ schema_id: @schema_id,
76
+ query: query,
77
+ started_at_ms: timestamp_to_ms(started_at),
78
+ ended_at_ms: timestamp_to_ms(ended_at),
79
+ duration_ns: duration_ns,
80
+ traces: traces,
81
+ },
82
+ ),
83
+ )
84
+
85
+ trace_id = JSON.parse(response.body)["id"]
86
+
87
+ if trace_id.present?
88
+ Rails.logger.info "[GQLMetricsTracer] Sent trace #{trace_id}"
89
+ else
90
+ Rails.logger.warn "[GQLMetricsTracer] Failed to send the trace..."
91
+ end
92
+ end
93
+ end
94
+
95
+ def trace_field!(field:, path:, parent_type:, context:)
96
+ query_starts_at = context["gqlmetrics-trace"]["starts-ns"]
97
+
98
+ sql_queries = []
99
+ ActiveSupport::Notifications.subscribe "sql.active_record" do |event|
100
+ unless /(pg_)|(max_identifier_length)/.match?(event.payload[:sql])
101
+ sql_queries.push(
102
+ {
103
+ sql: event.payload[:sql],
104
+ start_time: event.time,
105
+ end_time: event.end,
106
+ duration_ns: event.duration * 1_000_000,
107
+ allocations_count: event.allocations,
108
+ },
109
+ )
110
+ end
111
+ end
112
+
113
+ starts_at = precise_time
114
+ yield
115
+ ensure
116
+ ends_at = precise_time
117
+ ActiveSupport::Notifications.unsubscribe("sql.active_record")
118
+
119
+ trace_node = {
120
+ "response_name" => field.name,
121
+ "original_field_name" => field.name,
122
+ "type" => field.type.to_s,
123
+ "parent_type" => parent_type.to_s,
124
+ "start_time" => starts_at - query_starts_at,
125
+ "end_time" => ends_at - query_starts_at,
126
+ "sql_queries" => sql_queries,
127
+ "path" => path,
128
+ }
129
+
130
+ context["gqlmetrics-trace"]["traces"] << trace_node
131
+ end
132
+
133
+ # Introspect the schema and upload to the ingress server
134
+ def publish_schema!
135
+ return unless graph_token_configured?
136
+
137
+ schema = schema_definition.execute(GraphQL::Introspection::INTROSPECTION_QUERY)
138
+
139
+ Thread.new do
140
+ payload = {
141
+ query: PUBLISH_SCHEMA_QUERY,
142
+ variables: {
143
+ schema: schema.to_json,
144
+ },
145
+ }
146
+
147
+ response = Typhoeus.post(
148
+ "#{gqlmetrics_http_host}/graphql",
149
+ headers: {
150
+ 'Content-Type': "application/json",
151
+ 'Authorization': "Bearer #{ENV['GQLMETRICS_GRAPH_ID']}",
152
+ },
153
+ timeout: 10,
154
+ body: payload.to_json,
155
+ )
156
+
157
+ json_response = JSON.parse(response.body)
158
+ schema_id = json_response.dig("data", "publishSchema", "id")
159
+
160
+ if schema_id.present?
161
+ @schema_id = schema_id
162
+ else
163
+ Rails.logger.warn "[GQLMetricsTracer] Failed to publish the latest schema..."
164
+ nil
165
+ end
166
+ end
167
+ end
168
+
169
+ def graph_token_configured?
170
+ ENV["GQLMETRICS_GRAPH_ID"].present?
171
+ end
172
+
173
+ def gqlmetrics_http_host
174
+ ENV.fetch("GQLMETRICS_HTTP_HOST", "http://localhost:5000")
175
+ end
176
+
177
+ def precise_time
178
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
179
+ end
180
+
181
+ def timestamp_to_ms(timestamp)
182
+ (timestamp.to_f * 1_000).to_i
183
+ end
184
+
185
+ PUBLISH_SCHEMA_QUERY = <<~PUBLISH_SCHEMA_QUERY
186
+ mutation PublishSchema($schema: String!) {
187
+ publishSchema(schema: $schema) {
188
+ id
189
+ }
190
+ }
191
+ PUBLISH_SCHEMA_QUERY
192
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gql_metrics_tracer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Andreasson
8
+ - André Ligné
9
+ - Ante Wall
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2020-06-16 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: typhoeus
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: json
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '2.0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rspec
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.9'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '3.9'
57
+ description: Explore your GraphQL metrics
58
+ email: dan@saits.se
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/gql_metrics_tracer.rb
64
+ homepage: https://rubygems.org/gems/tracer
65
+ licenses:
66
+ - MIT
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.1.2
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Explore your GraphQL metrics
87
+ test_files: []