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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. 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