brainzlab 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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +26 -0
  4. data/README.md +311 -0
  5. data/lib/brainzlab/configuration.rb +215 -0
  6. data/lib/brainzlab/context.rb +91 -0
  7. data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
  8. data/lib/brainzlab/instrumentation/active_record.rb +111 -0
  9. data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
  10. data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
  11. data/lib/brainzlab/instrumentation/faraday.rb +182 -0
  12. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  13. data/lib/brainzlab/instrumentation/graphql.rb +251 -0
  14. data/lib/brainzlab/instrumentation/httparty.rb +194 -0
  15. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  16. data/lib/brainzlab/instrumentation/net_http.rb +109 -0
  17. data/lib/brainzlab/instrumentation/redis.rb +331 -0
  18. data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
  19. data/lib/brainzlab/instrumentation.rb +132 -0
  20. data/lib/brainzlab/pulse/client.rb +132 -0
  21. data/lib/brainzlab/pulse/instrumentation.rb +364 -0
  22. data/lib/brainzlab/pulse/propagation.rb +241 -0
  23. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  24. data/lib/brainzlab/pulse/tracer.rb +111 -0
  25. data/lib/brainzlab/pulse.rb +224 -0
  26. data/lib/brainzlab/rails/log_formatter.rb +801 -0
  27. data/lib/brainzlab/rails/log_subscriber.rb +341 -0
  28. data/lib/brainzlab/rails/railtie.rb +590 -0
  29. data/lib/brainzlab/recall/buffer.rb +64 -0
  30. data/lib/brainzlab/recall/client.rb +86 -0
  31. data/lib/brainzlab/recall/logger.rb +118 -0
  32. data/lib/brainzlab/recall/provisioner.rb +113 -0
  33. data/lib/brainzlab/recall.rb +155 -0
  34. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  35. data/lib/brainzlab/reflex/client.rb +85 -0
  36. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  37. data/lib/brainzlab/reflex.rb +374 -0
  38. data/lib/brainzlab/version.rb +5 -0
  39. data/lib/brainzlab-sdk.rb +3 -0
  40. data/lib/brainzlab.rb +140 -0
  41. data/lib/generators/brainzlab/install/install_generator.rb +61 -0
  42. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  43. metadata +159 -0
