fluyenta-ruby 0.1.14
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module GrapeInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::Grape::API)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
# Subscribe to Grape's ActiveSupport notifications
|
|
14
|
+
install_notifications!
|
|
15
|
+
|
|
16
|
+
@installed = true
|
|
17
|
+
BrainzLab.debug_log('Grape instrumentation installed')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def installed?
|
|
21
|
+
@installed
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset!
|
|
25
|
+
@installed = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def install_notifications!
|
|
31
|
+
# Grape emits these notifications
|
|
32
|
+
ActiveSupport::Notifications.subscribe('endpoint_run.grape') do |*args|
|
|
33
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
34
|
+
record_endpoint(event)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ActiveSupport::Notifications.subscribe('endpoint_render.grape') do |*args|
|
|
38
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
39
|
+
record_render(event)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ActiveSupport::Notifications.subscribe('endpoint_run_filters.grape') do |*args|
|
|
43
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
44
|
+
record_filters(event)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Format validation
|
|
48
|
+
ActiveSupport::Notifications.subscribe('format_response.grape') do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
record_format(event)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
BrainzLab.debug_log("Grape notifications setup failed: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_endpoint(event)
|
|
57
|
+
payload = event.payload
|
|
58
|
+
endpoint = payload[:endpoint]
|
|
59
|
+
env = payload[:env] || {}
|
|
60
|
+
|
|
61
|
+
method = env['REQUEST_METHOD'] || 'GET'
|
|
62
|
+
path = endpoint&.options&.dig(:path)&.first || env['PATH_INFO'] || '/'
|
|
63
|
+
route_pattern = extract_route_pattern(endpoint)
|
|
64
|
+
duration_ms = event.duration.round(2)
|
|
65
|
+
|
|
66
|
+
status = env['api.endpoint']&.status || 200
|
|
67
|
+
level = status >= 400 ? :error : :info
|
|
68
|
+
|
|
69
|
+
# Add breadcrumb for Reflex
|
|
70
|
+
if BrainzLab.configuration.reflex_enabled
|
|
71
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
72
|
+
"Grape #{method} #{route_pattern}",
|
|
73
|
+
category: 'grape.endpoint',
|
|
74
|
+
level: level,
|
|
75
|
+
data: {
|
|
76
|
+
method: method,
|
|
77
|
+
path: path,
|
|
78
|
+
route: route_pattern,
|
|
79
|
+
status: status,
|
|
80
|
+
duration_ms: duration_ms
|
|
81
|
+
}.compact
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Record span for Pulse
|
|
86
|
+
record_span(
|
|
87
|
+
name: "Grape #{method} #{route_pattern}",
|
|
88
|
+
kind: 'grape',
|
|
89
|
+
started_at: event.time,
|
|
90
|
+
ended_at: event.end,
|
|
91
|
+
duration_ms: duration_ms,
|
|
92
|
+
data: {
|
|
93
|
+
method: method,
|
|
94
|
+
path: path,
|
|
95
|
+
route: route_pattern,
|
|
96
|
+
status: status
|
|
97
|
+
}.compact,
|
|
98
|
+
error: status >= 500
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Log to Recall
|
|
102
|
+
if BrainzLab.configuration.recall_enabled
|
|
103
|
+
BrainzLab::Recall.info(
|
|
104
|
+
"Grape #{method} #{path} -> #{status} (#{duration_ms}ms)",
|
|
105
|
+
method: method,
|
|
106
|
+
path: path,
|
|
107
|
+
route: route_pattern,
|
|
108
|
+
status: status,
|
|
109
|
+
duration_ms: duration_ms
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
BrainzLab.debug_log("Grape endpoint recording failed: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def record_render(event)
|
|
117
|
+
duration_ms = event.duration.round(2)
|
|
118
|
+
|
|
119
|
+
record_span(
|
|
120
|
+
name: 'Grape render',
|
|
121
|
+
kind: 'grape.render',
|
|
122
|
+
started_at: event.time,
|
|
123
|
+
ended_at: event.end,
|
|
124
|
+
duration_ms: duration_ms,
|
|
125
|
+
data: { phase: 'render' }
|
|
126
|
+
)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
BrainzLab.debug_log("Grape render recording failed: #{e.message}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def record_filters(event)
|
|
132
|
+
payload = event.payload
|
|
133
|
+
duration_ms = event.duration.round(2)
|
|
134
|
+
filter_type = payload[:type] || 'filter'
|
|
135
|
+
|
|
136
|
+
record_span(
|
|
137
|
+
name: "Grape #{filter_type} filters",
|
|
138
|
+
kind: 'grape.filter',
|
|
139
|
+
started_at: event.time,
|
|
140
|
+
ended_at: event.end,
|
|
141
|
+
duration_ms: duration_ms,
|
|
142
|
+
data: { type: filter_type }
|
|
143
|
+
)
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
BrainzLab.debug_log("Grape filters recording failed: #{e.message}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_format(event)
|
|
149
|
+
duration_ms = event.duration.round(2)
|
|
150
|
+
|
|
151
|
+
record_span(
|
|
152
|
+
name: 'Grape format response',
|
|
153
|
+
kind: 'grape.format',
|
|
154
|
+
started_at: event.time,
|
|
155
|
+
ended_at: event.end,
|
|
156
|
+
duration_ms: duration_ms,
|
|
157
|
+
data: { phase: 'format' }
|
|
158
|
+
)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
BrainzLab.debug_log("Grape format recording failed: #{e.message}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def record_span(name:, kind:, started_at:, ended_at:, duration_ms:, data:, error: false)
|
|
164
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
165
|
+
return unless spans
|
|
166
|
+
|
|
167
|
+
spans << {
|
|
168
|
+
span_id: SecureRandom.uuid,
|
|
169
|
+
name: name,
|
|
170
|
+
kind: kind,
|
|
171
|
+
started_at: started_at,
|
|
172
|
+
ended_at: ended_at,
|
|
173
|
+
duration_ms: duration_ms,
|
|
174
|
+
data: data,
|
|
175
|
+
error: error
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def extract_route_pattern(endpoint)
|
|
180
|
+
return '/' unless endpoint
|
|
181
|
+
|
|
182
|
+
route = endpoint.route
|
|
183
|
+
return '/' unless route
|
|
184
|
+
|
|
185
|
+
route.pattern&.path || route.path || '/'
|
|
186
|
+
rescue StandardError
|
|
187
|
+
'/'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Middleware for Grape (alternative installation)
|
|
192
|
+
# Usage: use BrainzLab::Instrumentation::GrapeInstrumentation::Middleware
|
|
193
|
+
class Middleware
|
|
194
|
+
def initialize(app)
|
|
195
|
+
@app = app
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def call(env)
|
|
199
|
+
return @app.call(env) unless should_trace?
|
|
200
|
+
|
|
201
|
+
started_at = Time.now.utc
|
|
202
|
+
request = Rack::Request.new(env)
|
|
203
|
+
|
|
204
|
+
# Initialize Pulse tracing
|
|
205
|
+
Thread.current[:brainzlab_pulse_spans] = []
|
|
206
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
207
|
+
|
|
208
|
+
# Extract parent trace context
|
|
209
|
+
parent_context = BrainzLab::Pulse.extract!(env)
|
|
210
|
+
|
|
211
|
+
begin
|
|
212
|
+
status, headers, response = @app.call(env)
|
|
213
|
+
|
|
214
|
+
record_trace(request, env, started_at, status, parent_context)
|
|
215
|
+
|
|
216
|
+
[status, headers, response]
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
record_trace(request, env, started_at, 500, parent_context, e)
|
|
219
|
+
raise
|
|
220
|
+
ensure
|
|
221
|
+
cleanup_context
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def should_trace?
|
|
228
|
+
BrainzLab.configuration.pulse_enabled
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def cleanup_context
|
|
232
|
+
Thread.current[:brainzlab_pulse_spans] = nil
|
|
233
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
234
|
+
BrainzLab::Context.clear!
|
|
235
|
+
BrainzLab::Pulse::Propagation.clear!
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def record_trace(request, env, started_at, status, parent_context, error = nil)
|
|
239
|
+
ended_at = Time.now.utc
|
|
240
|
+
duration_ms = ((ended_at - started_at) * 1000).round(2)
|
|
241
|
+
|
|
242
|
+
method = request.request_method
|
|
243
|
+
path = request.path
|
|
244
|
+
|
|
245
|
+
# Get route pattern from Grape if available
|
|
246
|
+
route_pattern = env['grape.routing_args']&.dig(:route_info)&.pattern&.path || path
|
|
247
|
+
|
|
248
|
+
spans = Thread.current[:brainzlab_pulse_spans] || []
|
|
249
|
+
|
|
250
|
+
payload = {
|
|
251
|
+
trace_id: SecureRandom.uuid,
|
|
252
|
+
name: "#{method} #{route_pattern}",
|
|
253
|
+
kind: 'request',
|
|
254
|
+
started_at: started_at.utc.iso8601(3),
|
|
255
|
+
ended_at: ended_at.utc.iso8601(3),
|
|
256
|
+
duration_ms: duration_ms,
|
|
257
|
+
request_method: method,
|
|
258
|
+
request_path: path,
|
|
259
|
+
status: status,
|
|
260
|
+
error: error.present? || status >= 500,
|
|
261
|
+
error_class: error&.class&.name,
|
|
262
|
+
error_message: error&.message&.slice(0, 1000),
|
|
263
|
+
spans: spans.map { |s| format_span(s) },
|
|
264
|
+
environment: BrainzLab.configuration.environment,
|
|
265
|
+
commit: BrainzLab.configuration.commit,
|
|
266
|
+
host: BrainzLab.configuration.host
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if parent_context&.valid?
|
|
270
|
+
payload[:parent_trace_id] = parent_context.trace_id
|
|
271
|
+
payload[:parent_span_id] = parent_context.span_id
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
BrainzLab::Pulse.client.send_trace(payload.compact)
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
BrainzLab.debug_log("Grape trace recording failed: #{e.message}")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def format_span(span)
|
|
280
|
+
{
|
|
281
|
+
span_id: span[:span_id],
|
|
282
|
+
name: span[:name],
|
|
283
|
+
kind: span[:kind],
|
|
284
|
+
started_at: span[:started_at]&.utc&.iso8601(3),
|
|
285
|
+
ended_at: span[:ended_at]&.utc&.iso8601(3),
|
|
286
|
+
duration_ms: span[:duration_ms],
|
|
287
|
+
data: span[:data]
|
|
288
|
+
}.compact
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module GraphQLInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::GraphQL::Schema)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
# For GraphQL Ruby 2.0+
|
|
14
|
+
if ::GraphQL::Schema.respond_to?(:trace_with)
|
|
15
|
+
# Will be installed per-schema via BrainzLab::GraphQL::Tracer
|
|
16
|
+
BrainzLab.debug_log('GraphQL tracer available - add `trace_with BrainzLab::Instrumentation::GraphQLInstrumentation::Tracer` to your schema')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Subscribe to ActiveSupport notifications if available
|
|
20
|
+
install_notifications!
|
|
21
|
+
|
|
22
|
+
@installed = true
|
|
23
|
+
BrainzLab.debug_log('GraphQL instrumentation installed')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def installed?
|
|
27
|
+
@installed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset!
|
|
31
|
+
@installed = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def install_notifications!
|
|
37
|
+
# GraphQL-ruby emits ActiveSupport notifications
|
|
38
|
+
ActiveSupport::Notifications.subscribe('execute.graphql') do |*args|
|
|
39
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
40
|
+
record_execution(event)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ActiveSupport::Notifications.subscribe('analyze.graphql') do |*args|
|
|
44
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
45
|
+
record_analyze(event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ActiveSupport::Notifications.subscribe('validate.graphql') do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
record_validate(event)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
BrainzLab.debug_log("GraphQL notifications setup failed: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_execution(event)
|
|
57
|
+
payload = event.payload
|
|
58
|
+
query = payload[:query]
|
|
59
|
+
operation_name = query&.operation_name || 'anonymous'
|
|
60
|
+
operation_type = query&.selected_operation&.operation_type || 'query'
|
|
61
|
+
duration_ms = event.duration.round(2)
|
|
62
|
+
|
|
63
|
+
# Add breadcrumb
|
|
64
|
+
if BrainzLab.configuration.reflex_enabled
|
|
65
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
66
|
+
"GraphQL #{operation_type} #{operation_name}",
|
|
67
|
+
category: 'graphql.execute',
|
|
68
|
+
level: payload[:errors]&.any? ? :error : :info,
|
|
69
|
+
data: {
|
|
70
|
+
operation_name: operation_name,
|
|
71
|
+
operation_type: operation_type,
|
|
72
|
+
duration_ms: duration_ms,
|
|
73
|
+
error_count: payload[:errors]&.size || 0
|
|
74
|
+
}.compact
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Record span
|
|
79
|
+
record_span(
|
|
80
|
+
name: "GraphQL #{operation_type} #{operation_name}",
|
|
81
|
+
kind: 'graphql',
|
|
82
|
+
duration_ms: duration_ms,
|
|
83
|
+
started_at: event.time,
|
|
84
|
+
ended_at: event.end,
|
|
85
|
+
data: {
|
|
86
|
+
operation_name: operation_name,
|
|
87
|
+
operation_type: operation_type,
|
|
88
|
+
query: truncate_query(query&.query_string),
|
|
89
|
+
variables: sanitize_variables(query&.variables&.to_h),
|
|
90
|
+
error_count: payload[:errors]&.size || 0
|
|
91
|
+
}.compact,
|
|
92
|
+
error: payload[:errors]&.any?
|
|
93
|
+
)
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
BrainzLab.debug_log("GraphQL execution recording failed: #{e.message}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def record_analyze(event)
|
|
99
|
+
record_span(
|
|
100
|
+
name: 'GraphQL analyze',
|
|
101
|
+
kind: 'graphql',
|
|
102
|
+
duration_ms: event.duration.round(2),
|
|
103
|
+
started_at: event.time,
|
|
104
|
+
ended_at: event.end,
|
|
105
|
+
data: { phase: 'analyze' }
|
|
106
|
+
)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
BrainzLab.debug_log("GraphQL analyze recording failed: #{e.message}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def record_validate(event)
|
|
112
|
+
record_span(
|
|
113
|
+
name: 'GraphQL validate',
|
|
114
|
+
kind: 'graphql',
|
|
115
|
+
duration_ms: event.duration.round(2),
|
|
116
|
+
started_at: event.time,
|
|
117
|
+
ended_at: event.end,
|
|
118
|
+
data: { phase: 'validate' }
|
|
119
|
+
)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
BrainzLab.debug_log("GraphQL validate recording failed: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def record_span(name:, kind:, duration_ms:, started_at:, ended_at:, data:, error: false)
|
|
125
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
126
|
+
return unless spans
|
|
127
|
+
|
|
128
|
+
spans << {
|
|
129
|
+
span_id: SecureRandom.uuid,
|
|
130
|
+
name: name,
|
|
131
|
+
kind: kind,
|
|
132
|
+
started_at: started_at,
|
|
133
|
+
ended_at: ended_at,
|
|
134
|
+
duration_ms: duration_ms,
|
|
135
|
+
data: data,
|
|
136
|
+
error: error
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def truncate_query(query)
|
|
141
|
+
return nil unless query
|
|
142
|
+
|
|
143
|
+
query.to_s[0, 2000]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sanitize_variables(variables)
|
|
147
|
+
return nil unless variables
|
|
148
|
+
|
|
149
|
+
scrub_fields = BrainzLab.configuration.scrub_fields
|
|
150
|
+
variables.transform_values do |value|
|
|
151
|
+
if scrub_fields.any? { |f| value.to_s.downcase.include?(f.to_s) }
|
|
152
|
+
'[FILTERED]'
|
|
153
|
+
else
|
|
154
|
+
value
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
rescue StandardError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# GraphQL Ruby 2.0+ Tracer module
|
|
163
|
+
# Add to your schema: trace_with BrainzLab::Instrumentation::GraphQLInstrumentation::Tracer
|
|
164
|
+
module Tracer
|
|
165
|
+
def execute_query(query:)
|
|
166
|
+
started_at = Time.now.utc
|
|
167
|
+
operation_name = query.operation_name || 'anonymous'
|
|
168
|
+
operation_type = query.selected_operation&.operation_type || 'query'
|
|
169
|
+
|
|
170
|
+
result = super
|
|
171
|
+
|
|
172
|
+
duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
|
|
173
|
+
has_errors = result.to_h['errors']&.any?
|
|
174
|
+
|
|
175
|
+
# Add breadcrumb
|
|
176
|
+
if BrainzLab.configuration.reflex_enabled
|
|
177
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
178
|
+
"GraphQL #{operation_type} #{operation_name}",
|
|
179
|
+
category: 'graphql.execute',
|
|
180
|
+
level: has_errors ? :error : :info,
|
|
181
|
+
data: {
|
|
182
|
+
operation_name: operation_name,
|
|
183
|
+
operation_type: operation_type,
|
|
184
|
+
duration_ms: duration_ms
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Record span
|
|
190
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
191
|
+
if spans
|
|
192
|
+
spans << {
|
|
193
|
+
span_id: SecureRandom.uuid,
|
|
194
|
+
name: "GraphQL #{operation_type} #{operation_name}",
|
|
195
|
+
kind: 'graphql',
|
|
196
|
+
started_at: started_at,
|
|
197
|
+
ended_at: Time.now.utc,
|
|
198
|
+
duration_ms: duration_ms,
|
|
199
|
+
data: {
|
|
200
|
+
operation_name: operation_name,
|
|
201
|
+
operation_type: operation_type
|
|
202
|
+
},
|
|
203
|
+
error: has_errors
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
result
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
# Record error
|
|
210
|
+
if BrainzLab.configuration.reflex_enabled
|
|
211
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
212
|
+
"GraphQL #{operation_type} #{operation_name} failed",
|
|
213
|
+
category: 'graphql.error',
|
|
214
|
+
level: :error,
|
|
215
|
+
data: { error: e.class.name }
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
raise
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def execute_field(field:, query:, ast_node:, arguments:, object:)
|
|
222
|
+
started_at = Time.now.utc
|
|
223
|
+
|
|
224
|
+
result = super
|
|
225
|
+
|
|
226
|
+
duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
|
|
227
|
+
|
|
228
|
+
# Only track slow field resolutions (> 10ms) to avoid noise
|
|
229
|
+
if duration_ms > 10
|
|
230
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
231
|
+
if spans
|
|
232
|
+
spans << {
|
|
233
|
+
span_id: SecureRandom.uuid,
|
|
234
|
+
name: "GraphQL field #{field.owner.graphql_name}.#{field.graphql_name}",
|
|
235
|
+
kind: 'graphql.field',
|
|
236
|
+
started_at: started_at,
|
|
237
|
+
ended_at: Time.now.utc,
|
|
238
|
+
duration_ms: duration_ms,
|
|
239
|
+
data: {
|
|
240
|
+
field: field.graphql_name,
|
|
241
|
+
parent_type: field.owner.graphql_name
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
result
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|