@@ -0,0 +1,251 @@
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
+ query.to_s[0, 2000]
143
+ end
144
+
145
+ def sanitize_variables(variables)
146
+ return nil unless variables
147
+
148
+ scrub_fields = BrainzLab.configuration.scrub_fields
149
+ variables.transform_values do |value|
150
+ if scrub_fields.any? { |f| value.to_s.downcase.include?(f.to_s) }
151
+ "[FILTERED]"
152
+ else
153
+ value
154
+ end
155
+ end
156
+ rescue StandardError
157
+ nil
158
+ end
159
+ end
160
+
161
+ # GraphQL Ruby 2.0+ Tracer module
162
+ # Add to your schema: trace_with BrainzLab::Instrumentation::GraphQLInstrumentation::Tracer
163
+ module Tracer
164
+ def execute_query(query:)
165
+ started_at = Time.now.utc
166
+ operation_name = query.operation_name || "anonymous"
167
+ operation_type = query.selected_operation&.operation_type || "query"
168
+
169
+ result = super
170
+
171
+ duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
172
+ has_errors = result.to_h["errors"]&.any?
173
+
174
+ # Add breadcrumb
175
+ if BrainzLab.configuration.reflex_enabled
176
+ BrainzLab::Reflex.add_breadcrumb(
177
+ "GraphQL #{operation_type} #{operation_name}",
178
+ category: "graphql.execute",
179
+ level: has_errors ? :error : :info,
180
+ data: {
181
+ operation_name: operation_name,
182
+ operation_type: operation_type,
183
+ duration_ms: duration_ms
184
+ }
185
+ )
186
+ end
187
+
188
+ # Record span
189
+ spans = Thread.current[:brainzlab_pulse_spans]
190
+ if spans
191
+ spans << {
192
+ span_id: SecureRandom.uuid,
193
+ name: "GraphQL #{operation_type} #{operation_name}",
194
+ kind: "graphql",
195
+ started_at: started_at,
196
+ ended_at: Time.now.utc,
197
+ duration_ms: duration_ms,
198
+ data: {
199
+ operation_name: operation_name,
200
+ operation_type: operation_type
201
+ },
202
+ error: has_errors
203
+ }
204
+ end
205
+
206
+ result
207
+ rescue StandardError => e
208
+ # Record error
209
+ if BrainzLab.configuration.reflex_enabled
210
+ BrainzLab::Reflex.add_breadcrumb(
211
+ "GraphQL #{operation_type} #{operation_name} failed",
212
+ category: "graphql.error",
213
+ level: :error,
214
+ data: { error: e.class.name }
215
+ )
216
+ end
217
+ raise
218
+ end
219
+
220
+ def execute_field(field:, query:, ast_node:, arguments:, object:)
221
+ started_at = Time.now.utc
222
+
223
+ result = super
224
+
225
+ duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
226
+
227
+ # Only track slow field resolutions (> 10ms) to avoid noise
228
+ if duration_ms > 10
229
+ spans = Thread.current[:brainzlab_pulse_spans]
230
+ if spans
231
+ spans << {
232
+ span_id: SecureRandom.uuid,
233
+ name: "GraphQL field #{field.owner.graphql_name}.#{field.graphql_name}",
234
+ kind: "graphql.field",
235
+ started_at: started_at,
236
+ ended_at: Time.now.utc,
237
+ duration_ms: duration_ms,
238
+ data: {
239
+ field: field.graphql_name,
240
+ parent_type: field.owner.graphql_name
241
+ }
242
+ }
243
+ end
244
+ end
245
+
246
+ result
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module HTTPartyInstrumentation
6
+ @installed = false
7
+
8
+ class << self
9
+ def install!
10
+ return unless defined?(::HTTParty)
11
+ return if @installed
12
+
13
+ ::HTTParty.singleton_class.prepend(Patch)
14
+
15
+ @installed = true
16
+ BrainzLab.debug_log("HTTParty instrumentation installed")
17
+ end
18
+
19
+ def installed?
20
+ @installed
21
+ end
22
+
23
+ def reset!
24
+ @installed = false
25
+ end
26
+ end
27
+
28
+ module Patch
29
+ def perform_request(http_method, path, options = {}, &block)
30
+ return super unless should_track?(path, options)
31
+
32
+ # Inject distributed tracing headers
33
+ options = inject_trace_context(options)
34
+
35
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ error_info = nil
37
+
38
+ begin
39
+ response = super
40
+ track_request(http_method, path, options, response.code, started_at)
41
+ response
42
+ rescue StandardError => e
43
+ error_info = e.class.name
44
+ track_request(http_method, path, options, nil, started_at, error_info)
45
+ raise
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def should_track?(path, options)
52
+ return false unless BrainzLab.configuration.instrument_http
53
+
54
+ uri = parse_uri(path, options)
55
+ return true unless uri
56
+
57
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
58
+ !ignore_hosts.include?(uri.host)
59
+ end
60
+
61
+ def inject_trace_context(options)
62
+ return options unless BrainzLab.configuration.pulse_enabled
63
+
64
+ options = options.dup
65
+ options[:headers] ||= {}
66
+
67
+ trace_headers = {}
68
+ BrainzLab::Pulse.inject(trace_headers, format: :all)
69
+
70
+ options[:headers] = options[:headers].merge(trace_headers)
71
+ options
72
+ rescue StandardError => e
73
+ BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
74
+ options
75
+ end
76
+
77
+ def track_request(http_method, path, options, status, started_at, error = nil)
78
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
79
+ method = extract_method_name(http_method)
80
+ uri = parse_uri(path, options)
81
+ url = uri ? sanitize_url(uri) : path.to_s
82
+ host = uri&.host || "unknown"
83
+ request_path = uri&.path || path.to_s
84
+ level = error || (status && status >= 400) ? :error : :info
85
+
86
+ # Add breadcrumb for Reflex
87
+ if BrainzLab.configuration.reflex_enabled
88
+ BrainzLab::Reflex.add_breadcrumb(
89
+ "#{method} #{url}",
90
+ category: "http.httparty",
91
+ level: level,
92
+ data: {
93
+ method: method,
94
+ url: url,
95
+ host: host,
96
+ path: request_path,
97
+ status_code: status,
98
+ duration_ms: duration_ms,
99
+ error: error
100
+ }.compact
101
+ )
102
+ end
103
+
104
+ # Record span for Pulse APM
105
+ record_pulse_span(method, host, request_path, status, duration_ms, error)
106
+
107
+ # Log to Recall at debug level
108
+ if BrainzLab.configuration.recall_enabled
109
+ BrainzLab::Recall.debug(
110
+ "HTTP #{method} #{url} -> #{status || 'ERROR'}",
111
+ method: method,
112
+ url: url,
113
+ host: host,
114
+ status_code: status,
115
+ duration_ms: duration_ms,
116
+ error: error
117
+ )
118
+ end
119
+ rescue StandardError => e
120
+ BrainzLab.debug_log("HTTParty instrumentation error: #{e.message}")
121
+ end
122
+
123
+ def record_pulse_span(method, host, path, status, duration_ms, error)
124
+ spans = Thread.current[:brainzlab_pulse_spans]
125
+ return unless spans
126
+
127
+ span = {
128
+ span_id: SecureRandom.uuid,
129
+ name: "HTTP #{method} #{host}",
130
+ kind: "http",
131
+ started_at: Time.now.utc - (duration_ms / 1000.0),
132
+ ended_at: Time.now.utc,
133
+ duration_ms: duration_ms,
134
+ data: {
135
+ method: method,
136
+ host: host,
137
+ path: path,
138
+ status: status
139
+ }.compact
140
+ }
141
+
142
+ if error
143
+ span[:error] = true
144
+ span[:error_class] = error
145
+ end
146
+
147
+ spans << span
148
+ end
149
+
150
+ def extract_method_name(http_method)
151
+ case http_method.name
152
+ when /Get$/ then "GET"
153
+ when /Post$/ then "POST"
154
+ when /Put$/ then "PUT"
155
+ when /Patch$/ then "PATCH"
156
+ when /Delete$/ then "DELETE"
157
+ when /Head$/ then "HEAD"
158
+ when /Options$/ then "OPTIONS"
159
+ else http_method.name.split("::").last.upcase
160
+ end
161
+ end
162
+
163
+ def parse_uri(path, options)
164
+ base_uri = options[:base_uri]
165
+ if base_uri
166
+ URI.join(base_uri.to_s, path.to_s)
167
+ else
168
+ URI.parse(path.to_s)
169
+ end
170
+ rescue URI::InvalidURIError
171
+ nil
172
+ end
173
+
174
+ def sanitize_url(uri)
175
+ result = uri.dup
176
+ if result.query
177
+ params = URI.decode_www_form(result.query).reject do |key, _|
178
+ sensitive_param?(key)
179
+ end
180
+ result.query = params.empty? ? nil : URI.encode_www_form(params)
181
+ end
182
+ result.to_s
183
+ rescue StandardError
184
+ uri.to_s
185
+ end
186
+
187
+ def sensitive_param?(key)
188
+ key = key.to_s.downcase
189
+ %w[token api_key apikey secret password auth key].any? { |s| key.include?(s) }
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module MongoDBInstrumentation
6
+ @installed = false
7
+
8
+ class << self
9
+ def install!
10
+ return if @installed
11
+
12
+ installed_any = false
13
+
14
+ # Install MongoDB Ruby Driver monitoring
15
+ if defined?(::Mongo::Client)
16
+ install_mongo_driver!
17
+ installed_any = true
18
+ end
19
+
20
+ # Install Mongoid APM subscriber
21
+ if defined?(::Mongoid)
22
+ install_mongoid!
23
+ installed_any = true
24
+ end
25
+
26
+ return unless installed_any
27
+
28
+ @installed = true
29
+ BrainzLab.debug_log("MongoDB instrumentation installed")
30
+ end
31
+
32
+ def installed?
33
+ @installed
34
+ end
35
+
36
+ def reset!
37
+ @installed = false
38
+ end
39
+
40
+ private
41
+
42
+ def install_mongo_driver!
43
+ # Subscribe to command monitoring events
44
+ subscriber = CommandSubscriber.new
45
+
46
+ ::Mongo::Monitoring::Global.subscribe(
47
+ ::Mongo::Monitoring::COMMAND,
48
+ subscriber
49
+ )
50
+ end
51
+
52
+ def install_mongoid!
53
+ # For Mongoid 7+, use the APM module
54
+ if ::Mongoid.respond_to?(:subscribe)
55
+ ::Mongoid.subscribe(CommandSubscriber.new)
56
+ end
57
+ end
58
+ end
59
+
60
+ # MongoDB Command Subscriber
61
+ class CommandSubscriber
62
+ SKIP_COMMANDS = %w[isMaster ismaster buildInfo getLastError saslStart saslContinue].freeze
63
+
64
+ def initialize
65
+ @commands = {}
66
+ end
67
+
68
+ # Called when command starts
69
+ def started(event)
70
+ return if skip_command?(event.command_name)
71
+
72
+ @commands[event.request_id] = {
73
+ started_at: Time.now.utc,
74
+ command_name: event.command_name,
75
+ database: event.database_name,
76
+ collection: extract_collection(event)
77
+ }
78
+ end
79
+
80
+ # Called when command succeeds
81
+ def succeeded(event)
82
+ record_command(event, success: true)
83
+ end
84
+
85
+ # Called when command fails
86
+ def failed(event)
87
+ record_command(event, success: false, error: event.message)
88
+ end
89
+
90
+ private
91
+
92
+ def skip_command?(command_name)
93
+ SKIP_COMMANDS.include?(command_name.to_s)
94
+ end
95
+
96
+ def extract_collection(event)
97
+ # Try to extract collection name from command
98
+ cmd = event.command
99
+ cmd["collection"] || cmd[event.command_name] || cmd.keys.first
100
+ rescue StandardError
101
+ nil
102
+ end
103
+
104
+ def record_command(event, success:, error: nil)
105
+ command_data = @commands.delete(event.request_id)
106
+ return unless command_data
107
+
108
+ duration_ms = event.duration * 1000 # Convert seconds to ms
109
+ command_name = command_data[:command_name]
110
+ collection = command_data[:collection]
111
+ database = command_data[:database]
112
+
113
+ level = success ? :info : :error
114
+
115
+ # Add breadcrumb for Reflex
116
+ if BrainzLab.configuration.reflex_enabled
117
+ BrainzLab::Reflex.add_breadcrumb(
118
+ "MongoDB #{command_name}",
119
+ category: "mongodb",
120
+ level: level,
121
+ data: {
122
+ command: command_name,
123
+ collection: collection,
124
+ database: database,
125
+ duration_ms: duration_ms.round(2),
126
+ error: error
127
+ }.compact
128
+ )
129
+ end
130
+
131
+ # Record span for Pulse
132
+ record_span(
133
+ command_name: command_name,
134
+ collection: collection,
135
+ database: database,
136
+ started_at: command_data[:started_at],
137
+ duration_ms: duration_ms,
138
+ success: success,
139
+ error: error
140
+ )
141
+
142
+ # Log to Recall
143
+ if BrainzLab.configuration.recall_enabled
144
+ log_method = success ? :debug : :warn
145
+ BrainzLab::Recall.send(
146
+ log_method,
147
+ "MongoDB #{command_name} #{collection} (#{duration_ms.round(2)}ms)",
148
+ command: command_name,
149
+ collection: collection,
150
+ database: database,
151
+ duration_ms: duration_ms.round(2),
152
+ error: error
153
+ )
154
+ end
155
+ rescue StandardError => e
156
+ BrainzLab.debug_log("MongoDB command recording failed: #{e.message}")
157
+ end
158
+
159
+ def record_span(command_name:, collection:, database:, started_at:, duration_ms:, success:, error:)
160
+ spans = Thread.current[:brainzlab_pulse_spans]
161
+ return unless spans
162
+
163
+ span = {
164
+ span_id: SecureRandom.uuid,
165
+ name: "MongoDB #{command_name} #{collection}".strip,
166
+ kind: "mongodb",
167
+ started_at: started_at,
168
+ ended_at: Time.now.utc,
169
+ duration_ms: duration_ms.round(2),
170
+ data: {
171
+ command: command_name,
172
+ collection: collection,
173
+ database: database
174
+ }.compact
175
+ }
176
+
177
+ unless success
178
+ span[:error] = true
179
+ span[:error_message] = error
180
+ end
181
+
182
+ spans << span
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end